fix(api): stream audio store path to eliminate whole-file buffering (OOM)
Processors now emit a ProcessedAudio plan with a streamed writer instead of a whole-file AudioBinary; vault writes stream via RegisterResourceStreamingAsync. Header parsing is bounded. Wave 2 (waveform/Opus) still re-reads the full file by design.
This commit is contained in:
@@ -32,13 +32,18 @@ public class AudioProcessorTests
|
||||
[Test]
|
||||
public async Task StandardPcm_RoundTripsUnchanged()
|
||||
{
|
||||
var path = await WriteWavAsync(BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 16, audioFormat: WaveFormatPcm));
|
||||
var source = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 16, audioFormat: WaveFormatPcm);
|
||||
var path = await WriteWavAsync(source);
|
||||
|
||||
var audio = await new AudioProcessor().ProcessWavFileAsync(path);
|
||||
|
||||
Assert.That(audio, Is.Not.Null);
|
||||
Assert.That(audio!.Duration, Is.GreaterThan(0.0));
|
||||
Assert.That(audio.Bitrate, Is.GreaterThan(0));
|
||||
// Standard PCM is passthrough: the streamed bytes must be byte-identical to the source file.
|
||||
var stored = await Materialize(audio);
|
||||
Assert.That(stored, Is.EqualTo(source), "Standard PCM must be stored byte-identical (passthrough)");
|
||||
Assert.That(audio.Size, Is.EqualTo(source.Length), "Passthrough Size must equal the source length");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -54,7 +59,8 @@ public class AudioProcessorTests
|
||||
Assert.That(audio, Is.Not.Null);
|
||||
Assert.That(audio!.Duration, Is.GreaterThan(0.0));
|
||||
Assert.That(audio.Bitrate, Is.GreaterThan(0));
|
||||
Assert.That(ReadFmtAudioFormat(audio.Buffer), Is.EqualTo(WaveFormatPcm), "Stored buffer must be standard PCM");
|
||||
var stored = await Materialize(audio);
|
||||
Assert.That(ReadFmtAudioFormat(stored), Is.EqualTo(WaveFormatPcm), "Stored buffer must be standard PCM");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -70,17 +76,19 @@ public class AudioProcessorTests
|
||||
var audio = await new AudioProcessor().ProcessWavFileAsync(path);
|
||||
|
||||
Assert.That(audio, Is.Not.Null);
|
||||
Assert.That(ReadFmtBitsPerSample(audio!.Buffer), Is.EqualTo(16 + 8), "Float must convert to 24-bit PCM");
|
||||
Assert.That(ReadFmtAudioFormat(audio.Buffer), Is.EqualTo(WaveFormatPcm));
|
||||
var stored = await Materialize(audio!);
|
||||
Assert.That(ReadFmtBitsPerSample(stored), Is.EqualTo(16 + 8), "Float must convert to 24-bit PCM");
|
||||
Assert.That(ReadFmtAudioFormat(stored), Is.EqualTo(WaveFormatPcm));
|
||||
// 4 float samples (4 bytes each) → 4 PCM samples (3 bytes each) = 12 data bytes after the 44-byte header.
|
||||
Assert.That(audio.Buffer.Length, Is.EqualTo(44 + 12));
|
||||
Assert.That(stored.Length, Is.EqualTo(44 + 12));
|
||||
Assert.That(audio!.Size, Is.EqualTo(44 + 12), "Computed Size must match the streamed byte count");
|
||||
// Verify the converted sample values: (int)(sample * 8388607.0), clamped, little-endian 3 bytes.
|
||||
// 0.5f → 4194303 = 0x3FFFFF → FF FF 3F
|
||||
// -0.5f → -4194303 = 0xFFC00001 → 24-bit LE: 01 00 C0
|
||||
// 1.0f → 8388607 = 0x7FFFFF → FF FF 7F
|
||||
// -1.0f → -8388607 = 0xFF800001 → 24-bit LE: 01 00 80
|
||||
var expectedData = new byte[] { 0xFF, 0xFF, 0x3F, 0x01, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x01, 0x00, 0x80 };
|
||||
var actualData = audio.Buffer[44..];
|
||||
var actualData = stored[44..];
|
||||
Assert.That(actualData, Is.EqualTo(expectedData), "Float samples must be converted to 24-bit PCM correctly");
|
||||
}
|
||||
|
||||
@@ -97,17 +105,18 @@ public class AudioProcessorTests
|
||||
var audio = await new AudioProcessor().ProcessWavFileAsync(path);
|
||||
|
||||
Assert.That(audio, Is.Not.Null);
|
||||
Assert.That(ReadFmtBitsPerSample(audio!.Buffer), Is.EqualTo(24), "Padded container must repack to 24-bit");
|
||||
Assert.That(ReadFmtAudioFormat(audio.Buffer), Is.EqualTo(WaveFormatPcm));
|
||||
var stored = await Materialize(audio!);
|
||||
Assert.That(ReadFmtBitsPerSample(stored), Is.EqualTo(24), "Padded container must repack to 24-bit");
|
||||
Assert.That(ReadFmtAudioFormat(stored), Is.EqualTo(WaveFormatPcm));
|
||||
// 4 container samples (4 bytes each) → 4 PCM samples (3 bytes each) = 12 data bytes.
|
||||
Assert.That(audio.Buffer.Length, Is.EqualTo(44 + 12));
|
||||
Assert.That(stored.Length, Is.EqualTo(44 + 12));
|
||||
// Verify the repacked sample values: lowest 3 bytes of each 4-byte little-endian container.
|
||||
// 0x123456 → LE 4 bytes: 56 34 12 00 → keep 3: 56 34 12
|
||||
// 0xFFEDCBA9 → LE 4 bytes: A9 CB ED FF → keep 3: A9 CB ED
|
||||
// 0x000001 → LE 4 bytes: 01 00 00 00 → keep 3: 01 00 00
|
||||
// 0xFF800000 → LE 4 bytes: 00 00 80 FF → keep 3: 00 00 80
|
||||
var expectedData = new byte[] { 0x56, 0x34, 0x12, 0xA9, 0xCB, 0xED, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80 };
|
||||
var actualData = audio.Buffer[44..];
|
||||
var actualData = stored[44..];
|
||||
Assert.That(actualData, Is.EqualTo(expectedData), "Padded 24-in-32 samples must strip the padding byte correctly");
|
||||
}
|
||||
|
||||
@@ -265,6 +274,15 @@ public class AudioProcessorTests
|
||||
|
||||
// -- helpers --------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Streams a <see cref="ProcessedAudio"/> store plan into memory so its canonical bytes
|
||||
/// can be asserted — the store path no longer hands back a materialized buffer.</summary>
|
||||
private static async Task<byte[]> Materialize(ProcessedAudio audio)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await audio.WriteToAsync(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synthesises a minimal valid MPEG1 Layer III CBR MP3 buffer: one frame header plus enough body
|
||||
/// bytes for the frame, with an optional Xing VBR header in the side-information region. The body
|
||||
|
||||
Reference in New Issue
Block a user