feat(audio): add MP3 and FLAC upload support via format-routed processors
AudioProcessorRouter dispatches by extension; vault stores original bytes with correct MIME type.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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;
|
||||
|
||||
public async Task<AudioBinary?> ProcessFlacFileAsync(string filePath)
|
||||
{
|
||||
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 buffer = await File.ReadAllBytesAsync(filePath);
|
||||
var meta = ExtractFlacMetadata(buffer);
|
||||
|
||||
var parameters = new AudioBinaryParams(
|
||||
Buffer: buffer,
|
||||
Size: buffer.Length,
|
||||
Extension: ".flac",
|
||||
Duration: meta.Duration,
|
||||
Bitrate: meta.Bitrate);
|
||||
|
||||
return new AudioBinary(parameters);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
private static FlacMetadata ExtractFlacMetadata(byte[] buffer)
|
||||
{
|
||||
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)(buffer.LongLength * 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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user