载入中...
搜索中...
未找到
AudioSourceEditor.cpp
浏览该文件的文档.
2
4#include "common/Exception.h"
5#include "common/Module.h"
6
7#if defined(EVE_AUDIO_EDITOR_LIVE)
8#include "audio/Audio.h"
9#include "audio/Source.h"
10#include "sound/SoundData.h"
11#endif
12
13#include <algorithm>
14#include <cmath>
15#include <cstdint>
16#include <utility>
17#include <vector>
18
19namespace eve::audio_editor {
20namespace {
21
22template <class T = void>
23audio_editing::Result<T> editorError(audio_editing::EditorStatus status, std::string rule,
24 std::string message) {
25 return eve::editing::failed<T>(status, audio_editing::RuleId(std::move(rule)), std::move(message));
26}
27
28double readNumber(const audio_editing::AudioSourceTarget& target, const editor::SelectionSnapshot& selection,
29 const char* path, double fallback) {
30 auto value = target.read(selection, audio_editing::PropertyPath(path));
31 if (const auto* real = value.value.getIf<double>()) return *real;
32 if (const auto* integer = value.value.getIf<std::int64_t>()) return static_cast<double>(*integer);
33 return fallback;
34}
35
36bool readFlag(const audio_editing::AudioSourceTarget& target, const editor::SelectionSnapshot& selection,
37 const char* path, bool fallback) {
38 auto value = target.read(selection, audio_editing::PropertyPath(path));
39 const auto* flag = value.value.getIf<bool>();
40 return flag ? *flag : fallback;
41}
42
43} // namespace
44
46#if defined(EVE_AUDIO_EDITOR_LIVE)
47 std::unique_ptr<audio_editing::AudioSourceTransportBackend> backend;
48 std::unique_ptr<eve::sound::SoundData> data;
49 eve::audio::Source* source = nullptr;
51 backend.reset();
52 delete source;
53 source = nullptr;
54 }
55#endif
56};
57
58void AudioClockTransportBackend::play() { playing_ = true; }
59void AudioClockTransportBackend::pause() { playing_ = false; }
61 playing_ = false;
62 position_ = 0.0;
63}
64
66 if (!std::isfinite(seconds) || seconds < 0.0 || seconds > duration_)
67 return editorError(audio_editing::EditorStatus::Rejected, "editor.audio.clock-seek",
68 "Clock seek is outside the clip");
69 position_ = seconds;
70 return eve::editing::applied<void>();
71}
72
74 duration_ = seconds > 0.0 && std::isfinite(seconds) ? seconds : 1.0;
75 position_ = std::clamp(position_, 0.0, duration_);
76}
77
78void AudioClockTransportBackend::advance(double deltaSeconds) {
79 if (!playing_ || !std::isfinite(deltaSeconds) || deltaSeconds <= 0.0) return;
80 position_ += deltaSeconds;
81 if (position_ >= duration_) {
82 position_ = duration_;
83 playing_ = false;
84 }
85}
86
88 : target_(std::move(targetId)), authority_(&target_), transactions_(&authority_) {
89 seedPreviewDocument();
90 generateTonePcm();
91 clock_.setDuration(static_cast<double>(pcm_.samples.size()) / static_cast<double>(pcm_.sampleRate));
92 auto rebuilt = rebuildWaveform();
93 if (!rebuilt.ok()) rebuilt.ignore("preview waveform uses an empty envelope when generation is rejected");
94 auto bound = bindClockAudition();
95 if (!bound.ok()) bound.ignore("clock audition bind is retried after workspace setup");
96 auto looped = syncLoop();
97 if (!looped.ok()) looped.ignore("preview loop uses clip duration when the seeded range is rejected");
98 auto snap = transport_.snapshot(auditionRevision());
99 if (snap.ok()) lastTransport_ = snap.value();
100 else snap.ignore("transport snapshot waits until the first successful bind");
101}
102
104 transport_.unbind();
105 liveAudition_.reset();
106}
107
108void AudioSourceEditor::seedPreviewDocument() {
110 properties["clip.asset"] = std::string("asset://preview/tone.sine");
111 properties["clip.mode"] = std::string("static");
112 properties["play.autoplay"] = false;
113 properties["play.loop"] = true;
114 properties["play.volume"] = 0.8;
115 properties["play.pitch"] = 1.0;
116 properties["play.loop-start"] = 0.5;
117 properties["play.loop-end"] = 1.5;
118 properties["spatial.relative"] = true;
119 properties["spatial.reference-distance"] = 1.0;
120 properties["spatial.maximum-distance"] = 10000.0;
121 properties["spatial.position"] = audio_editing::EditorValue::Array{0.0, 0.0, 0.0};
122 properties["spatial.velocity"] = audio_editing::EditorValue::Array{0.0, 0.0, 0.0};
123 properties["spatial.direction"] = audio_editing::EditorValue::Array{0.0, 0.0, -1.0};
124 properties["mixer.bus"] = std::string("master");
126 root["schemaVersion"] = std::int64_t{1};
127 root["properties"] = audio_editing::EditorValue(std::move(properties));
128 auto loaded = target_.loadSnapshot(audio_editing::EditorValue(std::move(root)));
129 if (!loaded.ok())
130 loaded.ignore("audio source editor keeps defaults when the preview snapshot is rejected");
131}
132
133void AudioSourceEditor::generateTonePcm() {
134 constexpr int kRate = 22050;
135 constexpr double kSeconds = 2.0;
136 const int samples = static_cast<int>(kRate * kSeconds);
137 pcm_.sampleRate = kRate;
138 pcm_.channels = 1;
139 pcm_.samples.resize(static_cast<std::size_t>(samples));
140 for (int i = 0; i < samples; ++i) {
141 const double t = static_cast<double>(i) / static_cast<double>(kRate);
142 pcm_.samples[static_cast<std::size_t>(i)] =
143 static_cast<float>(0.32 * std::sin(6.283185307179586 * 440.0 * t) +
144 0.12 * std::sin(6.283185307179586 * 660.0 * t));
145 }
146}
147
148editor::SelectionSnapshot AudioSourceEditor::selection() const {
150 snapshot.channel = "audio";
152 item.domain = editor::SelectionDomain::Asset;
153 item.target = editor::TargetId(target_.targetId());
154 item.item = editor::StableId(target_.targetId().value());
155 item.type = "audio.source";
156 snapshot.items.push_back(item);
157 snapshot.primary = item;
158 return snapshot;
159}
160
161audio_editing::Revision AudioSourceEditor::auditionRevision() const {
162 return audio_editing::Revision(pcmRevision_);
163}
164
165audio_editing::Result<void> AudioSourceEditor::rebuildWaveform() {
166 const int width = std::max(8, static_cast<int>(std::lround(viewportWidth_)));
167 audio_editing::AudioWaveformRequest request;
168 request.asset = "asset://preview/tone.sine";
169 request.sourceRevision = auditionRevision();
170 request.pixelWidth = width;
171 request.loopStart = readNumber(target_, selection(), "play.loop-start", 0.0);
172 request.loopEnd = readNumber(target_, selection(), "play.loop-end", duration());
173 waveform_ = waveforms_.generate(request, pcm_);
174 if (waveform_.status != audio_editing::EditorStatus::Applied)
175 return editorError(waveform_.status, "editor.audio.waveform", "Could not rebuild the source waveform");
176 return eve::editing::applied<void>();
177}
178
179audio_editing::Result<void> AudioSourceEditor::bindClockAudition() {
180 transport_.unbind();
181 liveAudition_.reset();
182 liveBound_ = false;
183 clock_.setDuration(duration());
184 return transport_.bind(audio_editing::StableId(target_.targetId().value()), auditionRevision(), &clock_);
185}
186
187audio_editing::Result<void> AudioSourceEditor::syncLoop() {
188 const bool loop = readFlag(target_, selection(), "play.loop", false);
189 const double start = readNumber(target_, selection(), "play.loop-start", 0.0);
190 double end = readNumber(target_, selection(), "play.loop-end", 0.0);
191 if (end <= 0.0) end = duration();
192 auto applied = transport_.setLoop(auditionRevision(), loop, start, end);
193 if (!applied.ok()) return applied;
194 return rebuildWaveform();
195}
196
197audio_editing::Result<void> AudioSourceEditor::publishLive() {
198#if defined(EVE_AUDIO_EDITOR_LIVE)
199 if (!liveAudition_ || !liveAudition_->source) return eve::editing::applied<void>();
200 return audio_editing::AudioSourceRuntimeApplier().apply(target_, liveAudition_->source);
201#else
202 return eve::editing::applied<void>();
203#endif
204}
205
207 editor::EditorWorkspace candidate = workspace;
208 struct Panel {
209 const char* id;
210 const char* title;
211 const char* region;
212 const char* context;
213 int order;
214 };
215 constexpr Panel panels[] = {
216 {"audio.sources", "Sources", "left", "list", 100},
217 {"audio.waveform", "Waveform", "center", "preview", 100},
218 {"audio.inspector", "Source Inspector", "right", "inspector", 100},
219 {"audio.transport", "Transport", "bottom", "transport", 100},
220 };
221 for (const auto& panel : panels) {
222 if (!candidate.registerPanel(panel.id, panel.title, panel.region, panel.order) ||
223 !candidate.setPanelCapability(panel.id, "audio.source") ||
224 !candidate.setPanelContext(panel.id, panel.context))
225 return editorError(audio_editing::EditorStatus::Rejected, "editor.audio.workspace-conflict",
226 "Could not install the audio source workspace composition");
227 }
228 if (!candidate.activatePanel("audio.waveform"))
229 return editorError(audio_editing::EditorStatus::Rejected, "editor.audio.workspace-activate",
230 "Could not activate the waveform panel");
231 workspace = std::move(candidate);
232 return eve::editing::applied<void>();
233}
234
236 if (!std::isfinite(width) || width < 8.0f)
237 return editorError(audio_editing::EditorStatus::Rejected, "editor.audio.viewport",
238 "Waveform viewport width must be at least 8 pixels");
239 viewportWidth_ = width;
240 return rebuildWaveform();
241}
242
244 if (!std::isfinite(x) || viewportWidth_ <= 0.0f)
245 return editorError(audio_editing::EditorStatus::Rejected, "editor.audio.seek-x",
246 "Waveform seek requires a finite x and viewport");
247 const double seconds = std::clamp(static_cast<double>(x / viewportWidth_), 0.0, 1.0) * duration();
248 return seekSeconds(seconds);
249}
250
252 auto sought = transport_.seek(auditionRevision(), seconds);
253 if (!sought.ok()) return sought;
254 auto snap = transport_.snapshot(auditionRevision());
255 if (snap.ok()) lastTransport_ = snap.value();
256 return eve::editing::applied<void>();
257}
258
261 auto operation = target_.makeSet(selection(), audio_editing::PropertyPath(path), value,
262 audio_editing::PropertySetMode::Absolute);
263 if (!operation.ok())
266 spec.id = editor::TransactionId("audio.source.tx." + std::to_string(++txSequence_));
267 spec.label = "Set " + path;
268 spec.target = editor::TargetId(target_.targetId());
269 spec.baseRevision = target_.revision();
270 spec.mergeKey = "audio.source:" + target_.targetId().value() + ":" + path;
271 auto begun = transactions_.begin(std::move(spec));
272 if (!begun.ok())
273 return editorError(begun.code(), "editor.audio.begin", "Could not begin the audio property transaction");
274 auto appended = transactions_.append(std::move(operation).takeValue());
275 if (!appended.ok()) {
276 (void)transactions_.rollback();
277 return audio_editing::Result<void>::failure(appended.status());
278 }
279 auto committed = transactions_.commit();
280 if (!committed.ok())
281 return audio_editing::Result<void>::failure(committed.status());
282 auto looped = syncLoop();
283 if (!looped.ok()) return looped;
284 return publishLive();
285}
286
288 auto result = transactions_.undo();
289 if (!result.ok()) return result;
290 auto looped = syncLoop();
291 if (!looped.ok())
293 auto published = publishLive();
294 if (!published.ok())
296 return result;
297}
298
300 auto result = transactions_.redo();
301 if (!result.ok()) return result;
302 auto looped = syncLoop();
303 if (!looped.ok())
305 auto published = publishLive();
306 if (!published.ok())
308 return result;
309}
310
312 auto played = transport_.play(auditionRevision());
313 if (!played.ok()) return played;
314 auto snap = transport_.snapshot(auditionRevision());
315 if (snap.ok()) lastTransport_ = snap.value();
316 return eve::editing::applied<void>();
317}
318
320 auto paused = transport_.pause(auditionRevision());
321 if (!paused.ok()) return paused;
322 auto snap = transport_.snapshot(auditionRevision());
323 if (snap.ok()) lastTransport_ = snap.value();
324 return eve::editing::applied<void>();
325}
326
328 auto stopped = transport_.stop(auditionRevision());
329 if (!stopped.ok()) return stopped;
330 auto snap = transport_.snapshot(auditionRevision());
331 if (snap.ok()) lastTransport_ = snap.value();
332 return eve::editing::applied<void>();
333}
334
336 if (!liveBound_) clock_.advance(deltaSeconds);
337 auto observed = transport_.update(auditionRevision());
338 if (observed.ok()) lastTransport_ = observed.value();
339 return observed;
340}
341
343#if !defined(EVE_AUDIO_EDITOR_LIVE)
344 return editorError(audio_editing::EditorStatus::Unsupported, "editor.audio.live-module",
345 "Audio module is not available for live audition");
346#else
347 auto* audioMod = eve::ModuleManager::getInstance<eve::audio::Audio>("Audio");
348 if (!audioMod)
349 return editorError(audio_editing::EditorStatus::Unsupported, "editor.audio.live-module",
350 "Audio module is not available for live audition");
351 std::vector<std::uint8_t> bytes(pcm_.samples.size() * 2);
352 for (std::size_t i = 0; i < pcm_.samples.size(); ++i) {
353 const float sample = std::clamp(pcm_.samples[i], -1.0f, 1.0f);
354 const auto quantized = static_cast<std::int16_t>(std::lround(sample * 32767.0f));
355 bytes[i * 2] = static_cast<std::uint8_t>(quantized & 0xff);
356 bytes[i * 2 + 1] = static_cast<std::uint8_t>((quantized >> 8) & 0xff);
357 }
358 const bool wasPlaying = isPlaying();
359 const double head = playhead();
360 auto live = std::make_unique<LiveAudition>();
361 try {
362 live->data = std::make_unique<eve::sound::SoundData>(std::move(bytes), pcm_.sampleRate, 16, 1);
363 transport_.unbind();
364 live->source = audioMod->newSource(live->data.get());
365 } catch (const eve::Exception& ex) {
366 auto restored = bindClockAudition();
367 if (!restored.ok()) restored.ignore("clock audition restored after live attach failure");
368 return editorError(audio_editing::EditorStatus::Failed, "editor.audio.live-source",
369 std::string("Live audition source could not be created: ") + ex.what());
370 }
371 live->backend = std::make_unique<audio_editing::AudioSourceTransportBackend>(live->source);
372 auto bound = transport_.bind(audio_editing::StableId(target_.targetId().value()), auditionRevision(),
373 live->backend.get());
374 if (!bound.ok()) {
375 auto restored = bindClockAudition();
376 if (!restored.ok()) restored.ignore("clock audition restored after live bind failure");
377 auto looped = syncLoop();
378 if (!looped.ok()) looped.ignore("loop restored after live bind failure");
379 return bound;
380 }
381 liveAudition_ = std::move(live);
382 liveBound_ = true;
383 auto looped = syncLoop();
384 if (!looped.ok()) return looped;
385 auto published = publishLive();
386 if (!published.ok()) return published;
387 auto sought = transport_.seek(auditionRevision(), head);
388 if (!sought.ok()) return sought;
389 if (wasPlaying) return play();
390 return eve::editing::applied<void>();
391#endif
392}
393
394bool AudioSourceEditor::isPlaying() const noexcept {
395 return lastTransport_.state == audio_editing::AudioTransportState::Playing;
396}
397
398double AudioSourceEditor::duration() const noexcept {
399 if (pcm_.sampleRate <= 0 || pcm_.samples.empty() || pcm_.channels <= 0) return 0.0;
400 return static_cast<double>(pcm_.samples.size() / static_cast<std::size_t>(pcm_.channels)) /
401 static_cast<double>(pcm_.sampleRate);
402}
403
404double AudioSourceEditor::playhead() const noexcept { return lastTransport_.position; }
405
406int AudioSourceEditor::bucketCount() const noexcept {
407 if (waveform_.envelopes.empty()) return 0;
408 return static_cast<int>(waveform_.envelopes.front().size());
409}
410
411float AudioSourceEditor::playheadX() const noexcept {
412 if (duration() <= 0.0) return 0.0f;
413 return static_cast<float>(playhead() / duration()) * viewportWidth_;
414}
415
416float AudioSourceEditor::loopStartX() const noexcept {
417 if (duration() <= 0.0) return 0.0f;
418 return static_cast<float>(lastTransport_.loopStart / duration()) * viewportWidth_;
419}
420
421float AudioSourceEditor::loopEndX() const noexcept {
422 if (duration() <= 0.0) return viewportWidth_;
423 const double end = lastTransport_.loopEnd > 0.0 ? lastTransport_.loopEnd : duration();
424 return static_cast<float>(end / duration()) * viewportWidth_;
425}
426
428 return target_.read(selection(), audio_editing::PropertyPath(path));
429}
430
432 if (waveform_.envelopes.empty()) return nullptr;
433 const auto& row = waveform_.envelopes.front();
434 if (index < 0 || static_cast<std::size_t>(index) >= row.size()) return nullptr;
435 return &row[static_cast<std::size_t>(index)];
436}
437
438} // namespace eve::audio_editor
LogicalId target
ActionParameterOperation operation
double value
Duration start
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
UI-neutral audio source editor: workspace, waveform, audition, undo.
std::string title
std::string message
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
std::uint32_t width
std::map< std::string, PropertyRule > properties
std::uint64_t bytes
std::vector< std::int32_t > order
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::uint32_t targetId
float t
uint32_t index
const UnitySourceAsset & source
const VegetationPresetContext & context
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
virtual const char * what() const
Returns a string containing reason for the exception.
Definition Exception.h:24
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
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
A playable audio source (static buffer or streaming decoder). Not thread-safe for playback control ex...
Definition Source.h:30
Result< void > pause(Revision expectedRevision)
Pause.
Result< AudioTransportSnapshot > snapshot(Revision expectedRevision) const
Read the current playhead only for the bound source revision.
Result< void > setLoop(Revision expectedRevision, bool enabled, double startSeconds=0.0, double endSeconds=0.0)
Configure a bounded loop range; zero end uses the clip duration.
Result< void > bind(StableId asset, Revision sourceRevision, IAudioTransportBackend *backend)
Bind a borrowed backend and stop any previously bound audition.
Result< void > stop(Revision expectedRevision)
Stops .
void unbind()
Stop and forget the borrowed backend.
Result< void > seek(Revision expectedRevision, double seconds)
Seeks .
Result< AudioTransportSnapshot > update(Revision expectedRevision)
Poll backend state and wrap the custom loop without changing documents.
Result< void > play(Revision expectedRevision)
Play.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load a versioned source snapshot.
PropertyReadResult read(const SelectionSnapshot &selection, const PropertyPath &path) const override
Reads .
Result< DomainOperation > makeSet(const SelectionSnapshot &selection, const PropertyPath &path, const EditorValue &value, PropertySetMode mode) const override
Make set.
TargetId targetId() const override
Target id.
Definition AudioTarget.h:28
AudioWaveformResult generate(const AudioWaveformRequest &request, const EditorAudioPcm &pcm, int maxPixels=16384, std::size_t maxSamples=100000000) const
Build bounded per-channel envelopes from already-decoded PCM.
audio_editing::Result< void > seek(double seconds) override
Seeks .
void setDuration(double seconds)
Install clip length before bind.
void advance(double deltaSeconds)
Advance playhead when playing.
audio_editing::Result< audio_editing::AudioTransportSnapshot > update(double deltaSeconds)
Poll audition and advance the clock backend.
audio_editing::Result< void > play()
Play.
float loopEndX() const noexcept
Loop end x.
audio_editing::Result< editor::TransactionReceipt > redo()
Redo.
audio_editing::Result< void > setViewportWidth(float width)
Rebuild waveform envelopes for the given pixel width.
double playhead() const noexcept
Playhead.
audio_editing::PropertyReadResult read(const std::string &path) const
Read a property using the source selection.
audio_editing::Result< void > attachLiveAudition()
Bind a live OpenAL Source generated from the preview PCM when Audio/Sound exist.
audio_editing::Result< void > configureWorkspace(editor::EditorWorkspace &workspace) const
Install Sources / Waveform / Inspector / Transport panels.
audio_editing::EditorValue snapshot() const
Canonical snapshot of the source document.
audio_editing::Result< editor::TransactionReceipt > undo()
Undo.
audio_editing::Result< void > setProperty(const std::string &path, const audio_editing::EditorValue &value)
Commit an absolute property assignment through the document transaction path.
bool isPlaying() const noexcept
True when playing.
float loopStartX() const noexcept
Loop start x.
audio_editing::Result< void > seekX(float x)
Seek from a waveform x coordinate in the current viewport.
AudioSourceEditor(std::string targetId)
Construct a seeded preview source with a generated tone clip.
audio_editing::Result< void > pause()
Pause.
audio_editing::Result< void > stop()
Stops .
double duration() const noexcept
Duration.
int bucketCount() const noexcept
Bucket count.
const audio_editing::AudioWaveformBucket * bucket(int index) const
Envelope bucket for drawing; channel 0. @ownership Borrowed bucket owned by this editor....
audio_editing::Result< void > seekSeconds(double seconds)
Seek preview to an exact time in seconds.
float playheadX() const noexcept
Playhead x.
std::uint64_t revision() const override
Return the current revision.
Dotted property path (layout.size) compared and persisted as text.
Definition EditingIds.h:78
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
Composition model shared by project-specific developer and runtime editors.
bool setPanelContext(const std::string &id, const std::string &context)
Attach a semantic input context such as scene, graph, or timeline.
Result< WorkspacePanelDescriptor > registerPanel(WorkspacePanelDescriptor descriptor)
Register a checked owning panel descriptor.
Result< WorkspacePanelDescriptor > activatePanel(const StableId &id)
Activate a visible panel with structured not-found/rejected results.
bool setPanelCapability(const std::string &id, const std::string &capability)
Attach a capability requirement used by the host when filtering panels.
Result< TransactionReceipt > commit() override
Preflight and commit all active operations through the authority.
Result< TransactionId > begin(TransactionSpec specification) override
Begin a transaction with a stable id and base revision.
Result< TransactionReceipt > undo() override
Compensate the most recent committed transaction.
Result< void > rollback()
Compatibility spelling for discard.
Result< TransactionReceipt > redo() override
Reapply the most recently compensated transaction.
Result< void > append(DomainOperation operation) override
Append an operation to the active transaction without applying it.
editing::Value EditorValue
editing::Status EditorStatus
Result< T > applied(T value, std::vector< Diagnostic > diagnostics={})
Construct an Applied result with an owning payload and optional diagnostics.
eve::editing::StableId StableId
Definition EditorIds.h:36
eve::editing::SelectionItem SelectionItem
Stable item stored in a selection snapshot.
eve::editing::TargetId TargetId
Definition EditorIds.h:29
eve::editing::SelectionSnapshot SelectionSnapshot
Immutable multi-selection captured for property and command work.
eve::editing::TransactionId TransactionId
Definition EditorIds.h:34
bool readNumber(const EditorValue &value, const char *name, float &output)
One min/max/RMS envelope bucket for a single channel.
std::vector< std::vector< AudioWaveformBucket > > envelopes
PropertyReadResult public API.
TransactionSpec public API.