Files
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

105 lines
4.4 KiB
C#

using DeepDrftContent.FileDatabase.Models;
namespace DeepDrftContent.Processors;
/// <summary>
/// Extracts metadata from a FLAC file and wraps its <b>unmodified</b> bytes in an
/// <see cref="AudioBinary"/> tagged <c>.flac</c>. No transcoding — the vault stores the original
/// stream; duration and average bitrate come from the mandatory STREAMINFO metadata block.
/// </summary>
public class FlacAudioProcessor
{
private const double FallbackDuration = 180.0;
private const int FallbackBitrate = 1411;
// STREAMINFO is mandatory and always the first metadata block, immediately after the 4-byte magic
// (data at offset 8, 34 bytes). A small prefix read covers it without loading the body.
private const long HeaderCap = 64 * 1024;
public async Task<ProcessedAudio?> ProcessFlacFileAsync(string filePath, CancellationToken cancellationToken = default)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"FLAC file not found: {filePath}");
}
if (!Path.GetExtension(filePath).Equals(".flac", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("File must be a FLAC file", nameof(filePath));
}
var fileLength = new FileInfo(filePath).Length;
var window = await AudioStoreStream.ReadPrefixAsync(filePath, HeaderCap, cancellationToken);
var meta = ExtractFlacMetadata(window, fileLength);
// FLAC is stored unmodified — passthrough the original bytes via a streamed disk-to-disk copy.
return ProcessedAudio.Passthrough(filePath, ".flac", meta.Duration, meta.Bitrate, fileLength);
}
/// <summary>
/// Validates the <c>fLaC</c> magic and the leading STREAMINFO block, then computes duration from
/// total-samples / sample-rate and average bitrate from file size. 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 average-bitrate computation.
/// </summary>
private static FlacMetadata ExtractFlacMetadata(byte[] buffer, long fileLength)
{
try
{
// Magic (4) + metadata block header (4) + STREAMINFO data (34) = 42 bytes minimum.
if (buffer.Length < 42)
{
throw new InvalidDataException("File too short for FLAC STREAMINFO");
}
if (buffer[0] != 'f' || buffer[1] != 'L' || buffer[2] != 'a' || buffer[3] != 'C')
{
throw new InvalidDataException("Invalid fLaC magic");
}
// Metadata block header at offset 4: bits 6-0 of byte 0 are the block type (0 = STREAMINFO).
var blockType = buffer[4] & 0x7F;
if (blockType != 0)
{
throw new InvalidDataException($"First metadata block is not STREAMINFO (type {blockType})");
}
// STREAMINFO data begins at offset 8. Layout (bit-packed, big-endian):
// bytes 10-12 + top nibble of 13: sample rate (20 bits)
// bits 3-1 of byte 12: channels - 1
// bit 0 of byte 12 + top 4 bits of byte 13: bits per sample - 1
// low nibble of byte 13 + bytes 14-17: total samples (36 bits)
var d = 8;
var sampleRate = (buffer[d + 10] << 12) | (buffer[d + 11] << 4) | (buffer[d + 12] >> 4);
var totalSamples = ((long)(buffer[d + 13] & 0x0F) << 32)
| ((long)buffer[d + 14] << 24)
| ((long)buffer[d + 15] << 16)
| ((long)buffer[d + 16] << 8)
| buffer[d + 17];
if (sampleRate <= 0)
{
throw new InvalidDataException("Invalid FLAC sample rate");
}
var duration = (double)totalSamples / sampleRate;
var bitrate = duration > 0
? (int)(fileLength * 8L / (duration * 1000))
: FallbackBitrate;
return new FlacMetadata { Duration = duration, Bitrate = bitrate };
}
catch (Exception ex)
{
Console.WriteLine($"Warning: FLAC parsing failed, using defaults: {ex.Message}");
return new FlacMetadata { Duration = FallbackDuration, Bitrate = FallbackBitrate };
}
}
private sealed class FlacMetadata
{
public double Duration { get; init; }
public int Bitrate { get; init; }
}
}