Files
deepdrft/DeepDrftContent/Processors/ProcessedAudio.cs
T
daniel-c-harvey 79bbbd4956 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.
2026-06-25 15:27:28 -04:00

69 lines
3.1 KiB
C#

namespace DeepDrftContent.Processors;
/// <summary>
/// The product of processing an uploaded audio file on the store path: the metadata SQL and the
/// vault index need, plus a streamed writer that emits the canonical vault bytes to a destination
/// stream without ever materializing the whole file in a managed <c>byte[]</c>.
///
/// This replaces the former whole-file <c>AudioBinary</c> as the processor output for upload /
/// replace-audio (Wave 1 OOM fix): passthrough formats (standard-PCM WAV, MP3, FLAC) stream the
/// source file straight to the destination, and EXTENSIBLE WAVs stream their normalization to
/// standard PCM. The vault <em>load</em> path still uses <c>AudioBinary</c> (a full buffer) — that
/// is the Wave 2 read path and is out of scope here.
///
/// <see cref="WriteToAsync"/> is invoked exactly once by the streaming vault register, against the
/// freshly opened backing <see cref="System.IO.FileStream"/>. The writer re-opens the source file
/// itself, so the source (a staging file) must still exist when the register runs — it does, because
/// processing and registration are sequential within the store call, before the staging-file
/// <c>finally</c> cleanup.
/// </summary>
public sealed class ProcessedAudio
{
/// <summary>The stored file extension (e.g. <c>.wav</c>, <c>.mp3</c>, <c>.flac</c>).</summary>
public string Extension { get; }
/// <summary>Audio duration in seconds, extracted from the header.</summary>
public double Duration { get; }
/// <summary>Audio bitrate in kbps, extracted from (or estimated for) the header.</summary>
public int Bitrate { get; }
/// <summary>
/// The canonical stored byte count — computed from the header and file length, never by
/// buffering the body. Used only for diagnostics (confirming the streamed path was taken).
/// </summary>
public long Size { get; }
private readonly Func<Stream, CancellationToken, Task> _writeTo;
public ProcessedAudio(
string extension,
double duration,
int bitrate,
long size,
Func<Stream, CancellationToken, Task> writeTo)
{
Extension = extension;
Duration = duration;
Bitrate = bitrate;
Size = size;
_writeTo = writeTo;
}
/// <summary>
/// Streams the canonical vault bytes to <paramref name="destination"/>. Bounded-buffer — peak
/// managed memory is O(buffer), not O(filesize).
/// </summary>
public Task WriteToAsync(Stream destination, CancellationToken cancellationToken = default)
=> _writeTo(destination, cancellationToken);
/// <summary>
/// Builds a passthrough plan: the stored bytes are byte-identical to the source file (standard
/// PCM WAV, MP3, FLAC — no transcoding). The writer is a bounded disk-to-disk copy.
/// </summary>
public static ProcessedAudio Passthrough(
string sourcePath, string extension, double duration, int bitrate, long sourceLength)
=> new(extension, duration, bitrate, sourceLength,
(destination, ct) => AudioStoreStream.CopyFileAsync(sourcePath, destination, ct));
}