Commit Graph
24 Commits
Author SHA1 Message Date
Brandon Thomas 42ceeaf02f Merge pull request #628 from Silvertree2010/fix/h1620-zero-frame-flac-written-by-our-o
Decode and probe zero-frame FLAC files written by our encoder
2026-10-19 03:09:22 -04:00
Brandon Thomas 8ae491b6d2 Merge remote-tracking branch 'origin/main' into m/628
# Conflicts:
#	crates/audio-io/tests/roundtrip.rs
2026-10-19 03:04:59 -04:00
Brandon Thomas 81e4c5c1ec Merge remote-tracking branch 'origin/main' into m/498
# Conflicts:
#	ROADMAP.md
2026-10-19 03:04:41 -04:00
Brandon Thomas f3cc778673 Merge pull request #537 from saadkhantareen/fix/464-poison-stream-encoders
fix(audio-io): poison stream encoders after a failed write so later calls cannot claim success (#464)
2026-10-19 03:02:58 -04:00
Brandon Thomas afb8e10ff2 Merge pull request #510 from MarekSpodBiedry/issue-462
Keep audio-io stream test limits as u64 so it compiles on 32-bit targets (#462)
2026-10-19 03:02:51 -04:00
NetterSilverandClaude Haiku 5.5 8840fbc87a Decode and probe zero-frame FLAC files written by our encoder
Found while testing crates/audio-io/src/symph.rs:41 (open/run, used by probe and decode for FLAC)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 15:46:27 +02:00
Saad Abdullah 750db81371 fix(audio-io): poison stream encoders after a failed write so later calls cannot claim success (#464) 2026-10-13 22:04:57 +00:00
MarekSpodBiedry edd2b0ef73 Keep audio-io stream test limits as u64 so it compiles on 32-bit targets (#462) 2026-10-13 17:48:22 +02:00
MarekSpodBiedry 8ac95607b7 Return TooLarge when the WAV header size overflows u64 (#467) 2026-10-13 17:48:14 +02:00
NetterSilver 7f2ff83c44 WAV: an empty data chunk followed by metadata has no frames (#466)
A data chunk of size 0 was always read as "everything that follows", so a finished
empty WAV with a LIST or bext chunk after its data decoded the metadata as samples
and reported a longer file. The streaming-writer case is kept: a zero size is read
as empty only when the RIFF size is the file's and only whole chunks follow; a
recording whose sizes were never finalised still gives the audio that was written.
2026-10-13 17:11:28 +02:00
Brandon Thomas 5051b4a45f Merge main into #393 2026-10-13 05:12:31 -04:00
Brandon Thomas 849d1a6b07 Merge pull request #331 from grovebit/corrupt-audio
Refuse corrupt audio files instead of crashing
2026-10-13 04:43:37 -04:00
grovebit 5755795663 Stream bounces to disk instead of rendering them in memory
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.
2026-10-12 23:46:13 +01:00
grovebit de5f2e0fb3 Check INFO entry sizes in WAV files that symphonia reads
WAV files that aren't plain PCM (A-law, µ-law, ADPCM) go to symphonia,
which reads every entry of an INFO list into a buffer of its declared
size, bounded only by the list's and file's declared lengths. A file
claiming a 4 GB RIFF and a 2 GB title asked for 2 GB before failing.
macOS gives that lazily; a 32-bit build or a system that can't commit
it aborts.

Before symphonia reads such a WAV, its INFO entries are checked against
the bytes the file holds, and one that claims more is refused as
malformed. Chunks are walked where symphonia reads them, not where their
lengths say: A-law, µ-law and ADPCM `fmt` chunks are read by their own
extra size, extensible ones by 40 bytes, and a list is padded twice.
2026-10-12 18:39:48 +01:00
grovebit 777c08ef1a Skip ID3 tags instead of having symphonia read them
An MP3 whose ID3v2 tag had a frame claiming 0x7FFFFFFF bytes made
symphonia's probe allocate 2 GB to read that frame (4 GB at
0xFFFFFFFF). It reads every tag it finds ahead of the audio, though
SoundCraft never uses the result. macOS gives such memory lazily; a
32-bit build or a system that can't commit it aborts.

The probe loop now skips an ID3v2 tag by the size in its header and
carries on looking for the audio, so a tag can't cost more than the
bytes it occupies, and a damaged tag no longer stops an MP3 from
opening.
2026-10-12 18:39:48 +01:00
grovebit a4213cf90f Check FLAC picture and comment lengths before symphonia reads them
symphonia reads a FLAC file's picture and Vorbis comment blocks field
by field, allocating each field's declared length before it finds out
the block is shorter: a picture claiming 0xFFFFFFFF bytes of image
data asked for 4 GB. macOS gives that lazily, but a 32-bit build or a
system without that much memory to commit aborts.

When the probe finds a FLAC stream, the lengths of its picture fields
and comments are now checked against their metadata block first, and a
file whose fields don't fit is refused as malformed.
2026-10-12 18:39:48 +01:00
grovebit d64d5f491e Refuse files whose packets claim millions of frames
A 59 KB ALAC file in an M4A made the app use 25 GB of memory before it
gave up: symphonia's ALAC decoder sizes its buffers from the frame
length in the file's codec settings, and nothing bounds it. The PCM and
ADPCM decoders likewise allocate the frames per packet the container
declares, which an MP4 version-2 LPCM entry takes from the file.

Before a decoder is made, the frames a packet may hold, and an ALAC
frame length, must be at most 2^20 (about 22 seconds at 48 kHz;
Apple's ALAC encoder uses 4096), or the file is refused as malformed.
2026-10-12 18:39:48 +01:00
grovebit 37f240ed54 Don't crash on MP4 files that claim more data than they hold
An M4A whose handler atom claimed a 64-bit size of 2^60 bytes ended the
process on import: symphonia reads `hdlr` and the `data`, `name` and
`mean` atoms of iTunes metadata into buffers of their declared size, and
an allocation that fails aborts. Its sample tables reserve room for
whatever entry count they claim (up to 68 GB from a 19 KB file), and
every sample is read into a buffer of the size `stsz` gives it. macOS
hands out most of that lazily; on Linux, Windows and the 32-bit Windows
build such a request fails and aborts.

When the probe finds an MP4 file, the atoms symphonia descends into are
now walked first, the way symphonia reads them: a header that starts
inside its container is read whole, and the walk ends where symphonia
stops, after an atom that runs past its container. A file is refused as
malformed when one of those atoms, a sample table or a sample claims
more bytes than the file holds. Atoms symphonia skips, and an `mdat` cut
short, are left alone, so truncated recordings still read.
2026-10-12 18:39:48 +01:00
grovebit c40078f32a Don't crash on CAF files that claim more data than they hold
Importing a 72-byte CAF file ended the process. symphonia allocates
whatever a CAF magic cookie, packet table or packet claims before it
reads it, and when that allocation fails Rust aborts, which no
catch_unwind can stop. A cookie claiming 54 PB, a table of 2^50
packets and a single packet of 2^50 bytes each did it, on import and
in `soundcraft-cli info`.

symph::open now runs symphonia's probe loop itself, and when the
container it finds is a CAF stream, it first walks the chunks the way
symphonia reads them. A file whose cookie, packet table or packets
claim more bytes than it holds is refused as malformed. So is a packet
table symphonia would refuse only after reserving 32 bytes per packet:
one that runs past the file, or one listing packets of a constant bit
rate stream. The probe's hint was never read by symphonia 0.5, so it is
no longer passed.
2026-10-12 18:39:48 +01:00
MarekSpodBiedry dabcb4840a Open .m4b audiobook files like .m4a
M4B is the same MP4/AAC container as M4A, and the decoder already reads it,
but the Open dialog, the web file picker and the file browser hid .m4b files.
Add m4b to those lists and to the extension fallback.
2026-10-12 16:27:57 +02:00
grovebit cfe6b42d00 Record takes straight to disk
The input callback appended every block to one growing Vec: it
reallocated on the audio thread as the take grew, kept every input
channel in RAM, and the take existed only in memory until Stop, so a
crash during a take lost it.

- The input callback copies each block into a preallocated lock-free
  ring (atomic slots, no unsafe) and never locks or allocates. Samples
  that don't fit are paid back as silence before the next block, so the
  take keeps its timing; the loss is reported in the status line.
- A writer thread drains the ring every 10 ms into one 32-bit float WAV
  per armed track, holding only that track's input channels. Files go
  to the session's Audio Files folder, or to its autosave folder while
  the session has never been saved, named like the incumbent's takes
  (<track>_<NN>.wav). An existing file is never overwritten.
- The header is rewritten every second, so a file left by a crash plays
  up to the last update. Above 4 GiB it switches to RF64 in place.
- If a take file can't be created, Record doesn't start and says why;
  a write error ends that take where it failed.
- At Stop the files become the takes' sources. Loop recording keeps one
  file per track, one clip per pass (at most 1000) on its own playlist,
  in one undo step.
- Saving uses a file already in the target's Audio Files folder in
  place instead of rewriting it, so neither Save nor autosave copies a
  take. A take recorded at another rate than the session's is saved once
  at the session's rate.

soundcraft-audio-io gains wav_float_header (its length doesn't depend
on the frame count, so it can be rewritten in place); encode shares the
header code. Splitting a loop take into passes moved from the UI into
soundcraft_engine::io::add_recording, where it is tested.
2026-10-10 02:22:18 +01:00
MarekSpodBiedry 43f1fb5681 convert and bounce: refuse output extensions the encoder cannot write instead of saving WAV data (#47) 2026-10-09 19:30:25 +02:00
Brandon ThomasandClaude Opus 5.5 cc86842abe Surround mixing, VST3 hosting, and hardening against hostile inputs
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>
2026-10-06 00:39:59 -04:00
Brandon ThomasandClaude Opus 5.5 755f02fc85 Foundation: time, audio-io, midi, dsp, model, mix, engine, playback crates
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>
2026-10-05 22:27:29 -04:00