载入中...
搜索中...
未找到
AudioMixerTarget.cpp
浏览该文件的文档.
2
3#include <utility>
4
5namespace eve::audio_editing {
6namespace {
7
8const EditorValue* field(const EditorValue& value, const char* key) {
9 const auto* object = value.getIf<EditorValue::Object>();
10 if (!object) return nullptr;
11 const auto found = object->find(key);
12 return found == object->end() ? nullptr : &found->second;
13}
14
15} // namespace
16
17AudioMixerTarget::AudioMixerTarget(std::string id) : id_(std::move(id)) {
18 buses_.emplace(ObjectId("master"), AudioBusSnapshot{ObjectId("master"), {}, "Master"});
19}
20
22 return {TargetId(id_), "audio-mixer", revisionValue(), false, {CapabilityId("eve.editor.target.audio-mixer")}};
23}
24
25void* AudioMixerTarget::queryCapability(const CapabilityId& capability) {
26 return capability == CapabilityId("eve.editor.target.audio-mixer") ? this : nullptr;
27}
28
30 if (operation.target != TargetId(id_))
31 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-target"),
32 "Mixer operation targets another document");
33 auto parsed = parseBus(operation.payload);
34 if (!parsed.ok())
35 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-payload"),
36 "Mixer operation contains an invalid bus");
37 if (operation.type == "audio.bus.create.v1") {
38 if (buses_.contains(parsed.value().id) ||
39 (!parsed.value().parent.empty() && !buses_.contains(parsed.value().parent)))
40 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.audio.bus-create"),
41 "Mixer bus already exists or its parent is missing");
42 buses_.emplace(parsed.value().id, parsed.value());
43 } else if (operation.type == "audio.bus.delete.v1") {
44 if (parsed.value().id == ObjectId("master") || !buses_.contains(parsed.value().id) ||
45 !children(parsed.value().id).empty())
46 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.bus-delete"),
47 "Master, missing, or non-leaf mixer bus cannot be deleted");
48 buses_.erase(parsed.value().id);
49 } else if (operation.type == "audio.bus.replace.v1") {
50 if (!buses_.contains(parsed.value().id) || parsed.value().id == ObjectId("master") ||
51 !buses_.contains(parsed.value().parent) ||
52 wouldCycle(parsed.value().id, parsed.value().parent))
53 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.bus-replace"),
54 "Mixer bus replacement produces an invalid hierarchy");
55 buses_[parsed.value().id] = parsed.value();
56 } else {
57 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.audio.mixer-operation"),
58 "Unsupported mixer operation: " + operation.type);
59 }
61 widenDirty(0, 0);
62 return eve::editing::applied<void>();
63}
64
65std::unique_ptr<IDomainOperationTarget> AudioMixerTarget::cloneDomainState() const {
66 return std::make_unique<AudioMixerTarget>(*this);
67}
68
70 std::unique_ptr<IDomainOperationTarget> candidate) {
71 auto* typed = dynamic_cast<AudioMixerTarget*>(candidate.get());
72 if (!typed || typed->id_ != id_)
73 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.audio.mixer-candidate-mismatch"),
74 "Mixer candidate belongs to another target");
75 *this = *typed;
76 return eve::editing::applied<void>();
77}
78
80 const auto found = buses_.find(id);
81 if (found == buses_.end())
82 return eve::editing::failed<AudioBusSnapshot>(EditorStatus::NotFound, RuleId("editor.audio.bus-not-found"),
83 "Mixer bus does not exist: " + id.value());
84 return eve::editing::applied<AudioBusSnapshot>(found->second);
85}
86
87std::vector<ObjectId> AudioMixerTarget::children(const ObjectId& parent) const {
88 std::vector<ObjectId> result;
89 for (const auto& [id, bus] : buses_)
90 if (bus.parent == parent) result.push_back(id);
91 return result;
92}
93
95 if (bus.id.empty() || bus.id == ObjectId("master") || bus.name.empty() ||
96 bus.volume < 0.0 || buses_.contains(bus.id) || !buses_.contains(bus.parent))
97 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.audio.bus-create"),
98 "Mixer bus id/name/volume/parent is invalid");
100 operation.type = "audio.bus.create.v1";
101 operation.inverseType = "audio.bus.delete.v1";
102 operation.target = TargetId(id_);
103 operation.payload = busValue(bus);
104 operation.inverse = operation.payload;
105 operation.hasInverse = true;
106 operation.affectedObjects.push_back({TargetId(id_), bus.id.value(), 0});
107 return eve::editing::applied<DomainOperation>(std::move(operation));
108}
109
111 const auto found = buses_.find(id);
112 if (found == buses_.end() || id == ObjectId("master") || !children(id).empty())
113 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.audio.bus-delete"),
114 "Only an existing leaf bus can be deleted");
116 operation.type = "audio.bus.delete.v1";
117 operation.inverseType = "audio.bus.create.v1";
118 operation.target = TargetId(id_);
119 operation.payload = busValue(found->second);
120 operation.inverse = operation.payload;
121 operation.hasInverse = true;
122 operation.affectedObjects.push_back({TargetId(id_), id.value(), 0});
123 return eve::editing::applied<DomainOperation>(std::move(operation));
124}
125
127 const auto found = buses_.find(changed.id);
128 if (found == buses_.end() || changed.id == ObjectId("master") || changed.name.empty() ||
129 changed.volume < 0.0 || !buses_.contains(changed.parent) ||
130 wouldCycle(changed.id, changed.parent))
131 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.audio.bus-replace"),
132 "Mixer bus settings or hierarchy are invalid");
134 operation.type = "audio.bus.replace.v1";
135 operation.inverseType = operation.type;
136 operation.target = TargetId(id_);
137 operation.payload = busValue(changed);
138 operation.inverse = busValue(found->second);
139 operation.hasInverse = true;
140 operation.affectedObjects.push_back({TargetId(id_), changed.id.value(), 0});
141 operation.affectedProperties = {"bus"};
142 operation.mergeKey = "audio.bus:" + changed.id.value();
143 return eve::editing::applied<DomainOperation>(std::move(operation));
144}
145
146bool AudioMixerTarget::wouldCycle(const ObjectId& id, const ObjectId& parent) const {
147 ObjectId ancestor = parent;
148 while (!ancestor.empty()) {
149 if (ancestor == id) return true;
150 const auto found = buses_.find(ancestor);
151 if (found == buses_.end()) return false;
152 ancestor = found->second.parent;
153 }
154 return false;
155}
156
157EditorValue AudioMixerTarget::busValue(const AudioBusSnapshot& bus) {
159 value["id"] = bus.id.value();
160 value["parent"] = bus.parent.value();
161 value["name"] = bus.name;
162 value["volume"] = bus.volume;
163 value["mute"] = bus.mute;
164 value["solo"] = bus.solo;
165 value["effects"] = bus.effects;
166 return EditorValue(std::move(value));
167}
168
169Result<AudioBusSnapshot> AudioMixerTarget::parseBus(const EditorValue& value) {
170 const auto* id = field(value, "id") ? field(value, "id")->getIf<std::string>() : nullptr;
171 const auto* parent = field(value, "parent") ? field(value, "parent")->getIf<std::string>() : nullptr;
172 const auto* name = field(value, "name") ? field(value, "name")->getIf<std::string>() : nullptr;
173 const auto* volume = field(value, "volume") ? field(value, "volume")->getIf<double>() : nullptr;
174 const auto* mute = field(value, "mute") ? field(value, "mute")->getIf<bool>() : nullptr;
175 const auto* solo = field(value, "solo") ? field(value, "solo")->getIf<bool>() : nullptr;
176 const EditorValue* effects = field(value, "effects");
177 if (!id || id->empty() || !parent || !name || name->empty() || !volume || *volume < 0.0 ||
178 !mute || !solo || !effects || effects->type() != EditorValue::Type::Array)
179 return eve::editing::failed<AudioBusSnapshot>(EditorStatus::Rejected, RuleId("editor.audio.bus-value"),
180 "Mixer bus value is invalid");
181 return eve::editing::applied<AudioBusSnapshot>(
182 {ObjectId(*id), ObjectId(*parent), *name, *volume, *mute, *solo, *effects});
183}
184
186 EditorValue::Array buses;
187 for (const auto& [id, bus] : buses_) {
188 (void)id;
189 buses.push_back(busValue(bus));
190 }
192 root["schemaVersion"] = 1;
193 root["buses"] = EditorValue(std::move(buses));
194 return EditorValue(std::move(root));
195}
196
198 const EditorValue* versionValue = field(snapshot, "schemaVersion");
199 const EditorValue* busesValue = field(snapshot, "buses");
200 const auto* version = versionValue ? versionValue->getIf<int64_t>() : nullptr;
201 const auto* buses = busesValue ? busesValue->getIf<EditorValue::Array>() : nullptr;
202 if (!version || *version != 1 || !buses)
203 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-snapshot"),
204 "Mixer snapshot requires schemaVersion 1 and buses");
205 std::map<ObjectId, AudioBusSnapshot> candidate;
206 for (const EditorValue& value : *buses) {
207 auto parsed = parseBus(value);
208 if (!parsed.ok() || !candidate.emplace(parsed.value().id, parsed.value()).second)
209 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-snapshot-bus"),
210 "Mixer snapshot contains an invalid or duplicate bus");
211 }
212 const auto master = candidate.find(ObjectId("master"));
213 if (master == candidate.end() || !master->second.parent.empty())
214 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-master"),
215 "Mixer snapshot requires one root master bus");
216 for (const auto& [id, bus] : candidate) {
217 if (id == ObjectId("master")) continue;
218 if (!candidate.contains(bus.parent))
219 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-parent"),
220 "Mixer snapshot contains a missing parent bus");
221 ObjectId ancestor = bus.parent;
222 while (ancestor != ObjectId("master")) {
223 if (ancestor == id)
224 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-cycle"),
225 "Mixer snapshot contains a routing cycle");
226 const auto found = candidate.find(ancestor);
227 if (found == candidate.end() || found->second.parent.empty())
228 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.mixer-root"),
229 "Every mixer bus must route to master");
230 ancestor = found->second.parent;
231 }
232 }
233 buses_ = std::move(candidate);
234 bumpRevision();
236 return eve::editing::applied<void>();
237}
238
239} // namespace eve::audio_editing
ActionParameterOperation operation
double value
int root
Definition AnimSmr.cpp:119
double volume
std::uint32_t key
std::int32_t parent
std::string name
std::string id
Definition PlayHost.cpp:108
bool found
int children
Definition TreeMesh.cpp:295
Move-only operation result carrying either a value or Status.
Definition Result.h:155
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
bool empty() const noexcept
True when no compatibility spelling has been assigned.
Definition Identity.h:374
Serializable mixer-bus hierarchy, including master bus.
TargetDescriptor describe() const override
Describe.
Result< AudioBusSnapshot > bus(const ObjectId &id) const
Read one bus snapshot.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
EditorValue snapshotValue() const
Capture deterministic mixer hierarchy content.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load and validate a versioned mixer hierarchy.
std::vector< ObjectId > children(const ObjectId &parent) const
Enumerate direct child buses in stable order.
Result< DomainOperation > makeCreate(AudioBusSnapshot bus) const
Plan a reversible bus creation.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
AudioMixerTarget(std::string id)
Audio mixer target.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
Result< DomainOperation > makeDelete(const ObjectId &id) const
Plan a reversible leaf-bus deletion; master cannot be deleted.
Result< DomainOperation > makeReplace(AudioBusSnapshot bus) const
Plan a reversible bus settings/reparent change.
void bumpRevision()
Advance the revision without touching the dirty region.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
virtual void clearDirtyRegion()=0
Clears dirty region.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
editing::ObjectId ObjectId
editing::Value EditorValue
const EditorValue * field(const EditorValue &value, const char *name)
Immutable mixer bus snapshot for hierarchy and routing panels.
DomainOperation public API.
Stable discovery metadata exposed by every editable target.