Skip to content

feat(studio): play the scenario's audio while previewing - #182

Merged
LeadcodeDev merged 1 commit into
mainfrom
fix/studio-audio
Aug 12, 2026
Merged

feat(studio): play the scenario's audio while previewing#182
LeadcodeDev merged 1 commit into
mainfrom
fix/studio-audio

Conversation

@LeadcodeDev

@LeadcodeDev LeadcodeDev commented Aug 12, 2026

Copy link
Copy Markdown
Owner

What

The studio had no audio output at all: no backend in its dependencies, no stream anywhere, Present mode included. Judging a cut without sound is half the job, so the transport now plays.

How

What it plays is the output of mix_audio_tracks, the same function the encoder calls, so start/end, fades, per-track volume and volume keyframes are heard exactly as they will be muxed. A preview that ignored the envelope would misreport the edit.

The mix spans the whole scenario, silence included, which lets the playhead follow the audio clock instead of a 1/fps timer that drifts against the sound card over a long scenario. With no track — or no output device, as in CI — the timer stays in charge and nothing changes.

cpal's stream is not Send, so it runs on its own thread behind a command channel; position and state are published as atomics the UI reads without touching it. Seeking publishes the new position on the caller's side, so a scrub cannot read back as the old one and snap the playhead backwards.

Transport: play/pause and scrub drive it, plus a mute button next to play. The mix is rebuilt whenever the model reloads, so opening another file does not keep the previous scenario's sound.

Cost

One dependency, rodio with default-features = false, features = ["playback"] — no decoders, since the samples are already mixed. Pulls cpal and the platform audio bindings.

Not covered

Preview audio for --frames segments, and per-track soloing. Neither is needed to judge a cut.

Verification

cargo test --workspace green, cargo fmt --check and cargo clippy --all-targets clean, the studio binary links. The position logic is unit-tested; the actual sound output has not been listened to from this environment.

Rebased onto main after #181 merged.

Base automatically changed from fix/studio-audio-analysis to main August 12, 2026 11:56
The studio had no audio output at all — no backend in its dependencies, no
stream anywhere, Present mode included. Judging a cut without sound is half
the job, so the transport now plays.

What it plays is the output of mix_audio_tracks, the same function the encoder
calls, so start/end, fades, per-track volume and volume keyframes are heard
exactly as they will be muxed. A preview that ignored the envelope would
misreport the edit.

The mix spans the whole scenario, silence included, which lets the playhead
follow the audio clock instead of a 1/fps timer that drifts against the sound
card over a long scenario. With no track — or no output device, as in CI — the
timer stays in charge and nothing changes.

cpal's stream is not Send, so it runs on its own thread behind a command
channel; position and state are published as atomics the UI reads without
touching it. Seeking publishes the new position on the caller's side so a
scrub cannot read back as the old one and snap the playhead backwards.
Play/pause, scrub and a mute button drive it; the mix is rebuilt whenever the
model reloads, so opening another file does not keep the previous sound.
@LeadcodeDev
LeadcodeDev merged commit 9ca7644 into main Aug 12, 2026
1 of 3 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/studio-audio branch August 12, 2026 11:58
LeadcodeDev added a commit that referenced this pull request Aug 12, 2026
…nux (#184)

Preview audio (#182) added rodio, which pulls cpal, which needs ALSA's
development headers on Linux. The build was only ever checked on macOS, where
cpal goes through CoreAudio and alsa-sys never enters the graph — so main went
red on merge and every PR opened after it inherited the failure.

Same line as the webkit/gtk/xdo deps already there, in both jobs.
LeadcodeDev added a commit that referenced this pull request Aug 12, 2026
volume, volume_keyframes and the fades are what comes out of the speakers, and
since #182 the studio plays exactly that — a waveform drawing the raw file's
envelope while a fade takes the sound down contradicts what the viewer hears.

The gain comes from the encoder's own envelope, factored out of the mixer loop
as track_gain_at(track, t_in_track, audible) and expressed in seconds so the
mixer (OUTPUT_SAMPLE_RATE, resampled) and the analysis (the file's own rate,
decoded source) share one implementation. A second copy would drift, and the
whole point is that the picture matches the sound.

The cache fingerprint now hashes the serialised track rather than start/end
alone: two scenarios can name the same file with different mixes, and adding a
field to AudioTrack must not silently leave it out of the key.

Not mergeable yet — see #201. The components read scene-local time against a
scenario-time analysis, which is harmless while the envelope is flat and turns
into a flat trace as soon as it is not.
LeadcodeDev added a commit that referenced this pull request Aug 12, 2026
volume, volume_keyframes and the fades are what comes out of the speakers, and
since #182 the studio plays exactly that — a waveform drawing the raw file's
envelope while a fade takes the sound down contradicts what the viewer hears.

The gain comes from the encoder's own envelope, factored out of the mixer loop
as track_gain_at(track, t_in_track, audible) and expressed in seconds so the
mixer (OUTPUT_SAMPLE_RATE, resampled) and the analysis (the file's own rate,
decoded source) share one implementation. A second copy would drift, and the
whole point is that the picture matches the sound.

The cache fingerprint now hashes the serialised track rather than start/end
alone: two scenarios can name the same file with different mixes, and adding a
field to AudioTrack must not silently leave it out of the key.

Not mergeable yet — see #201. The components read scene-local time against a
scenario-time analysis, which is harmless while the envelope is flat and turns
into a flat trace as soon as it is not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant