using System; using System.IO; namespace ADOFAIRenderer.Util.Audio.Clip; /// /// Detects the VBR/info header frame ("Xing", "Info" or "VBRI") at the start of an MP3 file. /// /// The game decodes MP3 files with MP3Sharp (Assembly-CSharp-firstpass, MP3Sharp.MP3Stream), which has no header-frame handling: /// it decodes that frame as one frame of silence and keeps the encoder delay. FFmpeg's MP3 demuxer consumes the header frame /// without output. Measured with the clip harness (LAME 192k CBR, LAME VBR, 44.1/48 kHz, with and without ID3v2): /// MP3Sharp output is FFmpeg's untrimmed output preceded by one frame of silence when the header frame exists, /// and identical in timing when it does not. /// /// Frame layout used here (ISO/IEC 11172-3 and 13818-3 frame header; Xing/Info tag after the Layer III side information, /// VBRI tag 32 bytes after the frame header): /// - Header: 11 sync bits, version (2 bits: 3 = MPEG-1, 2 = MPEG-2, 0 = MPEG-2.5), layer (2 bits: 1 = III, 2 = II, 3 = I), /// protection bit (0 = 16-bit CRC follows), bitrate index, sample rate index, padding, private, channel mode (3 = mono). /// - Layer III side information: MPEG-1 32 bytes (17 mono), MPEG-2/2.5 17 bytes (9 mono). /// - Samples per frame: Layer I 384, Layer II 1152, Layer III 1152 (MPEG-1) or 576 (MPEG-2/2.5). /// - ID3v2 tags before the first frame: "ID3", version (2 bytes), flags (bit 0x10 = 10-byte footer), size (4 bytes, 7 bits each). /// internal static class MP3HeaderFrame { /// Bytes searched for the first frame sync after the ID3v2 tags. private const int SyncSearchBytes = 64 * 1024; /// /// Frames of silence MP3Sharp outputs for the header frame of : /// the samples per frame of the first frame when it is a Xing/Info/VBRI header frame, otherwise 0. /// public static int SilentFrames(string path) { // The game keeps the song open while it streams it (MP3Sharp.MP3Stream: new FileStream(path, FileMode.Open), // read/write access with read sharing), so the file is opened with read/write/delete sharing. using var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); return SilentFrames(file); } /// /// for a seekable stream positioned anywhere (the stream is read from offset 0). /// Only the first valid frame header within after the ID3v2 tags is examined. /// public static int SilentFrames(Stream stream) { // Skip consecutive ID3v2 tags: 10-byte header + syncsafe size (+ 10-byte footer when flagged). var tag = new byte[10]; long offset = 0; while (true) { stream.Position = offset; if (ReadFully(stream, tag, 10) < 10 || tag[0] != 'I' || tag[1] != 'D' || tag[2] != '3') break; var size = (tag[6] & 0x7F) << 21 | (tag[7] & 0x7F) << 14 | (tag[8] & 0x7F) << 7 | (tag[9] & 0x7F); offset += 10 + size + ((tag[5] & 0x10) != 0 ? 10 : 0); } stream.Position = offset; var data = new byte[SyncSearchBytes]; var length = ReadFully(stream, data, data.Length); for (var i = 0; i + 4 <= length; i++) { if (!TryParseHeader(data, i, out var samplesPerFrame, out var tagOffset)) continue; // Xing/Info after the side information; VBRI at a fixed offset of 32 bytes after the 4-byte header. if (HasTag(data, length, i + tagOffset, "Xing") || HasTag(data, length, i + tagOffset, "Info") || HasTag(data, length, i + 4 + 32, "VBRI")) return samplesPerFrame; return 0; } return 0; } /// /// Parses the 4-byte frame header at . False for a missing sync or a reserved version, layer, bitrate /// index (15) or sample rate index (3). /// /// Bytes to parse. /// Offset of the first header byte in . /// Samples per channel in the frame (384, 576 or 1152). /// Offset of a Xing/Info tag from the header start: 4 + CRC (2) + Layer III side information (0 for Layers I/II). private static bool TryParseHeader(byte[] data, int i, out int samplesPerFrame, out int tagOffset) { samplesPerFrame = 0; tagOffset = 0; if (data[i] != 0xFF || (data[i + 1] & 0xE0) != 0xE0) return false; var version = (data[i + 1] >> 3) & 0x3; var layer = (data[i + 1] >> 1) & 0x3; var hasCrc = (data[i + 1] & 0x1) == 0; var bitrateIndex = (data[i + 2] >> 4) & 0xF; var sampleRateIndex = (data[i + 2] >> 2) & 0x3; var mono = ((data[i + 3] >> 6) & 0x3) == 3; if (version == 1 || layer == 0 || bitrateIndex == 0xF || sampleRateIndex == 3) return false; var mpeg1 = version == 3; samplesPerFrame = layer switch { 3 => 384, 2 => 1152, _ => mpeg1 ? 1152 : 576 }; var sideInfo = layer == 1 ? (mpeg1 ? (mono ? 17 : 32) : (mono ? 9 : 17)) : 0; tagOffset = 4 + (hasCrc ? 2 : 0) + sideInfo; return true; } /// True when the ASCII string is at within the first bytes. private static bool HasTag(byte[] data, int length, int at, string tag) { if (at < 0 || at + tag.Length > length) return false; for (var k = 0; k < tag.Length; k++) if (data[at + k] != tag[k]) return false; return true; } /// Reads until bytes or the end of the stream; returns the bytes read. private static int ReadFully(Stream stream, byte[] buffer, int count) { var total = 0; int read; while (total < count && (read = stream.Read(buffer, total, count - total)) > 0) total += read; return total; } }