The ping-pong loop reflected a voice at the loop's end once and clamped it to the
edge, so a step wider than the loop (a narrow loop with high transposition or
sample-rate conversion) dropped the rest of the travel and made the pitch and loop
phase wrong. The distance past the edge is now folded into the loop: an even number
of crossings keeps the direction, an odd number turns it round. Steps narrower than
the loop move exactly as before.
detect_transients (Tab to Transient, Separate at Transients, Beat Detective)
missed most onsets that weren't the loudest around, and put notes that start
over a ringing note 25-35 ms early, in the previous note's tail. Its
threshold was a share of the loudest flux in the analysed audio, so quiet
hits vanished next to loud ones and the result changed with where the
analysis started. It placed each onset on the first sample reaching a
quarter of the frame's peak, which a ringing note already does.
- Vibrato-suppressed spectral flux: 21 ms windows 5.3 ms apart, log
magnitudes on 24 bands per octave from 30 Hz to 20 kHz, each band compared
with the loudest of its neighbours in the frame before, so vibrato and
chorus don't count. Channels are analysed as the sum of their energies,
so stereo content that cancels in mono still counts.
- A peak must be the highest within 30 ms and exceed the mean flux from
100 ms before to 70 ms after by a fixed amount (2.5 at sensitivity 0.5),
so quiet hits count and the result doesn't depend on where the audio
starts.
- A fast release smears energy between a note's partials, which looks like
a rise. A detection also needs a 3 dB step in its high-passed energy, or
total energy that grows in its frame or the next.
- Each onset goes to the step edge of the high-passed energy (the sample
where the next 3 ms gain most over the 6 ms before), then back to where
the energy crosses the midpoint between the two: 0.3-0.5 ms before
percussive attacks.
On 29 synthetic cases with known onsets (drums with ghost notes; drum-synth
grooves dry and through reverb, compression and limiting; 180 bpm; flams and
claps; legato piano; plucked bass and guitar; strums; 808 kicks; hand drums;
synth bass, leads, pads and staccato notes; string swells; a vibrato pad;
quiet hits over noise and hum; stereo pairs; formant syllables), the
F-measure went from 0.658 to 0.977. It found 1398 onsets instead of 870,
with 5 false ones instead of 315; all 5 are vowels starting after a
consonant. Still missed: hats under a crash, ghost notes under a louder
hit's tail, and slow string swells.
The old offline_meters test bursts stopped dead at -37 dB, an audible click
that now counts as a transient; they ring out instead.
A bounce rendered the whole mix (or bus) into memory and then encoded a
second copy, and the render silently stopped at 2^29 samples: about 93
minutes of stereo at 48 kHz, 31 of 5.1, 15 of 7.1.4. Longer bounces came
out cut short while file.bounce_mix still reported the requested length.
- audio-io: FileEncoder writes WAV/BWF (RF64 above 4 GiB), AIFF and FLAC
block by block into any Write + Seek, byte for byte what encode writes
for the whole buffer. check_room fails up front when an AIFF (4 GiB) or
a FLAC (2^36 frames) can't hold the bounce. AIFF and FLAC encode run on
the same writers; their output is unchanged.
- mix: RangeRender renders the main mix or one bus over a range of any
length, a block at a time; render_range and render_bus (in memory,
still capped) are built on it.
- engine: io::Bounce streams the main mix or a bus, or its stereo
fold-down, into "<name>.part" next to the target, syncs it and renames
it into place, so a failed bounce leaves no partial file and keeps the
old one. In the browser it encodes into memory (failing instead of
aborting when memory runs out) and delivers the file as before.
Normalize renders twice (peak, then the scaled write); levels are
measured as blocks are written. Stems stream the same way and share the
4-hour limit.
A 100-minute bounce of the demo session now writes all 288,000,000
frames with 83 MB peak memory.
The shelf corner (~1682 Hz) sits at or above Nyquist for rates under
~3.4 kHz. The bilinear prewarp tan() then wraps negative (unstable
poles, +750 LUFS readings, -144 integrated) or aliases. Hold the
design frequency below Nyquist (0.49 * sr, as Coeffs::design does).
Coefficients at 8 kHz and above are unchanged.
Fixes#240
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strip Silence rendered the whole selection at once, and a render stops
at 2^29 samples over all channels: 93 minutes of stereo at 48 kHz, 46
at 96 kHz. Everything past that read as silence, so the clips there
were deleted. A 105-minute selection on a stereo track lost its clip at
100 minutes.
It now renders at most 2^24 samples at a time, only where unmuted audio
clips play, into a streaming version of the silence detector
(`SilenceScan`, which `non_silent_ranges` now runs on). A window takes
at most 64 MB instead of up to 2 GiB, and more than a day of clips is
refused rather than scanned for minutes. `render_clips` now skips clips
outside its range before looking up their clip effects, so a track of
5,000 clips strips as fast as before. Results are unchanged on the demo
sessions and on 10 minutes of tone bursts that cross window edges.
Fixes#220: Render in the Signal Generator window did nothing on a time
selection over an empty part of a track. AudioSuite only processed the
clips under the selection, so with none it stopped with "select audio
clips first", and that went to the status line only.
- A generator (soundcraft_dsp::is_generator, today only the Signal
Generator) ignores its input, so it no longer needs clips. With a
time selection it renders a new clip over exactly the selection on
each selected audio track, over whatever was there, as Pro Tools'
Signal Generator does. Before, it replaced whole clips under the
selection and left the empty parts empty.
- Other tracks in the selection are skipped. With no audio track in
it, or no time selection and no clips, it reports why.
- Explicit `clips` still process those clips. Effects behave as before.
- The AudioSuite window's hint says what Render will do for a
generator.
Saving, bouncing and the two-minute autosave read every third-party
plugin's state through the audio callback: a command made the callback
call each plugin's save_state between two blocks and reply, while the
UI waited for up to 2 s. So:
- CLAP's clap.state and VST3's getState ran on the audio thread,
although both formats want them on the main thread.
- The callback allocated and waited on the plugins; a plugin with a
large state (a sampler) could make the audio drop out on every save
and autosave.
- Without a running callback (a stalled or lost device), every save
waited 2 s and then kept stale plugin states.
Each instance now hands Side::prepare a state reader before it leaves
for the audio thread, the way it hands over its editor handle. CLAP's
GuiLink and VST3's EditorLink (which now holds the component as well)
read the state under their lock; Audio Units get a StateLink that does
the same. Each instance cuts its link off before it is destroyed, so a
reader never calls into a dead plugin. Player::capture_states reads
them on the calling (UI) thread, and the CaptureStates command is gone.
Each host's tests read the state while another thread processes the
instance, and get nothing once it is dropped. Playback reads states
with no audio thread at all.
Imported MIDI keeps its controller events: sustain pedal and other CCs,
pitch bend, channel and poly pressure, program changes. Playback sent
instruments only note-ons and note-offs, so a piano part lost its pedal
and bends went flat.
- Plugin::midi takes any other MIDI 1.0 channel message at a sample
offset. The mix engine sends each clip's controllers at their sample,
after the Real-Time Properties delay (and transpose, for poly
pressure), on channel 1 like the notes.
- After a seek, a stop or a loop wrap, an instrument first gets Reset All
Controllers (CC 121), then the last value each controller had on its
track before the playhead. A pedal held before the start point is
still down.
- CLAP: raw MIDI events when the note port speaks the MIDI dialect.
- VST3: CCs, aftertouch and pitch bend become parameter changes at their
offset, through the plugin's IMidiMapping. It is asked once in prepare,
on the main thread, and there is one point per parameter per block.
Poly pressure is a PolyPressureEvent. Program changes have no VST3
mapping and are dropped.
- AU: MusicDeviceMIDIEvent for instruments and music effects.
- The built-in synth follows the sustain pedal, pitch bend (2 semitones
each way), Reset All Controllers and All Notes Off.
- The sampler ignores the pedal, pitch bend and Reset All Controllers
for now.
The AU check of MIDI volume lives inside the existing DLS test. Creating
two DLSMusicDevice instances at once on two threads crashes inside the
unit, with or without this change. Two instances created one after
another render in parallel fine.
Hosted plugins never learned the session's tempo or where the song was:
- CLAP got a null transport;
- VST3 got a fixed 120 BPM 4/4 context that never played and counted
processed samples as the song position;
- Audio Units had no host callbacks.
Tempo-synced delays, LFOs and arpeggiators ran at 120 BPM and ignored
the transport.
Before each block the mix engine now hands every insert, instrument and
master-fader plugin a soundcraft_dsp::Transport (Plugin::set_transport):
play and record state, tempo and meter at the block start, and the
timeline position in samples, seconds and quarter notes, with the
current bar. Built-in plugins ignore it.
- CLAP: a clap_event_transport (fixed-point beats and seconds, bar start
and number, time signature, play and record flags).
- VST3: ProcessContext tempo, time signature, project time in samples and
quarter notes, bar position, kPlaying and kRecording. Continuous time
stays the processed-sample count.
- AU: kAudioUnitProperty_HostCallbacks (beat and tempo, musical time
location, transport state 1 and 2), read from atomics the instance
owns and frees after disposing the unit.
Loop (cycle) ranges aren't reported yet: playback keeps the loop range
to itself.
A third built-in instrument next to Subtractive Synth and Drum Synth. It plays one
sample across the keyboard: root note, transpose and fine tune, key tracking, key and
velocity ranges, amp ADSR with velocity sensitivity, sample start, forward or ping-pong
loops, gate or one-shot playback, 32 voices with cubic interpolation. The sample's own
rate is converted as it plays.
- dsp: `Plugin::set_sample` and the `SampleSource` trait hand an instrument shared
audio without copying; the Adsr from the synth is reused.
- model: `Insert::sample` names the session source the sampler plays.
- mix: sync hands the pool audio to the instrument when it changes, and built-in
instruments now get their parameter values applied (they always ran on defaults).
- engine: `mix.sampler_load {track?, path, params?}` decodes WAV, AIFF, FLAC, MP3, OGG
(anything file.import_audio reads) into a session source, resampled to the session
rate and saved in Audio Files with the session; `mix.sampler_clear`; and
`mix.instrument_param {track?, param, value}` for any built-in instrument.
Tests generate their audio in code; no bundled assets.
Options > Click set a flag but nothing rendered sound, so a click track
was silent. The mix engine now synthesizes a short blip on every beat of
the tempo map (accent on beat 1), using the click.* settings from
Setup > Click/Countoff, and sums it after the master fader.
The metronome is opt-in per engine: only live playback enables it.
Offline renders (bounces, stems, exports) leave it off so the click is
never printed into a file.
Also: midi_to_hz moves from the engine demo into soundcraft-dsp so the
mix and demo share one tuning formula.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Video (new standalone crate soundcraft-video):
- Pure-Rust H.264 (Baseline/Main/High), ProRes (all flavours) and Motion JPEG decoding with
ISO-BMFF demux, adapted from FilmCraft's crates; fuzzed with mutated/truncated movies
- TrackKind::Video with ClipContent::Video referencing Session.videos; file.import_video adds
the picture track and the movie's audio; relinking on open; video.offset/online/list
- Video and Video Universe windows, thumbnails on the timeline, background decoding
Third-party plugins:
- Plugin state (CLAP state ext, VST3 component + controller state) saved in sessions
- Hosted instances created, state-loaded and dropped on the UI thread, never the audio thread
- Plugin editors: floating CLAP GUIs and VST3 views on macOS (verified with Melodyne)
- New isolated FFI crate soundcraft-au-host: Audio Units on macOS (effects and instruments,
parameters, latency, MIDI, ClassInfo state), wired into the mixer and plugin browser
UI: Window > Renderer (speaker layout with live levels and object positions), Window > UI
Customization (interface scale, reduce motion), Window > Configurations > Auto-Update.
Fix: saturating sample-index math in clip rendering (hostile stretch values overflowed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Surround:
- main and bus formats up to 9.1.6 and Ambisonics; pairwise constant-power
surround panner (divergence, centre, LFE, height) per track and send;
ITU fold-down; multichannel bounce with correct WAV channel masks;
multichannel device output; surround panner and multichannel meters in the
Mix window; setup.main_format, mix.surround_pan, mix.send_surround_pan
VST3:
- soundcraft-vst3-host: isolated unsafe crate (vst3 bindings, MIT), scans
standard folders, hosts components/processors/controllers, params,
latency, notes; VST3 menu; engine.vst3_plugins; e2e tests with a fixture
Hardening:
- positions from parameters clamped; offline renders capped and bounded to
track material; loop/transport limits; hostile-parameter property test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mix: independent strips process in parallel (rayon, native), parameter
pushes cached, plugin sync/routing order cached per session structure;
plugin and render benchmarks
- UI: Playback Engine, I/O Setup (bus rename/delete), Keyboard Shortcuts
windows; clip gain line + info; folder open/close; tempo changes from the
ruler; Inserts/Sends F-J, EQ curve and comments sections in the Mix window
- docs: control protocol and MCP guides
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Workspace for SoundCraft, a clean-room pure-Rust Pro Tools-class DAW:
timebases and tempo maps, WAV/AIFF/FLAC/symphonia audio IO with peaks,
SMF and MIDI ops, 26 original DSP plugins, the session model, the mix
engine, ~180 registered commands with undo, a cpal player, and the
agent rules (AGENTS.md, ATTRIBUTION.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>