10 if (!
object)
return nullptr;
11 const auto found =
object->find(
key);
12 return found ==
object->end() ? nullptr : &
found->second;
18 buses_.emplace(ObjectId(
"master"),
AudioBusSnapshot{ObjectId(
"master"), {},
"Master"});
22 return {TargetId(id_),
"audio-mixer",
revisionValue(),
false, {CapabilityId(
"eve.editor.target.audio-mixer")}};
26 return capability == CapabilityId(
"eve.editor.target.audio-mixer") ? this :
nullptr;
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);
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();
57 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId(
"editor.audio.mixer-operation"),
58 "Unsupported mixer operation: " +
operation.type);
62 return eve::editing::applied<void>();
66 return std::make_unique<AudioMixerTarget>(*
this);
70 std::unique_ptr<IDomainOperationTarget> candidate) {
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");
76 return eve::editing::applied<void>();
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);
88 std::vector<ObjectId> result;
89 for (
const auto& [
id,
bus] : buses_)
90 if (
bus.parent ==
parent) result.push_back(
id);
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");
101 operation.inverseType =
"audio.bus.delete.v1";
107 return eve::editing::applied<DomainOperation>(std::move(
operation));
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");
117 operation.inverseType =
"audio.bus.create.v1";
122 operation.affectedObjects.push_back({TargetId(id_),
id.
value(), 0});
123 return eve::editing::applied<DomainOperation>(std::move(
operation));
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");
140 operation.affectedObjects.push_back({TargetId(id_), changed.
id.value(), 0});
142 operation.mergeKey =
"audio.bus:" + changed.
id.value();
143 return eve::editing::applied<DomainOperation>(std::move(
operation));
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;
157EditorValue AudioMixerTarget::busValue(
const AudioBusSnapshot& bus) {
160 value[
"parent"] =
bus.parent.value();
169Result<AudioBusSnapshot> AudioMixerTarget::parseBus(
const EditorValue&
value) {
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>(
187 for (
const auto& [
id,
bus] : buses_) {
189 buses.push_back(busValue(
bus));
192 root[
"schemaVersion"] = 1;
198 const EditorValue* versionValue = field(snapshot,
"schemaVersion");
199 const EditorValue* busesValue = field(snapshot,
"buses");
200 const auto* version = versionValue ? versionValue->
getIf<int64_t>() :
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;
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");
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")) {
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;
233 buses_ = std::move(candidate);
236 return eve::editing::applied<void>();
ActionParameterOperation operation
Move-only operation result carrying either a value or Status.
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
bool empty() const noexcept
True when no compatibility spelling has been assigned.
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.