载入中...
搜索中...
未找到
AudioEffects.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <set>
6#include <utility>
7
8namespace eve::audio_editing {
9namespace {
10const EditorValue* field(const EditorValue& value, const char* key) {
11 const auto* object = value.getIf<EditorValue::Object>();
12 if (!object) return nullptr;
13 const auto found = object->find(key);
14 return found == object->end() ? nullptr : &found->second;
15}
16EditorValue effectValue(const AudioEffectRecord& effect) {
17 return EditorValue::Object{{"id", effect.id.value()},
18 {"type", effect.type},
19 {"bypass", effect.bypass},
20 {"mix", effect.mix},
21 {"parameters", effect.parameters}};
22}
23bool numberIn(const EditorValue::Object& parameters, const char* key, double minimum, double maximum) {
24 const auto found = parameters.find(key);
25 const auto* number = found == parameters.end() ? nullptr : found->second.getIf<double>();
26 return number && std::isfinite(*number) && *number >= minimum && *number <= maximum;
27}
28Result<AudioEffectRecord> parseEffect(const EditorValue& value) {
29 const auto *idEntry = field(value, "id"), *typeEntry = field(value, "type"), *bypassEntry = field(value, "bypass"),
30 *mixEntry = field(value, "mix"), *parametersEntry = field(value, "parameters");
31 const auto* id = idEntry ? idEntry->getIf<std::string>() : nullptr;
32 const auto* type = typeEntry ? typeEntry->getIf<std::string>() : nullptr;
33 const auto* bypass = bypassEntry ? bypassEntry->getIf<bool>() : nullptr;
34 const auto* mix = mixEntry ? mixEntry->getIf<double>() : nullptr;
35 const auto* parameters = parametersEntry ? parametersEntry->getIf<EditorValue::Object>() : nullptr;
36 static const std::set<std::string> types{"gain", "low-pass", "high-pass", "delay", "reverb", "compressor"};
37 bool valid = id && !id->empty() && type && types.contains(*type) && bypass && mix && std::isfinite(*mix) &&
38 *mix >= 0.0 && *mix <= 1.0 && parameters;
39 if (valid && *type == "gain")
40 valid = numberIn(*parameters, "decibels", -96.0, 24.0);
41 else if (valid && (*type == "low-pass" || *type == "high-pass"))
42 valid = numberIn(*parameters, "cutoff", 20.0, 24000.0) && numberIn(*parameters, "resonance", 0.1, 20.0);
43 else if (valid && *type == "delay")
44 valid = numberIn(*parameters, "seconds", 0.001, 5.0) && numberIn(*parameters, "feedback", 0.0, 0.99);
45 else if (valid && *type == "reverb")
46 valid = numberIn(*parameters, "room-size", 0.0, 1.0) && numberIn(*parameters, "damping", 0.0, 1.0);
47 else if (valid && *type == "compressor")
48 valid = numberIn(*parameters, "threshold", -96.0, 0.0) && numberIn(*parameters, "ratio", 1.0, 100.0);
49 if (!valid)
50 return eve::editing::failed<AudioEffectRecord>(EditorStatus::Rejected, RuleId("editor.audio.invalid-effect"),
51 "Effect identity, type, mix or typed parameters are invalid");
52 return eve::editing::applied<AudioEffectRecord>({StableId(*id), *type, *bypass, *mix, *parametersEntry});
53}
54EditorValue orderValue(const std::vector<StableId>& order) {
55 EditorValue::Array result;
56 for (const auto& id : order) result.push_back(id.value());
57 return result;
58}
59Result<std::vector<StableId>> parseOrder(const EditorValue& value) {
60 const auto* array = value.getIf<EditorValue::Array>();
61 if (!array)
62 return eve::editing::failed<std::vector<StableId>>(
63 EditorStatus::Rejected, RuleId("editor.audio.invalid-effect-order"), "Effect order must be an array");
64 std::vector<StableId> result;
65 std::set<StableId> unique;
66 for (const auto& entry : *array) {
67 const auto* id = entry.getIf<std::string>();
68 if (!id || id->empty() || !unique.emplace(*id).second)
69 return eve::editing::failed<std::vector<StableId>>(EditorStatus::Rejected,
70 RuleId("editor.audio.invalid-effect-order"),
71 "Effect order ids must be non-empty and unique");
72 result.emplace_back(*id);
73 }
74 return eve::editing::applied<std::vector<StableId>>(std::move(result));
75}
76DomainOperation operation(const char* type, const char* inverse, const std::string& target, EditorValue payload,
77 EditorValue undo, const StableId& id = {}) {
78 DomainOperation result;
79 result.type = type;
80 result.inverseType = inverse;
81 result.target = TargetId(target);
82 result.payload = std::move(payload);
83 result.inverse = std::move(undo);
84 result.hasInverse = true;
85 if (!id.empty()) result.affectedObjects.push_back({TargetId(target), id.value(), 0});
86 return result;
87}
88} // namespace
89
92 return {
93 TargetId(id_), "audio-effect-chain", revisionValue(), false, {CapabilityId("eve.editor.target.audio-effects")}};
94}
95void* AudioEffectChainTarget::queryCapability(const CapabilityId& capability) {
96 return capability == CapabilityId("eve.editor.target.audio-effects") ? static_cast<AudioEffectChainTarget*>(this)
97 : nullptr;
98}
100 if (op.target != TargetId(id_))
101 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.effect-target"),
102 "Effect operation targets another chain");
103 if (op.type == "audio.effect.set.v1") {
104 auto parsed = parseEffect(op.payload);
105 if (!parsed.ok()) return Result<void>::failure(parsed.status());
106 if (!effects_.contains(parsed.value().id)) order_.push_back(parsed.value().id);
107 effects_.insert_or_assign(parsed.value().id, parsed.value());
108 } else if (op.type == "audio.effect.delete.v1") {
109 const auto* id = op.payload.getIf<std::string>();
110 if (!id || !effects_.erase(StableId(*id)))
111 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.audio.effect-not-found"),
112 "Effect was not found");
113 std::erase(order_, StableId(*id));
114 } else if (op.type == "audio.effect.reorder.v1") {
115 auto order = parseOrder(op.payload);
116 if (!order.ok() || order.value().size() != effects_.size() ||
117 std::any_of(order.value().begin(), order.value().end(),
118 [&](const StableId& id) { return !effects_.contains(id); }))
119 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.effect-order"),
120 "Effect order must contain every effect exactly once");
121 order_ = std::move(order.value());
122 } else
123 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.effect-operation"),
124 "Unsupported effect operation");
125 bumpRevision();
126 widenDirty(0, 0);
127 return eve::editing::applied<void>();
128}
129std::unique_ptr<IDomainOperationTarget> AudioEffectChainTarget::cloneDomainState() const {
130 return std::make_unique<AudioEffectChainTarget>(*this);
131}
133 std::unique_ptr<IDomainOperationTarget> candidate) {
134 auto* typed = dynamic_cast<AudioEffectChainTarget*>(candidate.get());
135 if (!typed || typed->id_ != id_)
136 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.effect-staging"),
137 "Invalid staged effect chain");
138 *this = *typed;
139 return eve::editing::applied<void>();
140}
141std::vector<AudioEffectRecord> AudioEffectChainTarget::effects() const {
142 std::vector<AudioEffectRecord> result;
143 for (const auto& id : order_) result.push_back(effects_.at(id));
144 return result;
145}
147 auto parsed = parseEffect(effectValue(effect));
148 if (!parsed.ok()) return Result<DomainOperation>::failure(parsed.status());
149 const auto found = effects_.find(effect.id);
150 return eve::editing::applied<DomainOperation>(operation(
151 "audio.effect.set.v1", found == effects_.end() ? "audio.effect.delete.v1" : "audio.effect.set.v1", id_,
152 effectValue(effect), found == effects_.end() ? EditorValue(effect.id.value()) : effectValue(found->second),
153 effect.id));
154}
156 const auto found = effects_.find(id);
157 if (found == effects_.end())
158 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.audio.effect-not-found"),
159 "Effect was not found");
160 return eve::editing::applied<DomainOperation>(
161 operation("audio.effect.delete.v1", "audio.effect.set.v1", id_, id.value(), effectValue(found->second), id));
162}
164 if (order.size() != effects_.size()) {
165 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.audio.effect-order"),
166 "Effect order must contain every effect");
167 }
168 std::set<StableId> unique(order.begin(), order.end());
169 if (unique.size() != order.size() ||
170 std::any_of(order.begin(), order.end(), [&](const StableId& id) { return !effects_.contains(id); }))
171 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.audio.effect-order"),
172 "Effect order contains missing or duplicate ids");
173 return eve::editing::applied<DomainOperation>(
174 operation("audio.effect.reorder.v1", "audio.effect.reorder.v1", id_, orderValue(order), orderValue(order_)));
175}
176std::vector<EditorDiagnostic> AudioEffectChainTarget::validate() const {
177 std::vector<EditorDiagnostic> result;
178 if (order_.size() > 16)
179 result.push_back(eve::editing::ruleDiagnostic(
180 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.audio.effect-budget"),
181 DiagnosticSeverity::Error, "Effect chain exceeds the 16-stage safety budget"));
182 int reverbs = 0;
183 for (const auto& id : order_)
184 if (effects_.at(id).type == "reverb") ++reverbs;
185 if (reverbs > 2)
186 result.push_back(eve::editing::ruleDiagnostic(
187 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.audio.reverb-budget"),
188 DiagnosticSeverity::Warning, "More than two reverbs may be expensive"));
189 return result;
190}
193 for (const auto& effect : this->effects()) effects.push_back(effectValue(effect));
194 return EditorValue::Object{{"schemaVersion", int64_t{1}}, {"effects", std::move(effects)}};
195}
197 const auto *versionEntry = field(snapshot, "schemaVersion"), *effectsEntry = field(snapshot, "effects");
198 const auto* version = versionEntry ? versionEntry->getIf<int64_t>() : nullptr;
199 const auto* effects = effectsEntry ? effectsEntry->getIf<EditorValue::Array>() : nullptr;
200 if (!version || *version != 1 || !effects)
201 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.effect-snapshot"),
202 "Invalid effect snapshot envelope");
203 AudioEffectChainTarget candidate(id_);
204 for (const auto& entry : *effects) {
205 auto parsed = parseEffect(entry);
206 if (!parsed.ok() || candidate.effects_.contains(parsed.value().id))
207 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.effect-snapshot-entry"),
208 "Invalid or duplicate effect snapshot entry");
209 candidate.effects_.emplace(parsed.value().id, parsed.value());
210 candidate.order_.push_back(parsed.value().id);
211 }
212 const auto diagnostics = candidate.validate();
213 if (std::any_of(diagnostics.begin(), diagnostics.end(), [](const EditorDiagnostic& diagnostic) {
214 return diagnostic.severity() == DiagnosticSeverity::Error;
215 }))
216 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.audio.effect-snapshot-budget"),
217 "Effect snapshot exceeds safety budget");
218 effects_ = std::move(candidate.effects_);
219 order_ = std::move(candidate.order_);
220 bumpRevision();
221 widenDirty(0, 0);
222 return eve::editing::applied<void>();
223}
225 const ObjectId& busId) const {
226 const auto diagnostics = validate();
227 if (std::any_of(diagnostics.begin(), diagnostics.end(), [](const EditorDiagnostic& diagnostic) {
228 return diagnostic.severity() == DiagnosticSeverity::Error;
229 }))
230 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.audio.effect-budget"),
231 "Invalid effect chain cannot be assigned");
232 auto bus = mixer.bus(busId);
233 if (!bus.ok()) return Result<DomainOperation>::failure(bus.status());
234 bus.value().effects = EditorValue::Array{};
235 auto* array = bus.value().effects.getIf<EditorValue::Array>();
236 for (const auto& effect : effects()) array->push_back(effectValue(effect));
237 return mixer.makeReplace(bus.value());
238}
240 IAudioEffectChainSink& sink) const {
241 if (chain.revision() != expected)
242 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.audio.effect-stale"),
243 "Effect chain changed before publication");
244 const auto diagnostics = chain.validate();
245 for (const auto& diagnostic : diagnostics)
246 if (diagnostic.severity() == DiagnosticSeverity::Error)
247 return Result<void>::failure(eve::Status(EditorStatus::Rejected, diagnostics));
248 return sink.publish(chain.targetId().value(), chain.revision(), chain.effects());
249}
250} // namespace eve::audio_editing
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
float maximum[3]
float minimum[3]
std::uint32_t key
bool valid
std::vector< std::int32_t > order
std::unique_ptr< ParticleEffect > effect
std::string id
Definition PlayHost.cpp:108
double number
bool found
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
Result< void > publish(const AudioEffectChainTarget &chain, Revision expectedRevision, IAudioEffectChainSink &sink) const
Publish.
Revisioned reversible serial effect chain independent of an audio backend.
EditorValue snapshotValue() const
Capture deterministic schema-version-one effect chain.
Result< DomainOperation > makeReorder(const std::vector< StableId > &order) const
Plan complete processing-order replacement.
Result< DomainOperation > makeAssignToBus(const AudioMixerTarget &mixer, const ObjectId &bus) const
Plan assigning this chain snapshot to an existing mixer bus.
Result< DomainOperation > makeSet(const AudioEffectRecord &effect) const
Plan effect creation or replacement.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load a validated effect chain.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
Result< DomainOperation > makeDelete(const StableId &id) const
Plan effect removal.
std::vector< EditorDiagnostic > validate() const
Report invalid parameter ranges and unsafe chain budgets.
std::vector< AudioEffectRecord > effects() const
Enumerate effect records in processing order.
AudioEffectChainTarget(std::string id)
Audio effect chain target.
TargetId targetId() const override
Target id.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
TargetDescriptor describe() const override
Describe.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
Serializable mixer-bus hierarchy, including master bus.
Result< AudioBusSnapshot > bus(const ObjectId &id) const
Read one bus snapshot.
Result< DomainOperation > makeReplace(AudioBusSnapshot bus) const
Plan a reversible bus settings/reparent change.
Runtime backend boundary for atomic effect-chain publication.
virtual Result< void > publish(const std::string &chain, Revision revision, const std::vector< AudioEffectRecord > &effects)=0
Publish.
void bumpRevision()
Advance the revision without touching the dirty region.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
std::uint64_t revision() const override
Return the current revision.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
Type type() const
Return the active value type.
const T * getIf() const
Return an immediately borrowed typed value.
editing::StableId StableId
editing::DomainOperation DomainOperation
editing::TargetId TargetId
editing::Value EditorValue
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
const EditorValue * field(const EditorValue &value, const char *name)
Stable effect instance with normalized dry/wet mix and typed parameters.
DomainOperation public API.
Stable discovery metadata exposed by every editable target.