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:
@@ -25,7 +25,13 @@ public class Mp3AudioProcessor
|
||||
private const double FallbackDuration = 180.0;
|
||||
private const int FallbackBitrate = 320;
|
||||
|
||||
public async Task<AudioBinary?> ProcessMp3FileAsync(string filePath)
|
||||
// Metadata lives in the leading ID3v2 tag plus the first MPEG frame. Cap the header read so a
|
||||
// large MP3 is not pulled into memory whole just to read it; a tag larger than this (very large
|
||||
// embedded art) simply falls back to the CBR/default estimate, never an OOM. The body is stored
|
||||
// by streaming the original file, not from this window.
|
||||
private const long HeaderCap = 8 * 1024 * 1024;
|
||||
|
||||
public async Task<ProcessedAudio?> ProcessMp3FileAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
@@ -37,24 +43,21 @@ public class Mp3AudioProcessor
|
||||
throw new ArgumentException("File must be an MP3 file", nameof(filePath));
|
||||
}
|
||||
|
||||
var buffer = await File.ReadAllBytesAsync(filePath);
|
||||
var meta = ExtractMp3Metadata(buffer);
|
||||
var fileLength = new FileInfo(filePath).Length;
|
||||
var window = await AudioStoreStream.ReadPrefixAsync(filePath, HeaderCap, cancellationToken);
|
||||
var meta = ExtractMp3Metadata(window, fileLength);
|
||||
|
||||
var parameters = new AudioBinaryParams(
|
||||
Buffer: buffer,
|
||||
Size: buffer.Length,
|
||||
Extension: ".mp3",
|
||||
Duration: meta.Duration,
|
||||
Bitrate: meta.Bitrate);
|
||||
|
||||
return new AudioBinary(parameters);
|
||||
// MP3 is stored unmodified — passthrough the original bytes via a streamed disk-to-disk copy.
|
||||
return ProcessedAudio.Passthrough(filePath, ".mp3", meta.Duration, meta.Bitrate, fileLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the first valid MPEG frame (after any ID3v2 tag) and any Xing/VBRI tag inside it.
|
||||
/// On any parse failure, logs a warning and returns synthetic defaults — never throws.
|
||||
/// <paramref name="fileLength"/> is the true file size (the header window may be shorter), used
|
||||
/// for the CBR duration estimate.
|
||||
/// </summary>
|
||||
private static Mp3Metadata ExtractMp3Metadata(byte[] buffer)
|
||||
private static Mp3Metadata ExtractMp3Metadata(byte[] buffer, long fileLength)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -65,7 +68,7 @@ public class Mp3AudioProcessor
|
||||
}
|
||||
|
||||
var header = DecodeFrameHeader(buffer, frameStart);
|
||||
var duration = ComputeDuration(buffer, frameStart, header);
|
||||
var duration = ComputeDuration(buffer, frameStart, header, fileLength);
|
||||
|
||||
return new Mp3Metadata { Duration = duration, Bitrate = header.BitrateKbps };
|
||||
}
|
||||
@@ -202,7 +205,7 @@ public class Mp3AudioProcessor
|
||||
/// Computes duration from a Xing/Info or VBRI tag (accurate for VBR) when present; otherwise
|
||||
/// falls back to the CBR estimate fileSize / (bitrate_kbps * 125). Guards divide-by-zero.
|
||||
/// </summary>
|
||||
private static double ComputeDuration(byte[] buffer, int frameStart, FrameHeader header)
|
||||
private static double ComputeDuration(byte[] buffer, int frameStart, FrameHeader header, long fileLength)
|
||||
{
|
||||
var xingFrames = ReadXingFrameCount(buffer, frameStart, header);
|
||||
if (xingFrames > 0 && header.SampleRate > 0)
|
||||
@@ -216,10 +219,10 @@ public class Mp3AudioProcessor
|
||||
return (double)vbriFrames * header.SamplesPerFrame / header.SampleRate;
|
||||
}
|
||||
|
||||
// CBR fallback: bitrate_kbps * 1000 / 8 bytes per second = bitrate_kbps * 125.
|
||||
// Exclude the ID3v2 tag bytes (everything before frameStart) from the estimate.
|
||||
// CBR fallback: bitrate_kbps * 1000 / 8 bytes per second = bitrate_kbps * 125. Uses the true
|
||||
// file length (not the bounded header window), excluding the ID3v2 tag bytes before frameStart.
|
||||
var bytesPerSecond = header.BitrateKbps * 125;
|
||||
return bytesPerSecond > 0 ? (double)(buffer.Length - frameStart) / bytesPerSecond : FallbackDuration;
|
||||
return bytesPerSecond > 0 ? (double)(fileLength - frameStart) / bytesPerSecond : FallbackDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user