using DeepDrftContent.FileDatabase.Models; namespace DeepDrftContent.Processors; /// /// Extracts metadata from a FLAC file and wraps its unmodified bytes in an /// tagged .flac. No transcoding — the vault stores the original /// stream; duration and average bitrate come from the mandatory STREAMINFO metadata block. /// 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 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); } /// /// Validates the fLaC 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. is the true /// file size (the header window may be shorter), used for the average-bitrate computation. /// 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; } } }