├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md └── SngTool ├── NLayer ├── Decoder │ ├── BitReservoir.cs │ ├── FrameBase.cs │ ├── Huffman.cs │ ├── ID3Frame.cs │ ├── Layer1Decoder.cs │ ├── Layer2Decoder.cs │ ├── Layer2DecoderBase.cs │ ├── Layer3Decoder.cs │ ├── LayerDecoderBase.cs │ ├── MpegFrame.cs │ ├── MpegStreamReader.cs │ ├── RiffHeaderFrame.cs │ └── VBRInfo.cs ├── Enums.cs ├── IMpegFrame.cs ├── MpegFile.cs ├── MpegFrameDecoder.cs └── NLayer.csproj ├── NVorbis ├── Codebook.cs ├── Contracts │ ├── HuffmanListNode.cs │ ├── IContainerReader.cs │ ├── IFloor.cs │ ├── IPacketProvider.cs │ ├── IStreamDecoder.cs │ ├── IStreamStats.cs │ ├── ITagData.cs │ ├── IVorbisReader.cs │ └── Ogg │ │ ├── IForwardOnlyPacketProvider.cs │ │ ├── IPageReader.cs │ │ ├── IStreamPageReader.cs │ │ └── PageFlags.cs ├── EndOfStreamFlags.cs ├── Floor0.cs ├── Floor1.cs ├── FloorData.cs ├── Huffman.cs ├── Mapping.cs ├── Mdct.cs ├── Mode.cs ├── NVorbis.csproj ├── NewStreamEventArgs.cs ├── Ogg │ ├── ContainerReader.cs │ ├── Crc.Table.cs │ ├── Crc.cs │ ├── IPacketGranuleCountProvider.cs │ ├── PacketData.cs │ ├── PacketLocation.cs │ ├── PacketProvider.cs │ ├── PageData.cs │ ├── PageHeader.cs │ ├── PageReader.cs │ ├── PageReaderBase.cs │ ├── PageSlice.cs │ └── StreamPageReader.cs ├── PacketExtensions.cs ├── PageDataPool.cs ├── PreRollPacketException.cs ├── RefCounted.cs ├── Residue0.cs ├── Residue1.cs ├── Residue2.cs ├── SeekOutOfRangeException.cs ├── StreamDecoder.cs ├── StreamStats.cs ├── TagData.cs ├── Utils.cs ├── Vector128Helper.cs ├── Vector256Helper.cs ├── VectorHelper.cs ├── VorbisConfig.cs ├── VorbisPacket.cs └── VorbisReader.cs ├── SngCli ├── ConMan.cs ├── KnownKeys.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SngCli.csproj ├── SngDecode.cs ├── SngDecodingOptions.cs ├── SngEncode.cs ├── SngEncodingOptions.cs └── build.bat ├── SngLib ├── LargeFile.cs ├── NativeByteArray │ ├── NativeByteArray.cs │ ├── NativeByteArrayExtensions.cs │ ├── PointerMemoryManager.cs │ └── ThrowHelper.cs ├── SngFile.cs ├── SngLib.csproj └── SngSerializer.cs ├── SngTool.sln └── SongLib ├── AudioEncoding.cs ├── FormatDetection ├── OggParser.cs └── WavParser.cs ├── IniParser.cs ├── JpegEncoding.cs ├── SongLib.csproj ├── Utils.cs ├── WavFileWriter.cs └── deps └── opusenc.exe /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: mattgamedev 5 | custom: ['https://streamelements.com/mdsitton/tip'] 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/SngTool/SngCli/bin/Debug/net7.0/win-x64/SngCli.dll", 14 | "args": [ 15 | "encode", 16 | "--opusEncode", 17 | "--jpegEncode", 18 | "--albumUpscale", 19 | "--noStatusBar", 20 | "--in", 21 | "c:\\infinite-chart\\", 22 | "--out", 23 | "C:\\bad-inf-enc\\" 24 | ], 25 | "cwd": "${workspaceFolder}/SngTool/SngCli/bin/Debug/net7.0/win-x64/", 26 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 27 | "console": "integratedTerminal", 28 | "stopAtEntry": false 29 | }, 30 | { 31 | "name": ".NET Core Attach", 32 | "type": "coreclr", 33 | "request": "attach" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/SngTool/SngCli/SngCli.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/SngTool/SngCli/SngCli.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/SngTool/SngCli/SngCli.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Matthew Sitton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /SngTool/NLayer/Decoder/BitReservoir.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace NLayer.Decoder 6 | { 7 | internal class BitReservoir 8 | { 9 | public const int BufferSize = 8192; 10 | 11 | // Per the spec, the maximum buffer size for layer III is 7680 bits, which is 960 bytes. 12 | // The only catch is if we're decoding a "free" frame, which could be a lot more (since 13 | // some encoders allow higher bitrates to maintain audio transparency). 14 | private byte[] _buf = new byte[BufferSize]; 15 | private int _start = 0, _end = -1, _bitsLeft = 0; 16 | private long _bitsRead = 0L; 17 | 18 | private static int GetSlots(MpegFrame frame) 19 | { 20 | int cnt = frame.FrameLength - 4; 21 | if (frame.HasCrc) 22 | cnt -= 2; 23 | 24 | if (frame.Version == MpegVersion.Version1 && frame.ChannelMode != MpegChannelMode.Mono) 25 | return cnt - 32; 26 | if (frame.Version > MpegVersion.Version1 && frame.ChannelMode == MpegChannelMode.Mono) 27 | return cnt - 9; 28 | return cnt - 17; 29 | } 30 | 31 | public bool AddBits(MpegFrame frame, int overlap) 32 | { 33 | int originalEnd = _end; 34 | 35 | int slots = GetSlots(frame); 36 | while (--slots >= 0) 37 | { 38 | int tmp = frame.ReadBits(8); 39 | if (tmp == -1) 40 | ThrowFrameNotEnoughBytes(); 41 | 42 | _buf[++_end] = (byte)tmp; 43 | if (_end == BufferSize - 1) 44 | _end = -1; 45 | } 46 | 47 | _bitsLeft = 8; 48 | if (originalEnd == -1) 49 | { 50 | // it's either the start of the stream or we've reset... 51 | // only return true if overlap says this frame is enough 52 | return overlap == 0; 53 | } 54 | else 55 | { 56 | // it's not the start of the stream so calculate _start based on whether we have enough bytes left 57 | 58 | // if we have enough bytes, reset start to match overlap 59 | if ((originalEnd + 1 - _start + BufferSize) % BufferSize >= overlap) 60 | { 61 | _start = (originalEnd + 1 - overlap + BufferSize) % BufferSize; 62 | return true; 63 | } 64 | // otherwise, just set start to match the start of the frame (we probably skipped a frame) 65 | else 66 | { 67 | _start = originalEnd + overlap; 68 | return false; 69 | } 70 | } 71 | } 72 | 73 | public int GetBits(int count) 74 | { 75 | int bits = TryPeekBits(count, out int bitsRead); 76 | if (bitsRead < count) 77 | ThrowReservoirNotEnoughBits(); 78 | 79 | SkipBits(count); 80 | return bits; 81 | } 82 | 83 | // this is an optimized single-bit read 84 | public int Get1Bit() 85 | { 86 | if (_bitsLeft == 0) 87 | ThrowReservoirNotEnoughBits(); 88 | 89 | _bitsLeft--; 90 | _bitsRead++; 91 | int val = (_buf[_start] >> _bitsLeft) & 1; 92 | 93 | if (_bitsLeft == 0) 94 | { 95 | if (++_start >= BufferSize) 96 | _start = 0; 97 | 98 | if (_start != _end + 1) 99 | _bitsLeft = 8; 100 | } 101 | 102 | return val; 103 | } 104 | 105 | public int TryPeekBits(int count, out int readCount) 106 | { 107 | Debug.Assert(count >= 0 && count < 32, "Count must be between 0 and 32 bits."); 108 | 109 | int bitsLeft = _bitsLeft; 110 | 111 | // if we don't have any bits left, just return no bits read 112 | if (bitsLeft == 0 || count == 0) 113 | { 114 | readCount = 0; 115 | return 0; 116 | } 117 | 118 | byte[] buf = _buf; 119 | 120 | // get bits from the current start of the reservoir 121 | int bits = buf[_start]; 122 | if (count < bitsLeft) 123 | { 124 | // just grab the bits, adjust the "left" count, and return 125 | bits >>= bitsLeft - count; 126 | bits &= (1 << count) - 1; 127 | readCount = count; 128 | return bits; 129 | } 130 | 131 | // we have to do it the hard way... 132 | bits &= (1 << bitsLeft) - 1; 133 | count -= bitsLeft; 134 | readCount = bitsLeft; 135 | 136 | int resStart = _start; 137 | 138 | // arg... gotta grab some more bits... 139 | // advance the start marker, and if we just advanced it past the end of the buffer, bail 140 | while (count > 0) 141 | { 142 | if (++resStart >= BufferSize) 143 | resStart = 0; 144 | else if (resStart == _end + 1) 145 | break; 146 | 147 | // figure out how many bits to pull from it 148 | int bitsToRead = Math.Min(count, 8); 149 | 150 | // move the existing bits over 151 | bits <<= bitsToRead; 152 | bits |= buf[resStart] >> (8 - bitsToRead); 153 | 154 | // update our count 155 | count -= bitsToRead; 156 | 157 | // update our remaining bits 158 | readCount += bitsToRead; 159 | } 160 | 161 | return bits; 162 | } 163 | 164 | public int BitsAvailable 165 | { 166 | get 167 | { 168 | if (_bitsLeft > 0) 169 | return ((_end - _start + BufferSize) % BufferSize * 8) + _bitsLeft; 170 | 171 | return 0; 172 | } 173 | } 174 | 175 | public long BitsRead => _bitsRead; 176 | 177 | public void SkipBits(int count) 178 | { 179 | if (count > 0) 180 | { 181 | // make sure we have enough bits to skip 182 | Debug.Assert(count <= BitsAvailable); 183 | 184 | // now calculate the new positions 185 | int offset = (8 - _bitsLeft) + count; 186 | _start = ((offset / 8) + _start) % BufferSize; 187 | _bitsLeft = 8 - (offset % 8); 188 | 189 | _bitsRead += count; 190 | } 191 | } 192 | 193 | public void RewindBits(int count) 194 | { 195 | _bitsLeft += count; 196 | _bitsRead -= count; 197 | 198 | while (_bitsLeft > 8) 199 | { 200 | _start--; 201 | _bitsLeft -= 8; 202 | } 203 | 204 | while (_start < 0) 205 | { 206 | _start += BufferSize; 207 | } 208 | } 209 | 210 | public void FlushBits() 211 | { 212 | if (_bitsLeft < 8) 213 | { 214 | SkipBits(_bitsLeft); 215 | } 216 | } 217 | 218 | public void Reset() 219 | { 220 | _start = 0; 221 | _end = -1; 222 | _bitsLeft = 0; 223 | } 224 | 225 | private static void ThrowReservoirNotEnoughBits() 226 | { 227 | throw new System.IO.InvalidDataException("Reservoir did not have enough bytes!"); 228 | } 229 | 230 | private static void ThrowFrameNotEnoughBytes() 231 | { 232 | throw new System.IO.InvalidDataException("Frame did not have enough bytes!"); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /SngTool/NLayer/Decoder/FrameBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace NLayer.Decoder 5 | { 6 | public abstract class FrameBase 7 | { 8 | private static int _totalAllocation = 0; 9 | 10 | public static int TotalAllocation => 11 | System.Threading.Interlocked.CompareExchange(ref _totalAllocation, 0, 0); 12 | 13 | public long Offset { get; private set; } 14 | public int Length { get; set; } 15 | 16 | private MpegStreamReader? _reader; 17 | private byte[]? _savedBuffer; 18 | 19 | protected FrameBase() 20 | { 21 | } 22 | 23 | /// 24 | /// Called to validate the frame header 25 | /// 26 | /// The length of the frame, or -1 if frame is invalid 27 | protected abstract int ValidateFrameHeader(); 28 | 29 | public bool ValidateFrameHeader(long offset, MpegStreamReader reader) 30 | { 31 | _reader = reader ?? throw new ArgumentNullException(nameof(reader)); 32 | Offset = offset; 33 | 34 | int length = ValidateFrameHeader(); 35 | if (length > 0) 36 | { 37 | Length = length; 38 | return true; 39 | } 40 | return false; 41 | } 42 | 43 | protected int Read(int offset, Span destination) 44 | { 45 | if (_savedBuffer != null) 46 | { 47 | ReadOnlySpan source = _savedBuffer.AsSpan(offset); 48 | source = source.Slice(0, Math.Min(source.Length, destination.Length)); 49 | source.CopyTo(destination); 50 | return source.Length; 51 | } 52 | else 53 | { 54 | Debug.Assert(_reader != null); 55 | return _reader.Read(Offset + offset, destination); 56 | } 57 | } 58 | 59 | protected int ReadByte(int offset) 60 | { 61 | if (_savedBuffer != null) 62 | { 63 | if (offset >= _savedBuffer.Length) 64 | return -1; 65 | return _savedBuffer[offset]; 66 | } 67 | else 68 | { 69 | Debug.Assert(_reader != null); 70 | return _reader.ReadByte(Offset + offset); 71 | } 72 | } 73 | 74 | public void SaveBuffer() 75 | { 76 | Debug.Assert(_reader != null); 77 | 78 | _savedBuffer = new byte[Length]; 79 | _reader.Read(Offset, _savedBuffer); 80 | System.Threading.Interlocked.Add(ref _totalAllocation, Length); 81 | } 82 | 83 | public void ClearBuffer() 84 | { 85 | System.Threading.Interlocked.Add(ref _totalAllocation, -Length); 86 | _savedBuffer = null; 87 | } 88 | 89 | /// 90 | /// Called when the stream is not seekable. 91 | /// 92 | public virtual void Parse() 93 | { 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SngTool/NLayer/Decoder/ID3Frame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NLayer.Decoder 4 | { 5 | internal sealed class ID3Frame : FrameBase 6 | { 7 | private int _version; 8 | 9 | private ID3Frame() 10 | { 11 | } 12 | 13 | protected override int ValidateFrameHeader() 14 | { 15 | switch (_version) 16 | { 17 | case 2: 18 | // v2, yay! 19 | Span buf = stackalloc byte[7]; 20 | if (Read(3, buf) == 7) 21 | { 22 | byte flagsMask; 23 | switch (buf[0]) 24 | { 25 | case 2: 26 | flagsMask = 0x3F; 27 | break; 28 | case 3: 29 | flagsMask = 0x1F; 30 | break; 31 | 32 | case 4: 33 | flagsMask = 0x0F; 34 | break; 35 | 36 | default: 37 | return -1; 38 | } 39 | 40 | // ignore the flags (we don't need them for the validation) 41 | 42 | // get the size (7 bits per byte [MSB cleared]) 43 | var size = (buf[3] << 21) 44 | | (buf[4] << 14) 45 | | (buf[5] << 7) 46 | | (buf[6]); 47 | 48 | // finally, check to make sure that all the right bits are cleared 49 | int flags = 50 | (buf[2] & flagsMask) | 51 | (buf[3] & 0x80) | 52 | (buf[4] & 0x80) | 53 | (buf[5] & 0x80) | 54 | (buf[6] & 0x80); 55 | 56 | if (!(flags != 0 || buf[1] == 0xFF)) 57 | return size + 10; // don't forget the sync, flag & size bytes! 58 | } 59 | break; 60 | 61 | case 1: 62 | return 227 + 128; 63 | 64 | case 0: 65 | return 128; 66 | } 67 | 68 | return -1; 69 | } 70 | 71 | public override void Parse() 72 | { 73 | // assume we have to process it now or else... 74 | // we can still read the whole frame, so no biggie 75 | switch (_version) 76 | { 77 | case 2: 78 | ParseV2(); 79 | break; 80 | 81 | case 1: 82 | ParseV1Enh(); 83 | break; 84 | 85 | case 0: 86 | ParseV1(3); 87 | break; 88 | } 89 | } 90 | 91 | private void ParseV1(int offset) 92 | { 93 | //var buffer = new byte[125]; 94 | //if (Read(offset, buffer) == 125) 95 | //{ 96 | // // v1 tags use ASCII encoding... 97 | // // For now we'll use the built-in encoding, 98 | // // but for Win8 we'll have to build our own. 99 | // var encoding = Encoding.ASCII; 100 | // 101 | // // title (30) 102 | // Title = encoding.GetString(buffer, 0, 30); 103 | // 104 | // // artist (30) 105 | // Artist = encoding.GetString(buffer, 30, 30); 106 | // 107 | // // album (30) 108 | // Album = encoding.GetString(buffer, 60, 30); 109 | // 110 | // // year (4) 111 | // Year = encoding.GetString(buffer, 90, 30); 112 | // 113 | // // comment (30)* 114 | // Comment = encoding.GetString(buffer, 94, 30); 115 | // 116 | // if (buffer[122] == 0) 117 | // { 118 | // // track (1)* 119 | // Track = (int)buffer[123]; 120 | // } 121 | // 122 | // // genre (1) 123 | // // ignore for now 124 | // 125 | // // * if byte 29 of comment is 0, track is byte 30. Otherwise, track is unknown. 126 | //} 127 | } 128 | 129 | private void ParseV1Enh() 130 | { 131 | ParseV1(230); 132 | 133 | //var buffer = new byte[223]; 134 | //if (Read(4, buffer) == 223) 135 | //{ 136 | // // v1 tags use ASCII encoding... 137 | // // For now we'll use the built-in encoding, but for Win8 we'll have to build our own. 138 | // var encoding = Encoding.ASCII; 139 | // 140 | // // title (60) 141 | // Title += encoding.GetString(buffer, 0, 60); 142 | // 143 | // // artist (60) 144 | // Artist += encoding.GetString(buffer, 60, 60); 145 | // 146 | // // album (60) 147 | // Album += encoding.GetString(buffer, 120, 60); 148 | // 149 | // // speed (1) 150 | // //var speed = buffer[180]; 151 | // 152 | // // genre (30) 153 | // Genre = encoding.GetString(buffer, 181, 30); 154 | // 155 | // // start-time (6) 156 | // // 211 157 | // 158 | // // end-time (6) 159 | // // 217 160 | //} 161 | } 162 | 163 | private void ParseV2() 164 | { 165 | // v2 is much more complicated than v1... don't worry about it for now 166 | // look for any merged frames, as well 167 | } 168 | 169 | public int Version 170 | { 171 | get 172 | { 173 | if (_version == 0) 174 | return 1; 175 | return _version; 176 | } 177 | } 178 | 179 | //public string Title { get; private set; } 180 | //public string Artist { get; private set; } 181 | //public string Album { get; private set; } 182 | //public string Year { get; private set; } 183 | //public string Comment { get; private set; } 184 | //public int Track { get; private set; } 185 | //public string Genre { get; private set; } 186 | // speed 187 | //public TimeSpan StartTime { get; private set; } 188 | //public TimeSpan EndTime { get; private set; } 189 | 190 | public void Merge(ID3Frame newFrame) 191 | { 192 | // just save off the frame for parsing later 193 | } 194 | 195 | public static ID3Frame? TrySync(uint syncMark) 196 | { 197 | if ((syncMark & 0xFFFFFF00U) == 0x49443300) 198 | return new ID3Frame { _version = 2 }; 199 | 200 | if ((syncMark & 0xFFFFFF00U) == 0x54414700) 201 | { 202 | if ((syncMark & 0xFF) == 0x2B) 203 | return new ID3Frame { _version = 1 }; 204 | else 205 | return new ID3Frame { _version = 0 }; 206 | } 207 | 208 | return null; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /SngTool/NLayer/Decoder/Layer1Decoder.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * NLayer - A C# MPEG1/2/2.5 audio decoder 3 | * 4 | */ 5 | 6 | namespace NLayer.Decoder 7 | { 8 | // Layer I is really just a special case of Layer II... 9 | // 1 granule, 4 allocation bits per subband, 1 scalefactor per active subband, no grouping 10 | // That (of course) means we literally have no logic here 11 | internal class Layer1Decoder : Layer2DecoderBase 12 | { 13 | // this is simple: all 32 subbands have a 4-bit allocations, and positive allocation values are {bits per sample} - 1 14 | private static readonly int[] _rateTable = { 15 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 16 | }; 17 | 18 | private static readonly int[][] _allocLookupTable = { 19 | new int[] { 4, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } 20 | }; 21 | 22 | public Layer1Decoder() : base(_allocLookupTable, 1) 23 | { 24 | } 25 | 26 | public static bool GetCRC(MpegFrame frame, ref uint crc) 27 | { 28 | return GetCRC(frame, _rateTable, _allocLookupTable, false, ref crc); 29 | } 30 | 31 | protected override int[] GetRateTable(MpegFrame frame) 32 | { 33 | return _rateTable; 34 | } 35 | 36 | protected override void ReadScaleFactorSelection(MpegFrame frame, int[][] scfsi, int channels) 37 | { 38 | // this is a no-op since the base logic uses "2" as the "has energy" marker 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SngTool/NLayer/Decoder/Layer2Decoder.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * NLayer - A C# MPEG1/2/2.5 audio decoder 3 | * 4 | */ 5 | 6 | namespace NLayer.Decoder 7 | { 8 | // there's not much we have to do here... table selection, granule count, scalefactor selection 9 | internal class Layer2Decoder : Layer2DecoderBase 10 | { 11 | public Layer2Decoder() : base(_allocLookupTable, 3) 12 | { 13 | } 14 | 15 | public static bool GetCRC(MpegFrame frame, ref uint crc) 16 | { 17 | return GetCRC(frame, SelectTable(frame), _allocLookupTable, true, ref crc); 18 | } 19 | 20 | // figure out which rate table to use... 21 | // basically, high-rate full, high-rate limited, low-rate limited, low-rate minimal, and LSF. 22 | private static int[] SelectTable(MpegFrame frame) 23 | { 24 | var bitRatePerChannel = (frame.BitRate / (frame.ChannelMode == MpegChannelMode.Mono ? 1 : 2)) / 1000; 25 | 26 | if (frame.Version == MpegVersion.Version1) 27 | { 28 | if ((bitRatePerChannel >= 56 && bitRatePerChannel <= 80) || 29 | (frame.SampleRate == 48000 && bitRatePerChannel >= 56)) 30 | { 31 | return _rateLookupTable[0]; // high-rate, 27 subbands 32 | } 33 | else if (frame.SampleRate != 48000 && bitRatePerChannel >= 96) 34 | { 35 | return _rateLookupTable[1]; // high-rate, 30 subbands 36 | } 37 | else if (frame.SampleRate != 32000 && bitRatePerChannel <= 48) 38 | { 39 | return _rateLookupTable[2]; // low-rate, 8 subbands 40 | } 41 | else 42 | { 43 | return _rateLookupTable[3]; // low-rate, 12 subbands 44 | } 45 | } 46 | else 47 | { 48 | return _rateLookupTable[4]; // lsf, 30 subbands 49 | } 50 | } 51 | 52 | // this table tells us which allocation lookup list to use for each subband 53 | // note that each row has the same number of elements as there are subbands for that type... 54 | private static readonly int[][] _rateLookupTable = { 55 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 56 | new int[] { 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 }, // high-rate, 27 subbands 57 | new int[] { 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 }, // high-rate, 30 subbands 58 | new int[] { 4, 4, 5, 5, 5, 5, 5, 5 }, // low-rate, 7 subbands 59 | new int[] { 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 }, // low-rate, 12 subbands 60 | new int[] { 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 }, // lsf, 30 subbands 61 | }; 62 | 63 | // this tells the decode logic: a) how many bits per allocation, and b) how many bits per sample for the give allocation value 64 | // if negative, read -x bits and handle as a group 65 | private static readonly int[][] _allocLookupTable = { 66 | // bits 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 67 | new int[] { 2, 0, -5, -7, 16 }, // 0 (II) 68 | new int[] { 3, 0, -5, -7, 3,-10, 4, 5, 16 }, // 1 (II) 69 | new int[] { 4, 0, -5, -7, 3,-10, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 16 }, // 2 (II) 70 | new int[] { 4, 0, -5, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, // 3 (II) 71 | new int[] { 4, 0, -5, -7,-10, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, // 4 (II, 4, 4 bits per alloc) 72 | new int[] { 3, 0, -5, -7,-10, 4, 5, 6, 9 }, // 5 (II, 4, 3 bits per alloc) 73 | new int[] { 4, 0, -5, -7, 3,-10, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 }, // 6 (II) 74 | new int[] { 2, 0, -5, -7, 3 }, // 7 (II, 4, 2 bits per alloc) 75 | }; 76 | 77 | protected override int[] GetRateTable(MpegFrame frame) 78 | { 79 | return SelectTable(frame); 80 | } 81 | 82 | protected override void ReadScaleFactorSelection(MpegFrame frame, int[][] scfsi, int channels) 83 | { 84 | // we'll never have more than 30 active subbands 85 | for (int sb = 0; sb < 30; sb++) 86 | { 87 | for (int ch = 0; ch < channels; ch++) 88 | { 89 | if (scfsi[ch][sb] == 2) 90 | { 91 | scfsi[ch][sb] = frame.ReadBits(2); 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /SngTool/NLayer/Decoder/RiffHeaderFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | 4 | namespace NLayer.Decoder 5 | { 6 | /// 7 | /// RIFF header reader 8 | /// 9 | internal sealed class RiffHeaderFrame : FrameBase 10 | { 11 | private RiffHeaderFrame() 12 | { 13 | } 14 | 15 | protected override int ValidateFrameHeader() 16 | { 17 | Span buf = stackalloc byte[4]; 18 | 19 | // we expect this to be the "WAVE" chunk 20 | if (Read(8, buf) != 4) 21 | return -1; 22 | if (buf[0] != 'W' || buf[1] != 'A' || buf[2] != 'V' || buf[3] != 'E') 23 | return -1; 24 | 25 | // now the "fmt " chunk 26 | if (Read(12, buf) != 4) 27 | return -1; 28 | if (buf[0] != 'f' || buf[1] != 'm' || buf[2] != 't' || buf[3] != ' ') 29 | return -1; 30 | 31 | // we've found the fmt chunk, so look for the data chunk 32 | int offset = 16; 33 | while (true) 34 | { 35 | // read the length and seek forward 36 | if (Read(offset, buf) != 4) 37 | return -1; 38 | offset += 4 + BinaryPrimitives.ReadInt32LittleEndian(buf); 39 | 40 | // get the chunk ID 41 | if (Read(offset, buf) != 4) 42 | return -1; 43 | offset += 4; 44 | 45 | // if it's not the data chunk, try again 46 | if (buf[0] == 'd' && buf[1] == 'a' && buf[2] == 't' && buf[3] == 'a') 47 | break; 48 | } 49 | 50 | // ... and now we know exactly where the frame ends 51 | return offset + 4; 52 | } 53 | 54 | public static RiffHeaderFrame? TrySync(uint syncMark) 55 | { 56 | if (syncMark == 0x52494646U) 57 | return new RiffHeaderFrame(); 58 | 59 | return null; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SngTool/NLayer/Decoder/VBRInfo.cs: -------------------------------------------------------------------------------- 1 | namespace NLayer.Decoder 2 | { 3 | public struct VBRInfo 4 | { 5 | public int SampleCount; 6 | public int SampleRate; 7 | public int Channels; 8 | public int VBRFrames; 9 | public int VBRBytes; 10 | public int VBRQuality; 11 | public int VBRDelay; 12 | 13 | // we assume the entire stream is consistent wrt samples per frame 14 | public readonly long VBRStreamSampleCount => VBRFrames * SampleCount; 15 | 16 | public readonly int VBRAverageBitrate => 17 | (int)(VBRBytes / (VBRStreamSampleCount / (double)SampleRate) * 8); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SngTool/NLayer/Enums.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace NLayer 3 | { 4 | public enum MpegVersion 5 | { 6 | Unknown = 0, 7 | Version1 = 10, 8 | Version2 = 20, 9 | Version25 = 25, 10 | } 11 | 12 | public enum MpegLayer 13 | { 14 | Unknown = 0, 15 | LayerI = 1, 16 | LayerII = 2, 17 | LayerIII = 3, 18 | } 19 | 20 | public enum MpegChannelMode 21 | { 22 | Stereo, 23 | JointStereo, 24 | DualChannel, 25 | Mono, 26 | } 27 | 28 | public enum StereoMode 29 | { 30 | Both, 31 | LeftOnly, 32 | RightOnly, 33 | DownmixToMono, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SngTool/NLayer/IMpegFrame.cs: -------------------------------------------------------------------------------- 1 | namespace NLayer 2 | { 3 | /// 4 | /// Defines a way of representing an MPEG frame to the decoder. 5 | /// 6 | public interface IMpegFrame 7 | { 8 | /// 9 | /// Gets the sample rate of this frame. 10 | /// 11 | int SampleRate { get; } 12 | 13 | /// 14 | /// Gets the samplerate index (directly from the header). 15 | /// 16 | int SampleRateIndex { get; } 17 | 18 | /// 19 | /// Gets the frame length in bytes. 20 | /// 21 | int FrameLength { get; } 22 | 23 | /// 24 | /// Gets the bit rate. 25 | /// 26 | int BitRate { get; } 27 | 28 | /// 29 | /// Gets the MPEG Version. 30 | /// 31 | MpegVersion Version { get; } 32 | 33 | /// 34 | /// Gets the MPEG Layer. 35 | /// 36 | MpegLayer Layer { get; } 37 | 38 | /// 39 | /// Gets the channel mode. 40 | /// 41 | MpegChannelMode ChannelMode { get; } 42 | 43 | /// 44 | /// Gets the number of samples in this frame. 45 | /// 46 | int ChannelModeExtension { get; } 47 | 48 | /// 49 | /// Gets the channel extension bits. 50 | /// 51 | int SampleCount { get; } 52 | 53 | /// 54 | /// Gets the bitrate index (directly from the header) 55 | /// 56 | int BitRateIndex { get; } 57 | 58 | /// 59 | /// Gets whether the Copyright bit is set. 60 | /// 61 | bool IsCopyrighted { get; } 62 | 63 | /// 64 | /// Gets whether a CRC is present. 65 | /// 66 | bool HasCrc { get; } 67 | 68 | /// 69 | /// Gets whether the CRC check failed (use error concealment strategy). 70 | /// 71 | bool IsCorrupted { get; } 72 | 73 | /// 74 | /// Provides sequential access to the bitstream in the frame (after the header and optional CRC). 75 | /// 76 | /// The number of bits to read. 77 | /// -1 if the end of the frame has been encountered, otherwise the bits requested. 78 | int ReadBits(int bitCount); 79 | 80 | /// 81 | /// Resets the frame to read it from the beginning. 82 | /// 83 | void Reset(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /SngTool/NLayer/MpegFrameDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NLayer.Decoder; 3 | 4 | namespace NLayer 5 | { 6 | public class MpegFrameDecoder 7 | { 8 | private Layer1Decoder? _layer1Decoder; 9 | private Layer2Decoder? _layer2Decoder; 10 | private Layer3Decoder? _layer3Decoder; 11 | private float[]? _eqFactors; 12 | 13 | // channel buffers for getting data out of the decoders... 14 | // we do it this way so the stereo interleaving code is in one place: DecodeFrame(...) 15 | // if we ever add support for multi-channel, we'll have to add a pass after the initial 16 | // stereo decode (since multi-channel basically uses the stereo channels as a reference) 17 | private float[] _ch0, _ch1; 18 | 19 | public MpegFrameDecoder() 20 | { 21 | _ch0 = new float[1152]; 22 | _ch1 = new float[1152]; 23 | } 24 | 25 | /// 26 | /// Set the equalizer. 27 | /// 28 | /// The equalizer, represented by an array of 32 adjustments in dB. 29 | public void SetEQ(float[]? eq) 30 | { 31 | if (eq != null) 32 | { 33 | var factors = new float[32]; 34 | for (int i = 0; i < eq.Length; i++) 35 | // convert from dB -> scaling 36 | factors[i] = (float)Math.Pow(2, eq[i] / 6); 37 | 38 | _eqFactors = factors; 39 | } 40 | else 41 | { 42 | _eqFactors = null; 43 | } 44 | } 45 | 46 | /// 47 | /// Stereo mode used in decoding. 48 | /// 49 | public StereoMode StereoMode { get; set; } 50 | 51 | /// 52 | /// Decode the Mpeg frame into provided buffer. 53 | /// Result varies with different : 54 | /// 55 | /// 56 | /// For , sample data on both two channels will occur in turn (left first). 57 | /// 58 | /// 59 | /// For and , only data on 60 | /// specified channel will occur. 61 | /// 62 | /// 63 | /// For , two channels will be down-mixed into single channel. 64 | /// 65 | /// 66 | /// 67 | /// The Mpeg frame to be decoded. 68 | /// The buffer to fill with PCM samples. 69 | /// The actual amount of samples read. 70 | public int DecodeFrame(MpegFrame frame, Span destination) 71 | { 72 | if (frame == null) 73 | throw new ArgumentNullException(nameof(frame)); 74 | 75 | LayerDecoderBase decoder; 76 | switch (frame.Layer) 77 | { 78 | case MpegLayer.LayerI: 79 | if (_layer1Decoder == null) 80 | _layer1Decoder = new Layer1Decoder(); 81 | decoder = _layer1Decoder; 82 | break; 83 | 84 | case MpegLayer.LayerII: 85 | if (_layer2Decoder == null) 86 | _layer2Decoder = new Layer2Decoder(); 87 | decoder = _layer2Decoder; 88 | break; 89 | 90 | case MpegLayer.LayerIII: 91 | if (_layer3Decoder == null) 92 | _layer3Decoder = new Layer3Decoder(); 93 | decoder = _layer3Decoder; 94 | break; 95 | 96 | default: 97 | return 0; 98 | } 99 | 100 | frame.Reset(); 101 | 102 | decoder.SetEQ(_eqFactors); 103 | decoder.StereoMode = StereoMode; 104 | 105 | int decodedCount = decoder.DecodeFrame(frame, _ch0, _ch1); 106 | 107 | float[] ch0 = _ch0; 108 | float[] ch1 = _ch1; 109 | 110 | if (frame.ChannelMode == MpegChannelMode.Mono || 111 | decoder.StereoMode != StereoMode.Both) 112 | { 113 | ch0.AsSpan(0, decodedCount).CopyTo(destination); 114 | } 115 | else 116 | { 117 | // This is kinda annoying... if we're doing a downmix, 118 | // we should technically only output a single channel 119 | // The problem is, our caller is probably expecting stereo output. Grrrr.... 120 | 121 | // TODO: optimize 122 | for (int i = 0; i < decodedCount; i++) 123 | { 124 | destination[i * 2 + 0] = ch0[i]; 125 | destination[i * 2 + 1] = ch1[i]; 126 | } 127 | decodedCount *= 2; 128 | } 129 | 130 | return decodedCount; 131 | } 132 | 133 | /// 134 | /// Reset the decoder. 135 | /// 136 | public void Reset() 137 | { 138 | // the synthesis filters need to be cleared 139 | 140 | _layer1Decoder?.ResetForSeek(); 141 | _layer2Decoder?.ResetForSeek(); 142 | _layer3Decoder?.ResetForSeek(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /SngTool/NLayer/NLayer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | false 7 | true 8 | Mark Heath, Andrew Ward 9 | 1.15.0 10 | MIT 11 | https://github.com/naudio/NLayer 12 | Fully Managed MPEG 1 & 2 Decoder for Layers 1, 2, & 3 13 | audio csharp mp3 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/HuffmanListNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NVorbis.Contracts 4 | { 5 | internal struct HuffmanListNode : IComparable 6 | { 7 | public int Value; 8 | public int Length; 9 | public int Bits; 10 | public int Mask; 11 | 12 | public int CompareTo(HuffmanListNode y) 13 | { 14 | int len = Length - y.Length; 15 | if (len == 0) 16 | { 17 | return Bits - y.Bits; 18 | } 19 | return len; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/IContainerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NVorbis.Contracts 5 | { 6 | /// 7 | /// Encapsulates a method that initializes a stream reader, optionally ignoring the stream if desired. 8 | /// 9 | /// The instance for the new stream. 10 | /// to process the stream, otherwise . 11 | public delegate bool NewStreamHandler(IPacketProvider packetProvider); 12 | 13 | /// 14 | /// Provides an interface for a Vorbis logical stream container. 15 | /// 16 | public interface IContainerReader : IDisposable 17 | { 18 | /// 19 | /// Gets or sets the callback to invoke when a new stream is encountered in the container. 20 | /// 21 | NewStreamHandler? NewStreamCallback { get; set; } 22 | 23 | /// 24 | /// Returns a read-only list of the logical streams discovered in this container. 25 | /// 26 | IReadOnlyList GetStreams(); 27 | 28 | /// 29 | /// Gets whether the underlying stream can seek. 30 | /// 31 | bool CanSeek { get; } 32 | 33 | /// 34 | /// Gets the number of bits in the container that are strictly for framing of logical streams. 35 | /// 36 | long ContainerBits { get; } 37 | 38 | /// 39 | /// Gets the number of bits in the container that are not associated with a logical stream. 40 | /// 41 | long WasteBits { get; } 42 | 43 | /// 44 | /// Attempts to initialize the container. 45 | /// 46 | /// if successful, otherwise . 47 | bool TryInit(); 48 | 49 | /// 50 | /// Searches for the next logical stream in the container. 51 | /// 52 | /// if a new stream was found, otherwise . 53 | bool FindNextStream(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/IFloor.cs: -------------------------------------------------------------------------------- 1 | namespace NVorbis.Contracts 2 | { 3 | internal interface IFloor 4 | { 5 | FloorData CreateFloorData(); 6 | 7 | void Unpack(ref VorbisPacket packet, FloorData floorData, int blockSize, int channel); 8 | 9 | void Apply(FloorData floorData, int blockSize, float[] residue); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/IPacketProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NVorbis.Ogg; 3 | 4 | namespace NVorbis.Contracts 5 | { 6 | /// 7 | /// Describes an interface for a packet stream reader. 8 | /// 9 | public interface IPacketProvider : IDisposable 10 | { 11 | /// 12 | /// Gets whether the provider supports seeking. 13 | /// 14 | bool CanSeek { get; } 15 | 16 | /// 17 | /// Gets the serial number of this provider's data stream. 18 | /// 19 | int StreamSerial { get; } 20 | 21 | /// 22 | /// Gets the next packet in the stream and advances to the next packet position. 23 | /// 24 | /// The for the next packet if available. 25 | VorbisPacket GetNextPacket(); 26 | 27 | /// 28 | /// Seeks the stream to the packet that is prior to the requested granule position by the specified preroll number of packets. 29 | /// 30 | /// The granule position to seek to. 31 | /// The number of packets to seek backward prior to the granule position. 32 | /// A provider that calculates the number of granules in packets. 33 | /// The granule position at the start of the packet containing the requested position. 34 | long SeekTo(long granulePos, uint preRoll, IPacketGranuleCountProvider packetGranuleCountProvider); 35 | 36 | /// 37 | /// Gets the total number of granule available in the stream. 38 | /// 39 | long GetGranuleCount(IPacketGranuleCountProvider packetGranuleCountProvider); 40 | 41 | /// 42 | /// Gets packet data for the requested location. 43 | /// 44 | /// The packet data location. 45 | /// The packet data slice. 46 | PageSlice GetPacketData(PacketLocation location); 47 | 48 | /// 49 | /// Used to finish a packet. Using a finished packet is undefined behavior. 50 | /// 51 | /// The packet to finish. 52 | void FinishPacket(ref VorbisPacket packet); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/IStreamDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace NVorbis.Contracts 5 | { 6 | /// 7 | /// Describes a stream decoder instance for Vorbis data. 8 | /// 9 | public interface IStreamDecoder : IDisposable 10 | { 11 | /// 12 | /// Gets the number of channels in the stream. 13 | /// 14 | int Channels { get; } 15 | 16 | /// 17 | /// Gets the sample rate of the stream. 18 | /// 19 | int SampleRate { get; } 20 | 21 | /// 22 | /// Gets the upper bitrate limit for the stream, if specified. 23 | /// 24 | int UpperBitrate { get; } 25 | 26 | /// 27 | /// Gets the nominal bitrate of the stream, if specified. 28 | /// May be calculated from and . 29 | /// 30 | int NominalBitrate { get; } 31 | 32 | /// 33 | /// Gets the lower bitrate limit for the stream, if specified. 34 | /// 35 | int LowerBitrate { get; } 36 | 37 | /// 38 | /// Gets the tag data from the stream's header. 39 | /// 40 | ITagData Tags { get; } 41 | 42 | /// 43 | /// Gets the total duration of the decoded stream. 44 | /// 45 | TimeSpan TotalTime { get; } 46 | 47 | /// 48 | /// Gets the total number of samples in the decoded stream. 49 | /// 50 | long TotalSamples { get; } 51 | 52 | /// 53 | /// Gets or sets the current time position of the stream. 54 | /// 55 | TimeSpan TimePosition { get; set; } 56 | 57 | /// 58 | /// Gets or sets the current sample position of the stream. 59 | /// 60 | long SamplePosition { get; set; } 61 | 62 | /// 63 | /// Gets or sets whether to clip samples between negative one and one. 64 | /// 65 | bool ClipSamples { get; set; } 66 | 67 | /// 68 | /// Gets or sets whether the decoder should skip parsing tags. 69 | /// 70 | bool SkipTags { get; set; } 71 | 72 | /// 73 | /// Gets whether any samples have been clipped between negative one and one. 74 | /// 75 | /// 76 | /// Depends on being . 77 | /// 78 | bool HasClipped { get; } 79 | 80 | /// 81 | /// Gets whether the decoder has reached the end of the stream. 82 | /// 83 | bool IsEndOfStream { get; } 84 | 85 | /// 86 | /// Gets the instance for this stream. 87 | /// 88 | IStreamStats Stats { get; } 89 | 90 | /// 91 | /// Begin parsing the stream. 92 | /// 93 | /// The stream header could not be parsed. 94 | void Initialize(); 95 | 96 | /// 97 | /// Seeks the stream by the specified duration. 98 | /// 99 | /// The relative time to seek to. 100 | /// The reference point used to obtain the new position. 101 | void SeekTo(TimeSpan timePosition, SeekOrigin seekOrigin = SeekOrigin.Begin); 102 | 103 | /// 104 | /// Seeks the stream by the specified sample count. 105 | /// 106 | /// The relative sample position to seek to. 107 | /// The reference point used to obtain the new position. 108 | void SeekTo(long samplePosition, SeekOrigin seekOrigin = SeekOrigin.Begin); 109 | 110 | /// 111 | /// Reads interleaved samples into the specified buffer. 112 | /// 113 | /// 114 | /// The buffer to read the samples into. Length must be a multiple of . 115 | /// 116 | /// 117 | /// The amount of samples read. 118 | /// 119 | /// 120 | /// The buffer is too small or the length is not a multiple of . 121 | /// 122 | /// 123 | /// The is interleaved by channel 124 | /// (Left, Right, Left, Right, Left, Right). 125 | /// 126 | int Read(Span buffer); 127 | 128 | /// 129 | /// Reads non-interleaved samples into the specified buffer. 130 | /// 131 | /// 132 | /// The buffer to read the samples into, 133 | /// of which length must be a multiple of . 134 | /// 135 | /// 136 | /// The amount of samples to read per channel. 137 | /// 138 | /// 139 | /// The offset in values between each channel in the buffer. 140 | /// 141 | /// 142 | /// The amount of samples read. 143 | /// 144 | /// 145 | /// The buffer is too small or the length is not a multiple of . 146 | /// 147 | /// 148 | /// The is not interleaved 149 | /// (Left, Left, Left, Right, Right, Right). 150 | /// 151 | int Read(Span buffer, int samplesToRead, int channelStride); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/IStreamStats.cs: -------------------------------------------------------------------------------- 1 | namespace NVorbis.Contracts 2 | { 3 | /// 4 | /// Describes an interface for reading statistics about the current stream. 5 | /// 6 | public interface IStreamStats 7 | { 8 | /// 9 | /// Resets the counters for bit rate and bits. 10 | /// 11 | void ResetStats(); 12 | 13 | /// 14 | /// Gets the calculated bit rate of audio stream data for the everything decoded so far. 15 | /// 16 | int EffectiveBitRate { get; } 17 | 18 | /// 19 | /// Gets the calculated bit rate per second of audio for the last two packets. 20 | /// 21 | int InstantBitRate { get; } 22 | 23 | /// 24 | /// Gets the number of framing bits used by the container. 25 | /// 26 | long ContainerBits { get; } 27 | 28 | /// 29 | /// Gets the number of bits read that do not contribute to the output audio. Does not include framing bits from the container. 30 | /// 31 | long OverheadBits { get; } 32 | 33 | /// 34 | /// Gets the number of bits read that contribute to the output audio. 35 | /// 36 | long AudioBits { get; } 37 | 38 | /// 39 | /// Gets the number of bits skipped. 40 | /// 41 | long WasteBits { get; } 42 | 43 | /// 44 | /// Gets the number of packets read. 45 | /// 46 | int PacketCount { get; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/ITagData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NVorbis.Contracts 4 | { 5 | /// 6 | /// Describes an interface to tag data. 7 | /// 8 | public interface ITagData 9 | { 10 | /// 11 | /// Gets the full list of tags encountered in the stream. 12 | /// 13 | IReadOnlyDictionary> All { get; } 14 | 15 | /// 16 | /// The vendor string from the stream header. 17 | /// 18 | string EncoderVendor { get; } 19 | 20 | #region Standard Tags 21 | 22 | /// 23 | /// Track/Work name. 24 | /// 25 | string Title { get; } // TITLE 26 | 27 | /// 28 | /// Track version name. 29 | /// 30 | string Version { get; } // VERSION 31 | 32 | /// 33 | /// The collection name to which this track belongs. 34 | /// 35 | string Album { get; } // ALBUM 36 | 37 | /// 38 | /// The track number of this piece if part of a specific larger collection or album. 39 | /// 40 | string TrackNumber { get; } // TRACKNUMBER 41 | 42 | /// 43 | /// The artist generally considered responsible for the work. 44 | /// 45 | string Artist { get; } // ARTIST 46 | 47 | /// 48 | /// The artist(s) who performed the work. 49 | /// 50 | IReadOnlyList Performers { get; } // PERFORMER; can be "PERFORMER[instrument]" 51 | 52 | /// 53 | /// Copyright attribution. 54 | /// 55 | string Copyright { get; } // COPYRIGHT 56 | 57 | /// 58 | /// License information. 59 | /// 60 | string License { get; } // LICENSE 61 | 62 | /// 63 | /// The organization producing the track. 64 | /// 65 | string Organization { get; } // ORGANIZATION 66 | 67 | /// 68 | /// A short text description of the contents. 69 | /// 70 | string Description { get; } // DESCRIPTION 71 | 72 | /// 73 | /// A short text indication of the music genre. 74 | /// 75 | IReadOnlyList Genres { get; } // GENRE 76 | 77 | /// 78 | /// Date the track was recorded. May have other dates with descriptions. 79 | /// 80 | IReadOnlyList Dates { get; } // DATE; value is ISO 8601 date with free text following 81 | 82 | /// 83 | /// Location where the track was recorded. 84 | /// 85 | IReadOnlyList Locations { get; } // LOCATION 86 | 87 | /// 88 | /// Contact information for the creators or distributors of the track. 89 | /// 90 | string Contact { get; } // CONTACT 91 | 92 | /// 93 | /// The International Standard Recording Code number for the track. 94 | /// 95 | string Isrc { get; } // ISRC 96 | 97 | #endregion 98 | 99 | #region Extended Tags 100 | /* 101 | -- cover art and related... 102 | COVERART 103 | COVERARTMIME 104 | METADATA_BLOCK_PICTURE 105 | 106 | -- exiftool "common" tags 107 | COMPOSER 108 | DIRECTOR 109 | ACTOR 110 | ENCODED_BY 111 | ENCODED_USING 112 | ENCODER 113 | ENCODER_OPTIONS 114 | REPLAYGAIN_ALBUM_GAIN 115 | REPLAYGAIN_ALBUM_PEAK 116 | REPLAYGAIN_TRACK_GAIN 117 | REPLAYGAIN_TRACK_PEAK 118 | 119 | -- David Shea tags 120 | SOURCE ARTIST 121 | CONDUCTOR 122 | ENSEMBLE 123 | REMIXER 124 | PRODUCER 125 | ENGINEER 126 | GUEST ARTIST 127 | PUBLISHER 128 | PRODUCTNUMBER 129 | CATALOGNUMBER 130 | VOLUME 131 | RELEASE DATE 132 | SOURCE MEDIUM 133 | 134 | -- reactor-core.org tags 135 | ARRANGER 136 | AUTHOR 137 | COMMENT 138 | DISCNUMBER 139 | EAN/UPN 140 | ENCODED-BY 141 | ENCODING 142 | LABEL 143 | LABELNO 144 | LYRICIST 145 | OPUS 146 | PART 147 | PARTNUMBER 148 | SOURCE WORK 149 | SOURCEMEDIA 150 | SPARS 151 | */ 152 | #endregion 153 | 154 | /// 155 | /// Retrieves the value of a tag as a single value. 156 | /// 157 | /// The tag name to retrieve. 158 | /// to concatenate multiple instances into multiple lines. to return just the last instance. 159 | /// The requested tag value, if available. Otherwise . 160 | string GetTagSingle(string key, bool concatenate = false); 161 | 162 | /// 163 | /// Retrieves the values of a tag. 164 | /// 165 | /// The tag name to retrieve. 166 | /// An containing the values in the order read from the stream. 167 | IReadOnlyList GetTagMulti(string key); 168 | } 169 | } -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/IVorbisReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace NVorbis.Contracts 6 | { 7 | /// 8 | /// Raised when a new stream has been encountered in the container. 9 | /// 10 | public delegate void NewStreamEventHandler(IVorbisReader reader, ref NewStreamEventArgs eventArgs); 11 | 12 | /// 13 | /// Describes the interface for . 14 | /// 15 | public interface IVorbisReader : IDisposable 16 | { 17 | /// 18 | /// Raised when a new stream has been encountered in the container. 19 | /// 20 | event NewStreamEventHandler NewStream; 21 | 22 | /// 23 | /// Gets the number of bits read that are related to framing and transport alone. 24 | /// 25 | long ContainerOverheadBits { get; } 26 | 27 | /// 28 | /// Gets the number of bits skipped in the container due to framing, ignored streams, or sync loss. 29 | /// 30 | long ContainerWasteBits { get; } 31 | 32 | /// 33 | /// Gets the list of instances associated with the loaded container. 34 | /// 35 | IReadOnlyList Streams { get; } 36 | 37 | /// 38 | /// Gets the currently-selected stream's index. 39 | /// 40 | int StreamIndex { get; } 41 | 42 | /// 43 | /// Gets the number of channels in the stream. 44 | /// 45 | int Channels { get; } 46 | 47 | /// 48 | /// Gets the sample rate of the stream. 49 | /// 50 | int SampleRate { get; } 51 | 52 | /// 53 | /// Gets the upper bitrate limit for the stream, if specified. 54 | /// 55 | int UpperBitrate { get; } 56 | 57 | /// 58 | /// Gets the nominal bitrate of the stream, if specified. 59 | /// May be calculated from and . 60 | /// 61 | int NominalBitrate { get; } 62 | 63 | /// 64 | /// Gets the lower bitrate limit for the stream, if specified. 65 | /// 66 | int LowerBitrate { get; } 67 | 68 | /// 69 | /// Gets the total duration of the decoded stream. 70 | /// 71 | TimeSpan TotalTime { get; } 72 | 73 | /// 74 | /// Gets the total number of samples in the decoded stream. 75 | /// 76 | long TotalSamples { get; } 77 | 78 | /// 79 | bool ClipSamples { get; set; } 80 | 81 | /// 82 | /// Gets or sets the current time position of the stream. 83 | /// 84 | TimeSpan TimePosition { get; set; } 85 | 86 | /// 87 | /// Gets or sets the current sample position of the stream. 88 | /// 89 | long SamplePosition { get; set; } 90 | 91 | /// 92 | bool HasClipped { get; } 93 | 94 | /// 95 | /// Gets whether the current stream has ended. 96 | /// 97 | bool IsEndOfStream { get; } 98 | 99 | /// 100 | /// Gets the instance for this stream. 101 | /// 102 | IStreamStats StreamStats { get; } 103 | 104 | /// 105 | /// Gets the tag data from the stream's header. 106 | /// 107 | ITagData Tags { get; } 108 | 109 | /// 110 | /// Begin parsing the container in the stream. 111 | /// 112 | /// The Vorbis container could not be parsed. 113 | void Initialize(); 114 | 115 | /// 116 | /// Searches for the next stream in a concatenated container. 117 | /// Will raise for the found stream, 118 | /// and will add it to if not marked as ignored. 119 | /// 120 | /// if a new stream was found, otherwise . 121 | bool FindNextStream(); 122 | 123 | /// 124 | /// Switches to an alternate logical stream. 125 | /// 126 | /// The logical stream index to switch to 127 | /// 128 | /// if the properties of the logical stream differ from 129 | /// those of the one previously being decoded. Otherwise, . 130 | /// 131 | bool SwitchStreams(int index); 132 | 133 | /// 134 | int ReadSamples(Span buffer); 135 | 136 | /// 137 | int ReadSamples(Span buffer, int samplesToRead, int channelStride); 138 | 139 | /// 140 | void SeekTo(TimeSpan timePosition, SeekOrigin seekOrigin = SeekOrigin.Begin); 141 | 142 | /// 143 | void SeekTo(long samplePosition, SeekOrigin seekOrigin = SeekOrigin.Begin); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/Ogg/IForwardOnlyPacketProvider.cs: -------------------------------------------------------------------------------- 1 | using NVorbis.Ogg; 2 | 3 | namespace NVorbis.Contracts.Ogg 4 | { 5 | internal interface IForwardOnlyPacketProvider : IPacketProvider 6 | { 7 | bool AddPage(PageData pageData); 8 | 9 | void SetEndOfStream(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/Ogg/IPageReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using NVorbis.Ogg; 4 | 5 | namespace NVorbis.Contracts.Ogg 6 | { 7 | internal interface IPageReader : IDisposable 8 | { 9 | void Lock(); 10 | bool Release(); 11 | 12 | bool CanSeek { get; } 13 | long ContainerBits { get; } 14 | long WasteBits { get; } 15 | 16 | bool ReadNextPage([MaybeNullWhen(false)] out PageData pageData); 17 | 18 | bool ReadPageAt(long offset, [MaybeNullWhen(false)] out PageData pageData); 19 | 20 | bool ReadPageHeaderAt(long offset, Span headerBuffer); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/Ogg/IStreamPageReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NVorbis.Ogg; 3 | 4 | namespace NVorbis.Contracts.Ogg 5 | { 6 | internal interface IStreamPageReader : IDisposable 7 | { 8 | IPacketProvider PacketProvider { get; } 9 | 10 | void AddPage(PageData page, long pageOffset); 11 | 12 | PageData GetPage(long pageIndex); 13 | 14 | long FindPage(long granulePos); 15 | 16 | bool GetPage( 17 | long pageIndex, 18 | out long granulePos, 19 | out bool isResync, 20 | out bool isContinuation, 21 | out bool isContinued, 22 | out ushort packetCount, 23 | out int pageOverhead); 24 | 25 | void SetEndOfStream(); 26 | 27 | long PageCount { get; } 28 | 29 | bool HasAllPages { get; } 30 | 31 | long? MaxGranulePosition { get; } 32 | 33 | long FirstDataPageIndex { get; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Contracts/Ogg/PageFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NVorbis.Contracts.Ogg 4 | { 5 | [Flags] 6 | internal enum PageFlags 7 | { 8 | None = 0, 9 | ContinuesPacket = 1, 10 | BeginningOfStream = 2, 11 | EndOfStream = 4, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SngTool/NVorbis/EndOfStreamFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NVorbis 4 | { 5 | [Flags] 6 | internal enum EndOfStreamFlags : byte 7 | { 8 | None, 9 | InvalidPacket = 1 << 0, 10 | PacketFlag = 1 << 2, 11 | InvalidPreroll = 1 << 3, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Floor0.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using NVorbis.Contracts; 5 | 6 | namespace NVorbis 7 | { 8 | // Packed LSP values on dB amplittude and Bark frequency scale. 9 | // Virtually unused (libvorbis did not use past beta 4). Probably untested. 10 | internal sealed class Floor0 : IFloor 11 | { 12 | private sealed class Data : FloorData 13 | { 14 | internal readonly float[] Coeff; 15 | internal float Amp; 16 | 17 | public Data(float[] coeff) 18 | { 19 | Coeff = coeff; 20 | } 21 | 22 | public override bool ExecuteChannel => (ForceEnergy || Amp > 0f) && !ForceNoEnergy; 23 | 24 | public override void Reset() 25 | { 26 | Array.Clear(Coeff); 27 | Amp = 0; 28 | ForceEnergy = false; 29 | ForceNoEnergy = false; 30 | } 31 | } 32 | 33 | private int _order, _rate, _bark_map_size, _ampBits, _ampOfs, _ampDiv; 34 | private Codebook[] _books; 35 | private int _bookBits; 36 | private Dictionary _wMap; 37 | private Dictionary _barkMaps; 38 | 39 | public Floor0(ref VorbisPacket packet, int block0Size, int block1Size, Codebook[] codebooks) 40 | { 41 | // this is pretty well stolen directly from libvorbis... BSD license 42 | _order = (int)packet.ReadBits(8); 43 | _rate = (int)packet.ReadBits(16); 44 | _bark_map_size = (int)packet.ReadBits(16); 45 | _ampBits = (int)packet.ReadBits(6); 46 | _ampOfs = (int)packet.ReadBits(8); 47 | _books = new Codebook[(int)packet.ReadBits(4) + 1]; 48 | 49 | if (_order < 1 || _rate < 1 || _bark_map_size < 1 || _books.Length == 0) throw new InvalidDataException(); 50 | 51 | _ampDiv = (1 << _ampBits) - 1; 52 | 53 | for (int i = 0; i < _books.Length; i++) 54 | { 55 | int num = (int)packet.ReadBits(8); 56 | if (num < 0 || num >= codebooks.Length) throw new InvalidDataException(); 57 | Codebook book = codebooks[num]; 58 | 59 | if (book.MapType == 0 || book.Dimensions < 1) throw new InvalidDataException(); 60 | 61 | _books[i] = book; 62 | } 63 | _bookBits = Utils.ilog(_books.Length); 64 | 65 | _barkMaps = new Dictionary 66 | { 67 | [block0Size] = SynthesizeBarkCurve(block0Size / 2), 68 | [block1Size] = SynthesizeBarkCurve(block1Size / 2) 69 | }; 70 | 71 | _wMap = new Dictionary 72 | { 73 | [block0Size] = SynthesizeWDelMap(block0Size / 2), 74 | [block1Size] = SynthesizeWDelMap(block1Size / 2) 75 | }; 76 | } 77 | 78 | public FloorData CreateFloorData() 79 | { 80 | return new Data(new float[_order + 1]); 81 | } 82 | 83 | private int[] SynthesizeBarkCurve(int n) 84 | { 85 | float scale = _bark_map_size / ToBARK(_rate / 2); 86 | 87 | int[] map = new int[n + 1]; 88 | 89 | for (int i = 0; i < map.Length - 2; i++) 90 | { 91 | map[i] = Math.Min(_bark_map_size - 1, (int)Math.Floor(ToBARK((_rate / 2f) / n * i) * scale)); 92 | } 93 | map[n] = -1; 94 | return map; 95 | } 96 | 97 | private static float ToBARK(double lsp) 98 | { 99 | return (float)(13.1 * Math.Atan(0.00074 * lsp) + 2.24 * Math.Atan(0.0000000185 * lsp * lsp) + .0001 * lsp); 100 | } 101 | 102 | private float[] SynthesizeWDelMap(int n) 103 | { 104 | float wdel = (float)(Math.PI / _bark_map_size); 105 | 106 | float[] map = new float[n]; 107 | for (int i = 0; i < map.Length; i++) 108 | { 109 | map[i] = 2f * MathF.Cos(wdel * i); 110 | } 111 | return map; 112 | } 113 | 114 | public void Unpack(ref VorbisPacket packet, FloorData floorData, int blockSize, int channel) 115 | { 116 | Data data = (Data)floorData; 117 | 118 | data.Amp = packet.ReadBits(_ampBits); 119 | if (data.Amp > 0f) 120 | { 121 | // this is pretty well stolen directly from libvorbis... BSD license 122 | Array.Clear(data.Coeff, 0, data.Coeff.Length); 123 | 124 | data.Amp = data.Amp / _ampDiv * _ampOfs; 125 | 126 | uint bookNum = (uint)packet.ReadBits(_bookBits); 127 | if (bookNum >= (uint)_books.Length) 128 | { 129 | // we ran out of data or the packet is corrupt... 0 the floor and return 130 | data.Amp = 0; 131 | return; 132 | } 133 | Codebook book = _books[bookNum]; 134 | 135 | // first, the book decode... 136 | for (int i = 0; i < _order;) 137 | { 138 | int entry = book.DecodeScalar(ref packet); 139 | if (entry == -1) 140 | { 141 | // we ran out of data or the packet is corrupt... 0 the floor and return 142 | data.Amp = 0; 143 | return; 144 | } 145 | 146 | ReadOnlySpan lookup = book.GetLookup(entry); 147 | for (int j = 0; i < _order && j < lookup.Length; j++, i++) 148 | { 149 | data.Coeff[i] = lookup[j]; 150 | } 151 | } 152 | 153 | // then, the "averaging" 154 | float last = 0f; 155 | for (int j = 0; j < _order;) 156 | { 157 | for (int k = 0; j < _order && k < book.Dimensions; j++, k++) 158 | { 159 | data.Coeff[j] += last; 160 | } 161 | last = data.Coeff[j - 1]; 162 | } 163 | } 164 | } 165 | 166 | public void Apply(FloorData floorData, int blockSize, float[] residue) 167 | { 168 | Data data = (Data)floorData; 169 | int n = blockSize / 2; 170 | 171 | if (data.Amp > 0f) 172 | { 173 | // this is pretty well stolen directly from libvorbis... BSD license 174 | int[] barkMap = _barkMaps[blockSize]; 175 | float[] wMap = _wMap[blockSize]; 176 | 177 | Span coeff = data.Coeff.AsSpan(0, _order); 178 | for (int j = 0; j < coeff.Length; j++) 179 | { 180 | coeff[j] = 2f * MathF.Cos(coeff[j]); 181 | } 182 | 183 | int i = 0; 184 | while (i < n) 185 | { 186 | int j; 187 | int k = barkMap[i]; 188 | float p = .5f; 189 | float q = .5f; 190 | float w = wMap[k]; 191 | for (j = 1; j < _order; j += 2) 192 | { 193 | q *= w - data.Coeff[j - 1]; 194 | p *= w - data.Coeff[j]; 195 | } 196 | if (j == _order) 197 | { 198 | // odd order filter; slightly assymetric 199 | q *= w - data.Coeff[j - 1]; 200 | p *= p * (4f - w * w); 201 | q *= q; 202 | } 203 | else 204 | { 205 | // even order filter; still symetric 206 | p *= p * (2f - w); 207 | q *= q * (2f + w); 208 | } 209 | 210 | // calc the dB of this bark section 211 | q = data.Amp / MathF.Sqrt(p + q) - _ampOfs; 212 | 213 | // now convert to a linear sample multiplier 214 | q = MathF.Exp(q * 0.11512925f); 215 | 216 | residue[i] *= q; 217 | 218 | while (barkMap[++i] == k) residue[i] *= q; 219 | } 220 | } 221 | else 222 | { 223 | Array.Clear(residue, 0, n); 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /SngTool/NVorbis/FloorData.cs: -------------------------------------------------------------------------------- 1 | namespace NVorbis 2 | { 3 | internal abstract class FloorData 4 | { 5 | public abstract bool ExecuteChannel { get; } 6 | public bool ForceEnergy { get; set; } 7 | public bool ForceNoEnergy { get; set; } 8 | 9 | public abstract void Reset(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Huffman.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NVorbis.Contracts; 3 | 4 | namespace NVorbis 5 | { 6 | internal struct Huffman 7 | { 8 | public static Huffman Empty { get; } = new Huffman() 9 | { 10 | TableBits = 0, 11 | PrefixTree = Array.Empty(), 12 | OverflowList = Array.Empty(), 13 | }; 14 | 15 | private const int MAX_TABLE_BITS = 10; 16 | 17 | public int TableBits { get; private set; } 18 | public HuffmanListNode[] PrefixTree { get; private set; } 19 | public HuffmanListNode[] OverflowList { get; private set; } 20 | 21 | public static Huffman GenerateTable(int[]? values, int[] lengthList, int[] codeList) 22 | { 23 | HuffmanListNode[] list = new HuffmanListNode[lengthList.Length]; 24 | 25 | int maxLen = 0; 26 | for (int i = 0; i < list.Length; i++) 27 | { 28 | list[i] = new HuffmanListNode 29 | { 30 | Value = values != null ? values[i] : i, 31 | Length = lengthList[i] <= 0 ? 99999 : lengthList[i], 32 | Bits = codeList[i], 33 | Mask = (1 << lengthList[i]) - 1, 34 | }; 35 | if (lengthList[i] > 0 && maxLen < lengthList[i]) 36 | { 37 | maxLen = lengthList[i]; 38 | } 39 | } 40 | 41 | Array.Sort(list, 0, list.Length); 42 | 43 | int tableBits = maxLen > MAX_TABLE_BITS ? MAX_TABLE_BITS : maxLen; 44 | 45 | HuffmanListNode[] prefixList = new HuffmanListNode[1 << tableBits]; 46 | 47 | HuffmanListNode[] overflowList = Array.Empty(); 48 | int overflowIndex = 0; 49 | 50 | for (int i = 0; i < list.Length && list[i].Length < 99999; i++) 51 | { 52 | int itemBits = list[i].Length; 53 | if (itemBits > tableBits) 54 | { 55 | int maxOverflowLength = list.Length - i; 56 | if (overflowList.Length < maxOverflowLength) 57 | overflowList = new HuffmanListNode[maxOverflowLength]; 58 | 59 | overflowIndex = 0; 60 | 61 | for (; i < list.Length && list[i].Length < 99999; i++) 62 | { 63 | overflowList[overflowIndex++] = list[i]; 64 | } 65 | } 66 | else 67 | { 68 | int maxVal = 1 << (tableBits - itemBits); 69 | HuffmanListNode item = list[i]; 70 | for (int j = 0; j < maxVal; j++) 71 | { 72 | int idx = (j << itemBits) | item.Bits; 73 | prefixList[idx] = item; 74 | } 75 | } 76 | } 77 | 78 | if (overflowIndex < overflowList.Length) 79 | { 80 | Array.Resize(ref overflowList, overflowIndex); 81 | } 82 | 83 | return new Huffman 84 | { 85 | TableBits = tableBits, 86 | PrefixTree = prefixList, 87 | OverflowList = overflowList, 88 | }; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Mode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace NVorbis 7 | { 8 | internal struct Mode 9 | { 10 | private struct OverlapInfo 11 | { 12 | public int PacketStartIndex; 13 | public int PacketTotalLength; 14 | public int PacketValidLength; 15 | } 16 | 17 | private bool _blockFlag; 18 | private int _blockSize; 19 | private Mapping _mapping; 20 | private float[][] _windows; 21 | private OverlapInfo[] _overlapInfo; 22 | 23 | public Mode(ref VorbisPacket packet, int block0Size, int block1Size, Mapping[] mappings) 24 | { 25 | _blockFlag = packet.ReadBit(); 26 | if (0 != packet.ReadBits(32)) 27 | { 28 | throw new System.IO.InvalidDataException("Mode header had invalid window or transform type!"); 29 | } 30 | 31 | int mappingIdx = (int)packet.ReadBits(8); 32 | if (mappingIdx >= mappings.Length) 33 | { 34 | throw new System.IO.InvalidDataException("Mode header had invalid mapping index!"); 35 | } 36 | _mapping = mappings[mappingIdx]; 37 | 38 | if (_blockFlag) 39 | { 40 | _blockSize = block1Size; 41 | _windows = new float[][] 42 | { 43 | CalcWindow(block0Size, block1Size, block0Size), 44 | CalcWindow(block1Size, block1Size, block0Size), 45 | CalcWindow(block0Size, block1Size, block1Size), 46 | CalcWindow(block1Size, block1Size, block1Size), 47 | }; 48 | _overlapInfo = new OverlapInfo[] 49 | { 50 | CalcOverlap(block0Size, block1Size, block0Size), 51 | CalcOverlap(block1Size, block1Size, block0Size), 52 | CalcOverlap(block0Size, block1Size, block1Size), 53 | CalcOverlap(block1Size, block1Size, block1Size), 54 | }; 55 | } 56 | else 57 | { 58 | _blockSize = block0Size; 59 | _windows = new float[][] 60 | { 61 | CalcWindow(block0Size, block0Size, block0Size), 62 | }; 63 | _overlapInfo = Array.Empty(); 64 | } 65 | } 66 | 67 | private static float[] CalcWindow(int prevBlockSize, int blockSize, int nextBlockSize) 68 | { 69 | float[] array = new float[blockSize]; 70 | Span span = array; 71 | 72 | int left = prevBlockSize / 2; 73 | int wnd = blockSize; 74 | int right = nextBlockSize / 2; 75 | 76 | int leftbegin = wnd / 4 - left / 2; 77 | int rightbegin = wnd - wnd / 4 - right / 2; 78 | 79 | Span leftSpan = span.Slice(leftbegin, left); 80 | for (int i = 0; i < leftSpan.Length; i++) 81 | { 82 | double x = Math.Sin((i + .5) / left * Math.PI / 2); 83 | x *= x; 84 | leftSpan[i] = (float)Math.Sin(x * Math.PI / 2); 85 | } 86 | 87 | span[(leftbegin + left)..rightbegin].Fill(1.0f); 88 | 89 | Span rightSpan = span.Slice(rightbegin, right); 90 | for (int i = 0; i < rightSpan.Length; i++) 91 | { 92 | double x = Math.Sin((right - i - .5) / right * Math.PI / 2); 93 | x *= x; 94 | rightSpan[i] = (float)Math.Sin(x * Math.PI / 2); 95 | } 96 | 97 | return array; 98 | } 99 | 100 | private static OverlapInfo CalcOverlap(int prevBlockSize, int blockSize, int nextBlockSize) 101 | { 102 | int leftOverlapHalfSize = prevBlockSize / 4; 103 | int rightOverlapHalfSize = nextBlockSize / 4; 104 | 105 | int packetStartIndex = blockSize / 4 - leftOverlapHalfSize; 106 | int packetTotalLength = blockSize / 4 * 3 + rightOverlapHalfSize; 107 | int packetValidLength = packetTotalLength - rightOverlapHalfSize * 2; 108 | 109 | return new OverlapInfo 110 | { 111 | PacketStartIndex = packetStartIndex, 112 | PacketValidLength = packetValidLength, 113 | PacketTotalLength = packetTotalLength, 114 | }; 115 | } 116 | 117 | private bool GetPacketInfo(ref VorbisPacket packet, out int windowIndex, out int packetStartIndex, out int packetValidLength, out int packetTotalLength) 118 | { 119 | if (packet.IsShort) 120 | { 121 | windowIndex = 0; 122 | packetStartIndex = 0; 123 | packetValidLength = 0; 124 | packetTotalLength = 0; 125 | return false; 126 | } 127 | 128 | if (_blockFlag) 129 | { 130 | bool prevFlag = packet.ReadBit(); 131 | bool nextFlag = packet.ReadBit(); 132 | 133 | windowIndex = (prevFlag ? 1 : 0) + (nextFlag ? 2 : 0); 134 | 135 | ref OverlapInfo overlapInfo = ref _overlapInfo[windowIndex]; 136 | packetStartIndex = overlapInfo.PacketStartIndex; 137 | packetValidLength = overlapInfo.PacketValidLength; 138 | packetTotalLength = overlapInfo.PacketTotalLength; 139 | } 140 | else 141 | { 142 | windowIndex = 0; 143 | packetStartIndex = 0; 144 | packetValidLength = _blockSize / 2; 145 | packetTotalLength = _blockSize; 146 | } 147 | 148 | return true; 149 | } 150 | 151 | public unsafe bool Decode( 152 | ref VorbisPacket packet, 153 | ReadOnlySpan buffers, 154 | out int packetStartIndex, 155 | out int packetValidLength, 156 | out int packetTotalLength) 157 | { 158 | if (GetPacketInfo( 159 | ref packet, 160 | out int windowIndex, 161 | out packetStartIndex, 162 | out packetValidLength, 163 | out packetTotalLength)) 164 | { 165 | _mapping.DecodePacket(ref packet, _blockSize, buffers); 166 | 167 | int length = _blockSize; 168 | Span windowSpan = _windows[windowIndex].AsSpan(0, length); 169 | 170 | for (int ch = 0; ch < buffers.Length; ch++) 171 | { 172 | Span bufferSpan = buffers[ch].AsSpan(0, length); 173 | 174 | ref float buffer = ref MemoryMarshal.GetReference(bufferSpan); 175 | ref float window = ref MemoryMarshal.GetReference(windowSpan); 176 | int i = 0; 177 | 178 | if (Vector.IsHardwareAccelerated) 179 | { 180 | for (; i + Vector.Count <= length; i += Vector.Count) 181 | { 182 | Vector v_buffer = VectorHelper.LoadUnsafe(ref buffer, i); 183 | Vector v_window = VectorHelper.LoadUnsafe(ref window, i); 184 | 185 | Vector result = v_buffer * v_window; 186 | result.StoreUnsafe(ref buffer, (nuint) i); 187 | } 188 | } 189 | 190 | for (; i < length; i++) 191 | { 192 | Unsafe.Add(ref buffer, i) *= Unsafe.Add(ref window, i); 193 | } 194 | } 195 | return true; 196 | } 197 | return false; 198 | } 199 | 200 | public int GetPacketSampleCount(ref VorbisPacket packet) 201 | { 202 | GetPacketInfo(ref packet, out _, out int packetStartIndex, out int packetValidLength, out _); 203 | return packetValidLength - packetStartIndex; 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /SngTool/NVorbis/NVorbis.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | NVorbis 5 | A fully managed implementation of a Xiph.org Foundation Ogg Vorbis decoder. 6 | Copyright © TechPizza 2024 7 | TechPizza 8 | VorbisPizza 9 | Andrew Ward;TechPizza 10 | 1.4.2 11 | VorbisPizza 12 | https://github.com/TechPizzaDev/VorbisPizza 13 | -Respect EoS (or lack thereof) 14 | Ogg;Vorbis;Xiph;Audio;Sound 15 | en 16 | false 17 | true 18 | https://github.com/TechPizzaDev/VorbisPizza 19 | git 20 | true 21 | LICENSE 22 | true 23 | snupkg 24 | false 25 | true 26 | enable 27 | 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /SngTool/NVorbis/NewStreamEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NVorbis.Contracts; 3 | 4 | namespace NVorbis 5 | { 6 | /// 7 | /// Event data for when a new logical stream is found in a container. 8 | /// 9 | public struct NewStreamEventArgs 10 | { 11 | /// 12 | /// Creates a new instance of with the specified . 13 | /// 14 | /// An instance. 15 | public NewStreamEventArgs(IStreamDecoder streamDecoder) 16 | { 17 | StreamDecoder = streamDecoder ?? throw new ArgumentNullException(nameof(streamDecoder)); 18 | IgnoreStream = false; 19 | } 20 | 21 | /// 22 | /// Gets new the instance. 23 | /// 24 | public IStreamDecoder StreamDecoder { get; } 25 | 26 | /// 27 | /// Gets or sets whether to ignore the logical stream associated with the packet provider. 28 | /// 29 | public bool IgnoreStream { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/ContainerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using NVorbis.Contracts; 5 | using NVorbis.Contracts.Ogg; 6 | 7 | namespace NVorbis.Ogg 8 | { 9 | /// 10 | /// Implements for Ogg format files for low memory cost. 11 | /// 12 | public sealed class ContainerReader : IContainerReader 13 | { 14 | private IPageReader _reader; 15 | private List> _packetProviders; 16 | private bool _foundStream; 17 | 18 | /// 19 | /// Gets or sets the callback to invoke when a new stream is encountered in the container. 20 | /// 21 | public NewStreamHandler? NewStreamCallback { get; set; } 22 | 23 | /// 24 | /// Returns a list of streams available from this container. 25 | /// 26 | public IReadOnlyList GetStreams() 27 | { 28 | List list = new(_packetProviders.Count); 29 | for (int i = 0; i < _packetProviders.Count; i++) 30 | { 31 | if (_packetProviders[i].TryGetTarget(out IPacketProvider? pp)) 32 | { 33 | list.Add(pp); 34 | } 35 | else 36 | { 37 | list.RemoveAt(i); 38 | --i; 39 | } 40 | } 41 | return list; 42 | } 43 | 44 | /// 45 | /// Gets whether the underlying stream can seek. 46 | /// 47 | public bool CanSeek => _reader.CanSeek; 48 | 49 | /// 50 | public long WasteBits => _reader.WasteBits; 51 | 52 | /// 53 | public long ContainerBits => _reader.ContainerBits; 54 | 55 | /// 56 | /// Creates a new instance of . 57 | /// 58 | /// The configuration instance. 59 | /// The to read. 60 | /// to close the stream when disposed, otherwise . 61 | public ContainerReader(VorbisConfig config, Stream stream, bool leaveOpen) 62 | { 63 | if (stream == null) throw new ArgumentNullException(nameof(stream)); 64 | 65 | _packetProviders = new List>(); 66 | 67 | _reader = new PageReader(config, stream, leaveOpen, ProcessNewStream); 68 | } 69 | 70 | /// 71 | /// Attempts to initialize the container. 72 | /// 73 | /// if successful, otherwise . 74 | public bool TryInit() 75 | { 76 | return FindNextStream(); 77 | } 78 | 79 | /// 80 | /// Finds the next new stream in the container. 81 | /// 82 | /// True if a new stream was found, otherwise False. 83 | public bool FindNextStream() 84 | { 85 | _reader.Lock(); 86 | try 87 | { 88 | _foundStream = false; 89 | while (_reader.ReadNextPage(out PageData? pageData)) 90 | { 91 | pageData.DecrementRef(); 92 | 93 | if (_foundStream) 94 | { 95 | return true; 96 | } 97 | } 98 | return false; 99 | } 100 | finally 101 | { 102 | _reader.Release(); 103 | } 104 | } 105 | 106 | private bool ProcessNewStream(IPacketProvider packetProvider) 107 | { 108 | bool relock = _reader.Release(); 109 | try 110 | { 111 | if (NewStreamCallback?.Invoke(packetProvider) ?? true) 112 | { 113 | _packetProviders.Add(new WeakReference(packetProvider)); 114 | _foundStream = true; 115 | return true; 116 | } 117 | return false; 118 | } 119 | finally 120 | { 121 | if (relock) 122 | { 123 | _reader.Lock(); 124 | } 125 | } 126 | } 127 | 128 | /// 129 | public void Dispose() 130 | { 131 | foreach (WeakReference provider in _packetProviders) 132 | { 133 | if (provider.TryGetTarget(out IPacketProvider? target)) 134 | { 135 | target.Dispose(); 136 | } 137 | } 138 | _packetProviders.Clear(); 139 | 140 | _reader?.Dispose(); 141 | _reader = null!; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/Crc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | 4 | namespace NVorbis.Ogg 5 | { 6 | internal partial struct Crc 7 | { 8 | private uint _crc; 9 | private uint[] _table; 10 | 11 | public static Crc Create() 12 | { 13 | return new Crc 14 | { 15 | _crc = 0U, 16 | _table = s_crcTable, 17 | }; 18 | } 19 | 20 | private static unsafe uint Update(uint* table, uint crc, byte* buffer, nint length) 21 | { 22 | byte* end = buffer + length; 23 | while (((nint)buffer & 7) != 0 && buffer < end) 24 | { 25 | crc = table[((byte)crc) ^ *buffer++] ^ (crc >> 8); 26 | } 27 | 28 | uint* table1 = table + 1 * TableLength; 29 | uint* table2 = table + 2 * TableLength; 30 | uint* table3 = table + 3 * TableLength; 31 | uint* table4 = table + 4 * TableLength; 32 | uint* table5 = table + 5 * TableLength; 33 | uint* table6 = table + 6 * TableLength; 34 | uint* table7 = table + 7 * TableLength; 35 | 36 | while (buffer < end - 7) 37 | { 38 | ulong value = !BitConverter.IsLittleEndian 39 | ? BinaryPrimitives.ReverseEndianness(*(ulong*)buffer) 40 | : *(ulong*)buffer; 41 | 42 | uint high = (uint)(value >> 32); 43 | 44 | crc ^= (uint)value; 45 | 46 | crc = table[(high >> 24) & 0xFF] ^ 47 | table1[(high >> 16) & 0xFF] ^ 48 | table2[(high >> 8) & 0xFF] ^ 49 | table3[high & 0xFF] ^ 50 | table4[((crc >> 24))] ^ 51 | table5[((crc >> 16) & 0xFF)] ^ 52 | table6[((crc >> 8) & 0xFF)] ^ 53 | table7[(crc & 0xFF)]; 54 | 55 | buffer += 8; 56 | } 57 | 58 | while (buffer < end) 59 | { 60 | crc = table[((byte)crc) ^ *buffer++] ^ (crc >> 8); 61 | } 62 | return crc; 63 | } 64 | 65 | public unsafe void Update(ReadOnlySpan values) 66 | { 67 | fixed (uint* table = _table) 68 | fixed (byte* ptr = values) 69 | { 70 | _crc = Update(table, _crc, ptr, values.Length); 71 | } 72 | } 73 | 74 | public unsafe void Update(byte value) 75 | { 76 | _crc = _table[((byte)_crc) ^ value] ^ (_crc >> 8); 77 | } 78 | 79 | public bool Test(uint checkCrc) 80 | { 81 | return _crc == checkCrc; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/IPacketGranuleCountProvider.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace NVorbis.Ogg 3 | { 4 | /// 5 | /// Encapsulates a method that calculates the number of granules in a packet. 6 | /// 7 | public interface IPacketGranuleCountProvider 8 | { 9 | /// 10 | /// Calculates the number of granules decodable from the specified packet. 11 | /// 12 | /// The to calculate. 13 | /// The calculated number of granules. 14 | int GetPacketGranuleCount(ref VorbisPacket packet); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/PacketData.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace NVorbis.Ogg 5 | { 6 | /// 7 | /// Represents a packet location and potentially a data slice. 8 | /// 9 | public struct PacketData : IEquatable 10 | { 11 | /// 12 | /// Gets the packet location for this packet data. 13 | /// 14 | public PacketLocation Location { get; } 15 | 16 | /// 17 | /// Gets or sets the page slice for this packet data. Can be . 18 | /// 19 | public PageSlice Slice { get; set; } 20 | 21 | /// 22 | /// Constructs the with the given location and data. 23 | /// 24 | /// The location of the packet within the logical stream. 25 | /// The data of the packet from a page. Can be . 26 | public PacketData(PacketLocation location, PageSlice slice) 27 | { 28 | Location = location; 29 | Slice = slice; 30 | } 31 | 32 | /// 33 | /// Constructs the with the given location. 34 | /// 35 | /// 36 | public PacketData(PacketLocation location) : this(location, default) 37 | { 38 | } 39 | 40 | /// 41 | public bool Equals(PacketData other) 42 | { 43 | return Location.Equals(other.Location); 44 | } 45 | 46 | /// 47 | public override bool Equals(object? obj) 48 | { 49 | return obj is PacketData other && Equals(other); 50 | } 51 | 52 | /// 53 | public override int GetHashCode() 54 | { 55 | return Location.GetHashCode(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/PacketLocation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NVorbis.Ogg 4 | { 5 | /// 6 | /// Represents the location of a packet within a logical stream. 7 | /// 8 | public readonly struct PacketLocation : IEquatable 9 | { 10 | /// 11 | /// The maximum value of . 12 | /// 13 | public const ulong MaxPageIndex = ulong.MaxValue >> 8; 14 | 15 | /// 16 | /// The maximum value of . 17 | /// 18 | public const byte MaxPacketIndex = byte.MaxValue; 19 | 20 | private readonly ulong _value; 21 | 22 | /// 23 | /// Gets the page index of the packet. 24 | /// 25 | public ulong PageIndex => _value >> 8; 26 | 27 | /// 28 | /// Gets the packet index within the page. 29 | /// 30 | public byte PacketIndex => (byte)(_value & 0xff); 31 | 32 | /// 33 | /// Constructs the with the given values. 34 | /// 35 | /// The page index. Cannot be greater than . 36 | /// The packet index. Cannot be greater than . 37 | /// 38 | /// or was out of the allowed range. 39 | /// 40 | public PacketLocation(ulong pageIndex, uint packetIndex) 41 | { 42 | if (pageIndex > MaxPageIndex) 43 | throw new ArgumentOutOfRangeException(nameof(packetIndex)); 44 | 45 | if (packetIndex > MaxPacketIndex) 46 | throw new ArgumentOutOfRangeException(nameof(packetIndex)); 47 | 48 | _value = (pageIndex << 8) | packetIndex; 49 | } 50 | 51 | /// 52 | public PacketLocation(long pageIndex, int packetIndex) : this((ulong)pageIndex, (uint)packetIndex) 53 | { 54 | } 55 | 56 | /// 57 | public bool Equals(PacketLocation other) 58 | { 59 | return _value == other._value; 60 | } 61 | 62 | /// 63 | public override bool Equals(object? obj) 64 | { 65 | return obj is PacketLocation other && Equals(other); 66 | } 67 | 68 | /// 69 | public override int GetHashCode() 70 | { 71 | return _value.GetHashCode(); 72 | } 73 | 74 | /// 75 | public override string ToString() 76 | { 77 | return $"{PageIndex}[{PacketIndex}]"; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/PageData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using VoxelPizza.Memory; 5 | 6 | namespace NVorbis.Ogg 7 | { 8 | internal sealed class PageData : RefCounted 9 | { 10 | private readonly PageDataPool _pool; 11 | private ArraySegment _pageData; 12 | 13 | public bool IsResync { get; private set; } 14 | 15 | public PageHeader Header => new(AsSpan()); 16 | 17 | public int Length => AsSegment().Count; 18 | 19 | internal PageData(PageDataPool pool) 20 | { 21 | _pool = pool ?? throw new ArgumentNullException(nameof(pool)); 22 | 23 | _pageData = Array.Empty(); 24 | } 25 | 26 | public void Reset(ArraySegment pageData, bool isResync) 27 | { 28 | ResetState(); 29 | 30 | _pageData = pageData; 31 | IsResync = isResync; 32 | } 33 | 34 | public ArraySegment AsSegment() 35 | { 36 | if (IsClosed) 37 | { 38 | ThrowObjectDisposed(); 39 | } 40 | return _pageData; 41 | } 42 | 43 | public Span AsSpan() 44 | { 45 | return AsSegment().AsSpan(); 46 | } 47 | 48 | internal ArraySegment ReplaceSegment(ArraySegment newSegment) 49 | { 50 | ArraySegment previousSegment = _pageData; 51 | _pageData = newSegment; 52 | return previousSegment; 53 | } 54 | 55 | public PageSlice GetPacket(uint packetIndex) 56 | { 57 | ReadOnlySpan pageSpan = AsSpan(); 58 | PageHeader header = new(pageSpan); 59 | 60 | byte segmentCount = header.SegmentCount; 61 | ReadOnlySpan segments = pageSpan.Slice(27, segmentCount); 62 | int packetIdx = 0; 63 | int dataIdx = 27 + segments.Length; 64 | int size = 0; 65 | for (int i = 0; i < segments.Length; i++) 66 | { 67 | byte seg = segments[i]; 68 | size += seg; 69 | if (seg < 255) 70 | { 71 | if (packetIndex == packetIdx) 72 | { 73 | return new PageSlice(this, dataIdx, size); 74 | } 75 | packetIdx++; 76 | dataIdx += size; 77 | size = 0; 78 | } 79 | } 80 | if (packetIndex == packetIdx) 81 | { 82 | return new PageSlice(this, dataIdx, size); 83 | } 84 | return new PageSlice(this, 0, 0); 85 | } 86 | 87 | [MethodImpl(MethodImplOptions.NoInlining)] 88 | protected override void Release() 89 | { 90 | _pool.Return(this); 91 | 92 | base.Release(); 93 | } 94 | 95 | #if DEBUG 96 | ~PageData() 97 | { 98 | if (Count > 0) 99 | { 100 | _pool.Return(this); 101 | } 102 | } 103 | #endif 104 | } 105 | } -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/PageHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Diagnostics; 4 | using NVorbis.Contracts.Ogg; 5 | 6 | namespace NVorbis.Ogg 7 | { 8 | internal ref struct PageHeader 9 | { 10 | public const int MaxHeaderSize = 282; 11 | 12 | public ReadOnlySpan Data { get; } 13 | 14 | public int StreamSerial => BinaryPrimitives.ReadInt32LittleEndian(Data.Slice(14, sizeof(int))); 15 | public int SequenceNumber => BinaryPrimitives.ReadInt32LittleEndian(Data.Slice(18, sizeof(int))); 16 | public PageFlags PageFlags => (PageFlags)Data[5]; 17 | public long GranulePosition => BinaryPrimitives.ReadInt64LittleEndian(Data.Slice(6, sizeof(long))); 18 | public byte SegmentCount => Data[26]; 19 | public int PageOverhead => 27 + SegmentCount; 20 | 21 | public PageHeader(ReadOnlySpan headerData) 22 | { 23 | Debug.Assert(headerData.Length >= 27); 24 | 25 | Data = headerData; 26 | 27 | Debug.Assert(headerData.Length >= PageOverhead); 28 | } 29 | 30 | public void GetPacketCount(out ushort packetCount, out int dataLength, out bool isContinued) 31 | { 32 | GetPacketCount(Data, out packetCount, out dataLength, out isContinued); 33 | } 34 | 35 | public static void GetPacketCount( 36 | ReadOnlySpan headerData, out ushort packetCount, out int dataLength, out bool isContinued) 37 | { 38 | byte segCnt = headerData[26]; 39 | int dataLen = 0; 40 | ushort pktCnt = 0; 41 | 42 | ReadOnlySpan segments = headerData.Slice(27, segCnt); 43 | for (int i = 0; i < segments.Length; i++) 44 | { 45 | byte seg = segments[i]; 46 | dataLen += seg; 47 | if (seg < 255) 48 | { 49 | ++pktCnt; 50 | } 51 | } 52 | 53 | isContinued = segments[^1] == 255; 54 | if (isContinued) 55 | ++pktCnt; 56 | 57 | packetCount = pktCnt; 58 | dataLength = dataLen; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/PageReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.IO; 6 | using System.Threading; 7 | using NVorbis.Contracts.Ogg; 8 | 9 | namespace NVorbis.Ogg 10 | { 11 | internal sealed class PageReader : PageReaderBase 12 | { 13 | private readonly Dictionary _streamReaders = new(); 14 | private readonly List _readersToDispose = new(); 15 | private readonly NewStreamCallback _newStreamCallback; 16 | private readonly object _readLock = new(); 17 | 18 | private PageData? _page; 19 | private long _pageOffset; 20 | private long _nextPageOffset; 21 | 22 | public PageReader(VorbisConfig config, Stream stream, bool leaveOpen, NewStreamCallback newStreamCallback) 23 | : base(config, stream, leaveOpen) 24 | { 25 | _newStreamCallback = newStreamCallback; 26 | } 27 | 28 | public override void Lock() 29 | { 30 | Monitor.Enter(_readLock); 31 | } 32 | 33 | protected override bool CheckLock() 34 | { 35 | return Monitor.IsEntered(_readLock); 36 | } 37 | 38 | public override bool Release() 39 | { 40 | if (Monitor.IsEntered(_readLock)) 41 | { 42 | Monitor.Exit(_readLock); 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | protected override void SaveNextPageSearch() 49 | { 50 | _nextPageOffset = StreamPosition; 51 | } 52 | 53 | protected override void PrepareStreamForNextPage() 54 | { 55 | SeekStream(_nextPageOffset); 56 | } 57 | 58 | protected override bool AddPage(PageData pageData) 59 | { 60 | PageHeader header = pageData.Header; 61 | header.GetPacketCount(out ushort packetCount, out int dataLength, out _); 62 | 63 | // if the page doesn't have any packets, we can't use it 64 | if (packetCount == 0) 65 | { 66 | return false; 67 | } 68 | 69 | int streamSerial = header.StreamSerial; 70 | long pageOffset = StreamPosition - (header.PageOverhead + dataLength); 71 | PageFlags pageFlags = header.PageFlags; 72 | 73 | if (_streamReaders.TryGetValue(streamSerial, out IStreamPageReader? spr)) 74 | { 75 | spr.AddPage(pageData, pageOffset); 76 | 77 | // if we've read the last page, remove from our list so cleanup can happen. 78 | // this is safe because the instance still has access to us for reading. 79 | if ((pageFlags & PageFlags.EndOfStream) == PageFlags.EndOfStream) 80 | { 81 | if (_streamReaders.Remove(streamSerial, out IStreamPageReader? sprToDispose)) 82 | { 83 | Debug.Assert(spr == sprToDispose); 84 | _readersToDispose.Add(spr); 85 | } 86 | } 87 | } 88 | else 89 | { 90 | StreamPageReader streamReader = new(this, streamSerial); 91 | streamReader.AddPage(pageData, pageOffset); 92 | 93 | _streamReaders.Add(streamSerial, streamReader); 94 | if (!_newStreamCallback.Invoke(streamReader.PacketProvider)) 95 | { 96 | streamReader.Dispose(); 97 | _streamReaders.Remove(streamSerial); 98 | return false; 99 | } 100 | } 101 | return true; 102 | } 103 | 104 | public override bool ReadPageAt(long offset, [MaybeNullWhen(false)] out PageData pageData) 105 | { 106 | Span hdrBuf = stackalloc byte[PageHeader.MaxHeaderSize]; 107 | 108 | // make sure we're locked; no sense reading if we aren't 109 | if (!CheckLock()) 110 | throw new InvalidOperationException("Must be locked prior to reading!"); 111 | 112 | // this should be safe; we've already checked the page by now 113 | if (offset == _pageOffset) 114 | { 115 | pageData = _page; 116 | if (pageData != null) 117 | { 118 | // short circuit for when we've already loaded the page 119 | pageData.IncrementRef(); 120 | return true; 121 | } 122 | } 123 | 124 | SeekStream(offset); 125 | int cnt = EnsureRead(hdrBuf.Slice(0, 27)); 126 | 127 | _pageOffset = offset; 128 | ClearLastPage(); 129 | 130 | if (VerifyHeader(hdrBuf, ref cnt)) 131 | { 132 | PageHeader header = new(hdrBuf); 133 | header.GetPacketCount(out _, out int dataLength, out _); 134 | 135 | int length = header.PageOverhead + dataLength; 136 | pageData = Config.PageDataPool.Rent(length, false); 137 | 138 | Span pageSpan = pageData.AsSpan(); 139 | hdrBuf.Slice(0, cnt).CopyTo(pageSpan); 140 | 141 | Span dataSpan = pageSpan.Slice(cnt); 142 | int read = EnsureRead(dataSpan); 143 | if (read != dataSpan.Length) 144 | { 145 | pageData.DecrementRef(); 146 | pageData = null; 147 | return false; 148 | } 149 | 150 | pageData.IncrementRef(); 151 | _page = pageData; 152 | return true; 153 | } 154 | 155 | pageData = null; 156 | return false; 157 | } 158 | 159 | public override bool ReadPageHeaderAt(long offset, Span headerBuffer) 160 | { 161 | if (headerBuffer.Length < PageHeader.MaxHeaderSize) 162 | throw new ArgumentException(null, nameof(headerBuffer)); 163 | 164 | // make sure we're locked; no sense reading if we aren't 165 | if (!CheckLock()) 166 | throw new InvalidOperationException("Must be locked prior to reading!"); 167 | 168 | // this should be safe; we've already checked the page by now 169 | if (offset == _pageOffset) 170 | { 171 | if (_page != null) 172 | { 173 | // short circuit for when we've already loaded the page 174 | ReadOnlySpan data = _page.AsSpan(); 175 | data.Slice(0, Math.Min(data.Length, headerBuffer.Length)).CopyTo(headerBuffer); 176 | return true; 177 | } 178 | } 179 | 180 | SeekStream(offset); 181 | int cnt = EnsureRead(headerBuffer.Slice(0, 27)); 182 | 183 | _pageOffset = offset; 184 | ClearLastPage(); 185 | 186 | if (VerifyHeader(headerBuffer, ref cnt)) 187 | { 188 | return true; 189 | } 190 | return false; 191 | } 192 | 193 | private void ClearLastPage() 194 | { 195 | if (_page != null) 196 | { 197 | _page.DecrementRef(); 198 | _page = null; 199 | } 200 | } 201 | 202 | protected override void SetEndOfStreams() 203 | { 204 | foreach (KeyValuePair kvp in _streamReaders) 205 | { 206 | kvp.Value.SetEndOfStream(); 207 | } 208 | _streamReaders.Clear(); 209 | } 210 | 211 | protected override void Dispose(bool disposing) 212 | { 213 | if (disposing && !IsDisposed) 214 | { 215 | foreach (KeyValuePair kvp in _streamReaders) 216 | { 217 | kvp.Value.Dispose(); 218 | } 219 | _streamReaders.Clear(); 220 | 221 | foreach (IStreamPageReader spr in _readersToDispose) 222 | { 223 | spr.Dispose(); 224 | } 225 | _readersToDispose.Clear(); 226 | 227 | ClearLastPage(); 228 | } 229 | base.Dispose(disposing); 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /SngTool/NVorbis/Ogg/PageSlice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace NVorbis.Ogg 5 | { 6 | /// 7 | /// Represents a slice of page data. 8 | /// 9 | public readonly struct PageSlice 10 | { 11 | internal PageData Page { get; } 12 | 13 | /// 14 | /// Gets the data offset within the page. 15 | /// 16 | public int Start { get; } 17 | 18 | /// 19 | /// Gets the length of the page slice. 20 | /// 21 | public int Length { get; } 22 | 23 | internal PageSlice(PageData page, int start, int length) 24 | { 25 | Debug.Assert((uint)start <= (uint)page.Length); 26 | Debug.Assert((uint)length <= (uint)(page.Length - start)); 27 | 28 | Page = page; 29 | Start = start; 30 | Length = length; 31 | } 32 | 33 | /// 34 | /// Gets a span view over the page data slice. 35 | /// 36 | /// 37 | public Span AsSpan() 38 | { 39 | if (Page == null) 40 | { 41 | return Span.Empty; 42 | } 43 | return Page.AsSpan().Slice(Start, Length); 44 | } 45 | 46 | /// 47 | /// Gets an array segment view over the page data slice. 48 | /// 49 | public ArraySegment AsSegment() 50 | { 51 | if (Page == null) 52 | { 53 | return ArraySegment.Empty; 54 | } 55 | return Page.AsSegment().Slice(Start, Length); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SngTool/NVorbis/PacketExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace NVorbis 5 | { 6 | /// 7 | /// Provides extension methods for . 8 | /// 9 | public static class PacketExtensions 10 | { 11 | /// 12 | /// Reads into the specified buffer. 13 | /// 14 | /// The packet to read from. 15 | /// The buffer to read into. 16 | /// The number of bytes actually read into the buffer. 17 | public static int Read(ref this VorbisPacket packet, Span buffer) 18 | { 19 | for (int i = 0; i < buffer.Length; i++) 20 | { 21 | byte value = (byte)packet.TryPeekBits(8, out int bitsRead); 22 | if (bitsRead == 0) 23 | { 24 | return i; 25 | } 26 | buffer[i] = value; 27 | packet.SkipBits(8); 28 | } 29 | return buffer.Length; 30 | } 31 | 32 | /// 33 | /// Reads the specified number of bytes from the packet and advances the position counter. 34 | /// 35 | /// The packet to read from. 36 | /// The number of bytes to read. 37 | /// A byte array holding the data read. 38 | /// The packet did not contain enough data. 39 | public static byte[] ReadBytes(ref this VorbisPacket packet, int count) 40 | { 41 | byte[] buf = new byte[count]; 42 | int cnt = Read(ref packet, buf.AsSpan(0, count)); 43 | if (cnt < count) 44 | { 45 | throw new EndOfStreamException(); 46 | } 47 | return buf; 48 | } 49 | 50 | /// 51 | /// Reads one bit from the packet and advances the read position. 52 | /// 53 | /// The packet to read from. 54 | /// if the bit was a one, otehrwise . 55 | public static bool ReadBit(ref this VorbisPacket packet) 56 | { 57 | return packet.ReadBits(1) == 1; 58 | } 59 | 60 | /// 61 | /// Reads the next byte from the packet. Does not advance the position counter. 62 | /// 63 | /// The packet to read from. 64 | /// The byte read from the packet. 65 | public static byte PeekByte(ref this VorbisPacket packet) 66 | { 67 | return (byte)packet.TryPeekBits(8, out _); 68 | } 69 | 70 | /// 71 | /// Reads the next byte from the packet and advances the position counter. 72 | /// 73 | /// The packet to read from. 74 | /// The byte read from the packet. 75 | public static byte ReadByte(ref this VorbisPacket packet) 76 | { 77 | return (byte)packet.ReadBits(8); 78 | } 79 | 80 | /// 81 | /// Reads the next 16 bits from the packet as a and advances the position counter. 82 | /// 83 | /// The packet to read from. 84 | /// The value of the next 16 bits. 85 | public static short ReadInt16(ref this VorbisPacket packet) 86 | { 87 | return (short)packet.ReadBits(16); 88 | } 89 | 90 | /// 91 | /// Reads the next 32 bits from the packet as a and advances the position counter. 92 | /// 93 | /// The packet to read from. 94 | /// The value of the next 32 bits. 95 | public static int ReadInt32(ref this VorbisPacket packet) 96 | { 97 | return (int)packet.ReadBits(32); 98 | } 99 | 100 | /// 101 | /// Reads the next 64 bits from the packet as a and advances the position counter. 102 | /// 103 | /// The packet to read from. 104 | /// The value of the next 64 bits. 105 | public static long ReadInt64(ref this VorbisPacket packet) 106 | { 107 | return (long)packet.ReadBits(64); 108 | } 109 | 110 | /// 111 | /// Reads the next 16 bits from the packet as a and advances the position counter. 112 | /// 113 | /// The packet to read from. 114 | /// The value of the next 16 bits. 115 | public static ushort ReadUInt16(ref this VorbisPacket packet) 116 | { 117 | return (ushort)packet.ReadBits(16); 118 | } 119 | 120 | /// 121 | /// Reads the next 32 bits from the packet as a and advances the position counter. 122 | /// 123 | /// The packet to read from. 124 | /// The value of the next 32 bits. 125 | public static uint ReadUInt32(ref this VorbisPacket packet) 126 | { 127 | return (uint)packet.ReadBits(32); 128 | } 129 | 130 | /// 131 | /// Reads the next 64 bits from the packet as a and advances the position counter. 132 | /// 133 | /// The packet to read from. 134 | /// The value of the next 64 bits. 135 | public static ulong ReadUInt64(ref this VorbisPacket packet) 136 | { 137 | return packet.ReadBits(64); 138 | } 139 | 140 | /// 141 | /// Advances the position counter by the specified number of bytes. 142 | /// 143 | /// The packet to read from. 144 | /// The number of bytes to advance. 145 | /// The packet did not contain enough data. 146 | public static void SkipBytes(ref this VorbisPacket packet, int count) 147 | { 148 | int bitsSkipped = packet.SkipBits(count * 8); 149 | if (bitsSkipped != count * 8) 150 | { 151 | throw new EndOfStreamException(); 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /SngTool/NVorbis/PageDataPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using NVorbis.Ogg; 6 | 7 | namespace NVorbis 8 | { 9 | internal sealed class PageDataPool 10 | { 11 | private ArrayPool _arrayPool = ArrayPool.Create(); 12 | private Queue _objectPool = new(); 13 | 14 | public PageData Rent(int length, bool isResync) 15 | { 16 | byte[] array = _arrayPool.Rent(length); 17 | ArraySegment segment = new(array, 0, length); 18 | 19 | PageData obj = RentObject(); 20 | obj.Reset(segment, isResync); 21 | return obj; 22 | } 23 | 24 | private PageData RentObject() 25 | { 26 | lock (_objectPool) 27 | { 28 | if (_objectPool.TryDequeue(out PageData? result)) 29 | { 30 | return result; 31 | } 32 | } 33 | return new PageData(this); 34 | } 35 | 36 | public void Return(PageData pageData) 37 | { 38 | if (!pageData.IsClosed) 39 | { 40 | ThrowNotClosedPage(); 41 | } 42 | 43 | ArraySegment segment = pageData.ReplaceSegment(default); 44 | if (segment.Array != null) 45 | { 46 | _arrayPool.Return(segment.Array); 47 | } 48 | 49 | ReturnObject(pageData); 50 | } 51 | 52 | private void ReturnObject(PageData pageData) 53 | { 54 | lock (_objectPool) 55 | { 56 | if (_objectPool.Count <= 4) 57 | { 58 | _objectPool.Enqueue(pageData); 59 | return; 60 | } 61 | } 62 | } 63 | 64 | [DoesNotReturn] 65 | private static void ThrowNotClosedPage() 66 | { 67 | throw new InvalidOperationException("Attempt at returning PageData that is not closed."); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SngTool/NVorbis/PreRollPacketException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NVorbis 4 | { 5 | public class PreRollPacketException : Exception 6 | { 7 | private const string DefaultMessage = "Could not read pre-roll packet. Try seeking again prior to reading more samples."; 8 | 9 | public PreRollPacketException() : base(DefaultMessage) 10 | { 11 | } 12 | 13 | public PreRollPacketException(string? message) : base(message ?? DefaultMessage) 14 | { 15 | } 16 | 17 | public PreRollPacketException(string? message, Exception? innerException) : base(message ?? DefaultMessage, innerException) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Residue1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NVorbis 4 | { 5 | // each channel gets its own pass, with the dimensions interleaved 6 | internal sealed class Residue1 : Residue0 7 | { 8 | public Residue1(ref VorbisPacket packet, int channels, Codebook[] codebooks) : base(ref packet, channels, codebooks) 9 | { 10 | } 11 | 12 | protected override bool WriteVectors( 13 | Codebook codebook, ref VorbisPacket packet, ReadOnlySpan residues, int channel, int offset, int partitionSize) 14 | { 15 | for (int i = 0; i < partitionSize;) 16 | { 17 | int entry = codebook.DecodeScalar(ref packet); 18 | if (entry == -1) 19 | { 20 | return true; 21 | } 22 | 23 | ReadOnlySpan lookup = codebook.GetLookup(entry); 24 | Span res = residues[channel].AsSpan(offset + i, lookup.Length); 25 | 26 | for (int j = 0; j < lookup.Length; j++) 27 | { 28 | res[j] += lookup[j]; 29 | } 30 | 31 | i += lookup.Length; 32 | } 33 | 34 | return false; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Residue2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Runtime.Intrinsics; 6 | 7 | namespace NVorbis 8 | { 9 | // all channels in one pass, interleaved 10 | internal sealed class Residue2 : Residue0 11 | { 12 | private int _channels; 13 | 14 | public Residue2(ref VorbisPacket packet, int channels, Codebook[] codebooks) : base(ref packet, 1, codebooks) 15 | { 16 | _channels = channels; 17 | } 18 | 19 | public override void Decode( 20 | ref VorbisPacket packet, ReadOnlySpan doNotDecodeChannel, int blockSize, ReadOnlySpan buffers) 21 | { 22 | // since we're doing all channels in a single pass, the block size has to be multiplied. 23 | // otherwise this is just a pass-through call 24 | base.Decode(ref packet, doNotDecodeChannel, blockSize * _channels, buffers); 25 | } 26 | 27 | protected override bool WriteVectors( 28 | Codebook codebook, ref VorbisPacket packet, ReadOnlySpan residues, int channel, int offset, int partitionSize) 29 | { 30 | uint dimensions = (uint) codebook.Dimensions; 31 | uint channels = (uint) _channels; 32 | Debug.Assert(residues.Length == _channels); 33 | 34 | if (dimensions != 1 && channels == 2) 35 | { 36 | return WriteVectors(codebook, ref packet, residues, offset, partitionSize); 37 | } 38 | else if (channels == 1) 39 | { 40 | return WriteVectors(codebook, ref packet, residues, offset, partitionSize); 41 | } 42 | else 43 | { 44 | return WriteVectors(codebook, ref packet, residues, offset, partitionSize); 45 | } 46 | } 47 | 48 | private bool WriteVectors( 49 | Codebook codebook, ref VorbisPacket packet, ReadOnlySpan residues, int offset, int partitionSize) 50 | where TState : struct, IWriteVectorState 51 | { 52 | uint dimensions = (uint) codebook.Dimensions; 53 | uint channels = (uint) _channels; 54 | uint o = (uint) offset / channels; 55 | 56 | ref float lookupTable = ref MemoryMarshal.GetReference(codebook.GetLookupTable()); 57 | TState state = new(); 58 | 59 | for (uint c = 0; c < partitionSize; c += dimensions) 60 | { 61 | int entry = codebook.DecodeScalar(ref packet); 62 | if (entry == -1) 63 | { 64 | return true; 65 | } 66 | 67 | ref float lookup = ref Unsafe.Add(ref lookupTable, (uint) entry * dimensions); 68 | state.Invoke(residues, ref lookup, dimensions, ref o); 69 | } 70 | return false; 71 | } 72 | 73 | private readonly struct WriteVectorStereo : IWriteVectorState 74 | { 75 | public void Invoke(ReadOnlySpan residues, ref float lookup, uint dimensions, ref uint o) 76 | { 77 | ref float res0 = ref MemoryMarshal.GetArrayDataReference(residues[0]); 78 | ref float res1 = ref MemoryMarshal.GetArrayDataReference(residues[1]); 79 | 80 | uint d = 0; 81 | 82 | if (Vector128.IsHardwareAccelerated) 83 | { 84 | for (; d + 8 <= dimensions; d += 8, o += 4) 85 | { 86 | Vector128 lookup0 = Vector128.LoadUnsafe(ref lookup, d + 0); // [ 0, 1, 2, 3 ] 87 | Vector128 lookup1 = Vector128.LoadUnsafe(ref lookup, d + 4); // [ 4, 5, 6, 7 ] 88 | 89 | Vector128 aLo = Vector128Helper.UnpackLow(lookup0, lookup1); // [ 0, 4, 1, 5 ] 90 | Vector128 aHi = Vector128Helper.UnpackHigh(lookup0, lookup1); // [ 2, 6, 3, 7 ] 91 | 92 | Vector128 bLo = Vector128Helper.UnpackLow(aLo, aHi); // [ 0, 2, 4, 6 ] 93 | Vector128 bHi = Vector128Helper.UnpackHigh(aLo, aHi); // [ 1, 3, 5, 7 ] 94 | 95 | Vector128 vres0 = Vector128.LoadUnsafe(ref res0, o); 96 | Vector128 vres1 = Vector128.LoadUnsafe(ref res1, o); 97 | 98 | Vector128 sum0 = vres0 + bLo; 99 | Vector128 sum1 = vres1 + bHi; 100 | 101 | sum0.StoreUnsafe(ref res0, o); 102 | sum1.StoreUnsafe(ref res1, o); 103 | } 104 | } 105 | 106 | for (; d < dimensions; d += 2, o++) 107 | { 108 | Unsafe.Add(ref res0, o) += Unsafe.Add(ref lookup, d + 0); 109 | Unsafe.Add(ref res1, o) += Unsafe.Add(ref lookup, d + 1); 110 | } 111 | } 112 | } 113 | 114 | private readonly struct WriteVectorMono : IWriteVectorState 115 | { 116 | public void Invoke(ReadOnlySpan residues, ref float lookup, uint dimensions, ref uint o) 117 | { 118 | ref float res0 = ref MemoryMarshal.GetArrayDataReference(residues[0]); 119 | 120 | for (uint d = 0; d < dimensions; d += 1, o++) 121 | { 122 | Unsafe.Add(ref res0, o) += Unsafe.Add(ref lookup, d); 123 | } 124 | } 125 | } 126 | 127 | private struct WriteVectorFallback : IWriteVectorState 128 | { 129 | private int _ch; 130 | 131 | public void Invoke(ReadOnlySpan residues, ref float lookup, uint dimensions, ref uint o) 132 | { 133 | for (uint d = 0; d < dimensions; d++) 134 | { 135 | residues[_ch][o] += Unsafe.Add(ref lookup, d); 136 | if (++_ch == residues.Length) 137 | { 138 | _ch = 0; 139 | o++; 140 | } 141 | } 142 | } 143 | } 144 | 145 | private interface IWriteVectorState 146 | { 147 | void Invoke(ReadOnlySpan residues, ref float lookup, uint dimensions, ref uint o); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /SngTool/NVorbis/SeekOutOfRangeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NVorbis 4 | { 5 | public class SeekOutOfRangeException : Exception 6 | { 7 | private const string DefaultMessage = "The requested seek position extends beyond the stream."; 8 | 9 | public SeekOutOfRangeException() : base(DefaultMessage) 10 | { 11 | } 12 | 13 | public SeekOutOfRangeException(string? message) : base(message ?? DefaultMessage) 14 | { 15 | } 16 | 17 | public SeekOutOfRangeException(string? message, Exception? innerException) : base(message ?? DefaultMessage, innerException) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SngTool/NVorbis/StreamStats.cs: -------------------------------------------------------------------------------- 1 | using NVorbis.Contracts; 2 | 3 | namespace NVorbis 4 | { 5 | internal unsafe class StreamStats : IStreamStats 6 | { 7 | private int _sampleRate; 8 | 9 | private PacketInt2 _packetBits; 10 | private PacketInt2 _packetSamples; 11 | private int _packetIndex; 12 | 13 | private long _totalSamples; 14 | private long _audioBits; 15 | private long _headerBits; 16 | private long _containerBits; 17 | private long _wasteBits; 18 | 19 | private object _lock = new(); 20 | private int _packetCount; 21 | 22 | public int EffectiveBitRate 23 | { 24 | get 25 | { 26 | long samples, bits; 27 | lock (_lock) 28 | { 29 | samples = _totalSamples; 30 | bits = _audioBits + _headerBits + _containerBits + _wasteBits; 31 | } 32 | if (samples > 0) 33 | { 34 | return (int)(((double)bits / samples) * _sampleRate); 35 | } 36 | return 0; 37 | } 38 | } 39 | 40 | public int InstantBitRate 41 | { 42 | get 43 | { 44 | int samples, bits; 45 | lock (_lock) 46 | { 47 | bits = _packetBits.Buffer[0] + _packetBits.Buffer[1]; 48 | samples = _packetSamples.Buffer[0] + _packetSamples.Buffer[1]; 49 | } 50 | if (samples > 0) 51 | { 52 | return (int)(((double)bits / samples) * _sampleRate); 53 | } 54 | return 0; 55 | } 56 | } 57 | 58 | public long ContainerBits => _containerBits; 59 | 60 | public long OverheadBits => _headerBits; 61 | 62 | public long AudioBits => _audioBits; 63 | 64 | public long WasteBits => _wasteBits; 65 | 66 | public int PacketCount => _packetCount; 67 | 68 | public void ResetStats() 69 | { 70 | lock (_lock) 71 | { 72 | _packetBits = default; 73 | _packetSamples = default; 74 | _packetIndex = 0; 75 | _packetCount = 0; 76 | _audioBits = 0; 77 | _totalSamples = 0; 78 | _headerBits = 0; 79 | _containerBits = 0; 80 | _wasteBits = 0; 81 | } 82 | } 83 | 84 | internal void SetSampleRate(int sampleRate) 85 | { 86 | lock (_lock) 87 | { 88 | _sampleRate = sampleRate; 89 | 90 | ResetStats(); 91 | } 92 | } 93 | 94 | internal void AddPacket(int samples, int bits, int waste, int container) 95 | { 96 | lock (_lock) 97 | { 98 | if (samples >= 0) 99 | { 100 | // audio packet 101 | _audioBits += bits; 102 | _wasteBits += waste; 103 | _containerBits += container; 104 | _totalSamples += samples; 105 | _packetBits.Buffer[_packetIndex] = bits + waste; 106 | _packetSamples.Buffer[_packetIndex] = samples; 107 | 108 | if (++_packetIndex == 2) 109 | { 110 | _packetIndex = 0; 111 | } 112 | } 113 | else 114 | { 115 | // header packet 116 | _headerBits += bits; 117 | _wasteBits += waste; 118 | _containerBits += container; 119 | } 120 | } 121 | } 122 | 123 | private struct PacketInt2 124 | { 125 | public fixed int Buffer[2]; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /SngTool/NVorbis/TagData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using NVorbis.Contracts; 5 | 6 | namespace NVorbis 7 | { 8 | internal class TagData : ITagData 9 | { 10 | private Dictionary> _tags; 11 | 12 | public TagData(byte[] utf8Vendor, byte[][] utf8Comments) 13 | { 14 | EncoderVendor = Encoding.UTF8.GetString(utf8Vendor); 15 | 16 | Dictionary> tags = new(); 17 | for (int i = 0; i < utf8Comments.Length; i++) 18 | { 19 | string[] parts = Encoding.UTF8.GetString(utf8Comments[i]).Split('='); 20 | if (parts.Length == 1) 21 | { 22 | parts = new[] { parts[0], string.Empty }; 23 | } 24 | 25 | int bktIdx = parts[0].IndexOf('['); 26 | if (bktIdx > -1) 27 | { 28 | parts[1] = parts[0] 29 | .Substring(bktIdx + 1, parts[0].Length - bktIdx - 2) 30 | .ToUpper(System.Globalization.CultureInfo.CurrentCulture) 31 | + ": " 32 | + parts[1]; 33 | parts[0] = parts[0].Substring(0, bktIdx); 34 | } 35 | 36 | if (tags.TryGetValue(parts[0].ToUpperInvariant(), out IReadOnlyList? list)) 37 | { 38 | ((List)list).Add(parts[1]); 39 | } 40 | else 41 | { 42 | tags.Add(parts[0].ToUpperInvariant(), new List { parts[1] }); 43 | } 44 | } 45 | _tags = tags; 46 | } 47 | 48 | public string GetTagSingle(string key, bool concatenate = false) 49 | { 50 | IReadOnlyList values = GetTagMulti(key); 51 | if (values.Count > 0) 52 | { 53 | if (concatenate) 54 | { 55 | return string.Join(Environment.NewLine, values); 56 | } 57 | return values[values.Count - 1]; 58 | } 59 | return string.Empty; 60 | } 61 | 62 | public IReadOnlyList GetTagMulti(string key) 63 | { 64 | if (_tags.TryGetValue(key.ToUpperInvariant(), out IReadOnlyList? values)) 65 | { 66 | return values; 67 | } 68 | return Array.Empty(); 69 | } 70 | 71 | public IReadOnlyDictionary> All => _tags; 72 | 73 | public string EncoderVendor { get; } 74 | 75 | public string Title => GetTagSingle("TITLE"); 76 | 77 | public string Version => GetTagSingle("VERSION"); 78 | 79 | public string Album => GetTagSingle("ALBUM"); 80 | 81 | public string TrackNumber => GetTagSingle("TRACKNUMBER"); 82 | 83 | public string Artist => GetTagSingle("ARTIST"); 84 | 85 | public IReadOnlyList Performers => GetTagMulti("PERFORMER"); 86 | 87 | public string Copyright => GetTagSingle("COPYRIGHT"); 88 | 89 | public string License => GetTagSingle("LICENSE"); 90 | 91 | public string Organization => GetTagSingle("ORGANIZATION"); 92 | 93 | public string Description => GetTagSingle("DESCRIPTION"); 94 | 95 | public IReadOnlyList Genres => GetTagMulti("GENRE"); 96 | 97 | public IReadOnlyList Dates => GetTagMulti("DATE"); 98 | 99 | public IReadOnlyList Locations => GetTagMulti("LOCATION"); 100 | 101 | public string Contact => GetTagSingle("CONTACT"); 102 | 103 | public string Isrc => GetTagSingle("ISRC"); 104 | } 105 | } -------------------------------------------------------------------------------- /SngTool/NVorbis/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.Intrinsics; 4 | 5 | namespace NVorbis 6 | { 7 | internal static class Utils 8 | { 9 | private const float LowerClip = -0.99999994f; 10 | private const float UpperClip = 0.99999994f; 11 | 12 | internal static int ilog(int x) 13 | { 14 | int cnt = 0; 15 | while (x > 0) 16 | { 17 | ++cnt; 18 | x >>= 1; // this is safe because we'll never get here if the sign bit is set 19 | } 20 | return cnt; 21 | } 22 | 23 | internal static uint BitReverse(uint n) 24 | { 25 | return BitReverse(n, 32); 26 | } 27 | 28 | internal static uint BitReverse(uint n, int bits) 29 | { 30 | n = ((n & 0xAAAAAAAA) >> 1) | ((n & 0x55555555) << 1); 31 | n = ((n & 0xCCCCCCCC) >> 2) | ((n & 0x33333333) << 2); 32 | n = ((n & 0xF0F0F0F0) >> 4) | ((n & 0x0F0F0F0F) << 4); 33 | n = ((n & 0xFF00FF00) >> 8) | ((n & 0x00FF00FF) << 8); 34 | return ((n >> 16) | (n << 16)) >> (32 - bits); 35 | } 36 | 37 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 38 | internal static float ClipValue(float value, ref bool clipped) 39 | { 40 | if (value > UpperClip) 41 | { 42 | clipped = true; 43 | return UpperClip; 44 | } 45 | if (value < LowerClip) 46 | { 47 | clipped = true; 48 | return LowerClip; 49 | } 50 | return value; 51 | } 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | internal static Vector ClipValue(Vector value, ref Vector clipped) 55 | { 56 | Vector upper = new(UpperClip); 57 | Vector lower = new(LowerClip); 58 | 59 | Vector gt = Vector.GreaterThan(value, upper); 60 | Vector lt = Vector.LessThan(value, lower); 61 | clipped = Vector.BitwiseOr(clipped, Vector.BitwiseOr(gt, lt)); 62 | 63 | value = Vector.ConditionalSelect(gt, upper, value); 64 | value = Vector.ConditionalSelect(lt, lower, value); 65 | 66 | return value; 67 | } 68 | 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | internal static Vector128 ClipValue(Vector128 value, ref Vector128 clipped) 71 | { 72 | Vector128 upper = Vector128.Create(UpperClip); 73 | Vector128 lower = Vector128.Create(LowerClip); 74 | 75 | Vector128 gt = Vector128.GreaterThan(value, upper); 76 | Vector128 lt = Vector128.LessThan(value, lower); 77 | clipped = Vector128.BitwiseOr(clipped, Vector128.BitwiseOr(gt, lt)); 78 | 79 | value = Vector128.ConditionalSelect(gt, upper, value); 80 | value = Vector128.ConditionalSelect(lt, lower, value); 81 | 82 | return value; 83 | } 84 | 85 | internal static float ConvertFromVorbisFloat32(uint bits) 86 | { 87 | // do as much as possible with bit tricks in integer math 88 | int sign = (int) bits >> 31; // sign-extend to the full 32-bits 89 | int exponent = (int) ((bits & 0x7fe00000) >> 21) - 788; // grab the exponent, remove the bias. 90 | float mantissa = ((int) (bits & 0x1fffff) ^ sign) + (sign & 1); // grab the mantissa and apply the sign bit. 91 | 92 | // NB: We could use bit tricks to calc the exponent, but it can't be more than 63 in either direction. 93 | // This creates an issue, since the exponent field allows for a *lot* more than that. 94 | // On the flip side, larger exponent values don't seem to be used by the Vorbis codebooks... 95 | // Either way, we'll play it safe and let the BCL calculate it. 96 | 97 | return System.MathF.ScaleB(mantissa, exponent); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Vector128Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.Intrinsics; 4 | using System.Runtime.Intrinsics.Arm; 5 | using System.Runtime.Intrinsics.X86; 6 | 7 | namespace NVorbis 8 | { 9 | internal static class Vector128Helper 10 | { 11 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | public static Vector128 UnpackLow(Vector128 left, Vector128 right) 13 | { 14 | if (Sse.IsSupported) 15 | { 16 | return Sse.UnpackLow(left, right); 17 | } 18 | else if (AdvSimd.Arm64.IsSupported) 19 | { 20 | return AdvSimd.Arm64.ZipLow(left, right); 21 | } 22 | else 23 | { 24 | return SoftwareFallback(left, right); 25 | } 26 | 27 | static Vector128 SoftwareFallback(Vector128 left, Vector128 right) 28 | { 29 | Unsafe.SkipInit(out Vector128 result); 30 | result = result.WithElement(0, left.GetElement(0)); 31 | result = result.WithElement(1, right.GetElement(0)); 32 | result = result.WithElement(2, left.GetElement(1)); 33 | result = result.WithElement(3, right.GetElement(1)); 34 | return result; 35 | } 36 | } 37 | 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public static Vector128 UnpackHigh(Vector128 left, Vector128 right) 40 | { 41 | if (Sse.IsSupported) 42 | { 43 | return Sse.UnpackHigh(left, right); 44 | } 45 | else if (AdvSimd.Arm64.IsSupported) 46 | { 47 | return AdvSimd.Arm64.ZipHigh(left, right); 48 | } 49 | else 50 | { 51 | return SoftwareFallback(left, right); 52 | } 53 | 54 | static Vector128 SoftwareFallback(Vector128 left, Vector128 right) 55 | { 56 | Unsafe.SkipInit(out Vector128 result); 57 | result = result.WithElement(0, left.GetElement(2)); 58 | result = result.WithElement(1, right.GetElement(2)); 59 | result = result.WithElement(2, left.GetElement(3)); 60 | result = result.WithElement(3, right.GetElement(3)); 61 | return result; 62 | } 63 | } 64 | 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public static unsafe Vector128 Gather( 67 | float* baseAddress, 68 | Vector128 index, 69 | [ConstantExpected(Min = 1, Max = 8)] byte scale) 70 | { 71 | if (Avx2.IsSupported) 72 | { 73 | return Avx2.GatherVector128(baseAddress, index, scale); 74 | } 75 | else 76 | { 77 | return SoftwareFallback(baseAddress, index, scale); 78 | } 79 | 80 | static Vector128 SoftwareFallback( 81 | float* baseAddress, 82 | Vector128 index, 83 | byte scale) 84 | { 85 | Unsafe.SkipInit(out Vector128 result); 86 | result = result.WithElement(0, baseAddress[(long) index.GetElement(0) * scale]); 87 | result = result.WithElement(1, baseAddress[(long) index.GetElement(1) * scale]); 88 | result = result.WithElement(2, baseAddress[(long) index.GetElement(2) * scale]); 89 | result = result.WithElement(3, baseAddress[(long) index.GetElement(3) * scale]); 90 | return result; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SngTool/NVorbis/Vector256Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.Intrinsics; 4 | using System.Runtime.Intrinsics.X86; 5 | 6 | namespace NVorbis 7 | { 8 | internal static class Vector256Helper 9 | { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static unsafe Vector256 Gather( 12 | float* baseAddress, 13 | Vector256 index, 14 | [ConstantExpected(Min = 1, Max = 8)] byte scale) 15 | { 16 | if (Avx2.IsSupported) 17 | { 18 | return Avx2.GatherVector256(baseAddress, index, scale); 19 | } 20 | else 21 | { 22 | return SoftwareFallback(baseAddress, index, scale); 23 | } 24 | 25 | static Vector256 SoftwareFallback( 26 | float* baseAddress, 27 | Vector256 index, 28 | [ConstantExpected(Min = 1, Max = 8)] byte scale) 29 | { 30 | return Vector256.Create( 31 | Vector128Helper.Gather(baseAddress, index.GetLower(), scale), 32 | Vector128Helper.Gather(baseAddress, index.GetUpper(), scale)); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SngTool/NVorbis/VectorHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Numerics; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace NVorbis 7 | { 8 | internal static class VectorHelper 9 | { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static Vector LoadUnsafe(ref T source, int elementOffset) 12 | where T : struct 13 | { 14 | ThrowForUnsupportedNumericsVectorBaseType(); 15 | ref byte address = ref Unsafe.As(ref Unsafe.Add(ref source, elementOffset)); 16 | return Unsafe.ReadUnaligned>(ref address); 17 | } 18 | 19 | #if !NET8_0_OR_GREATER 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public static void StoreUnsafe(this Vector source, ref T destination, nuint elementOffset) 22 | where T : struct 23 | { 24 | ThrowForUnsupportedNumericsVectorBaseType(); 25 | ref byte address = ref Unsafe.As(ref Unsafe.Add(ref destination, elementOffset)); 26 | Unsafe.WriteUnaligned(ref address, source); 27 | } 28 | #endif 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | internal static void ThrowForUnsupportedNumericsVectorBaseType() 32 | where T : struct 33 | { 34 | if (!Vector.IsSupported) 35 | { 36 | ThrowNotSupportedException(); 37 | 38 | [DoesNotReturn] 39 | static void ThrowNotSupportedException() 40 | { 41 | throw new NotSupportedException(); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SngTool/NVorbis/VorbisConfig.cs: -------------------------------------------------------------------------------- 1 | namespace NVorbis 2 | { 3 | /// 4 | /// Holds configuration and state used by the library. 5 | /// 6 | public class VorbisConfig 7 | { 8 | /// 9 | /// Gets the global config instance. 10 | /// 11 | public static VorbisConfig Default { get; } = new() 12 | { 13 | PageDataPool = new PageDataPool(), 14 | }; 15 | 16 | internal PageDataPool PageDataPool { get; init; } 17 | 18 | private VorbisConfig() 19 | { 20 | PageDataPool = null!; 21 | } 22 | 23 | /// 24 | /// Clones the config instance. 25 | /// 26 | public VorbisConfig Clone() 27 | { 28 | return new VorbisConfig() 29 | { 30 | PageDataPool = PageDataPool, 31 | }; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SngTool/SngCli/ConMan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | 5 | namespace SngCli 6 | { 7 | public static class ConMan 8 | { 9 | public static int ProgressItems { get; set; } 10 | private static int progress; 11 | private static Stopwatch stopwatch; 12 | private static readonly object consoleLock = new object(); 13 | private static bool progressActive = false; 14 | private static bool errorDisableOutput = false; 15 | private static int updateInterval = 80; 16 | private static Thread? updateThread; 17 | 18 | static ConMan() 19 | { 20 | progress = 0; 21 | AppDomain.CurrentDomain.ProcessExit += (s, ev) => DisableProgress(); 22 | Console.CancelKeyPress += (s, ev) => DisableProgress(); 23 | stopwatch = new Stopwatch(); 24 | } 25 | 26 | public static void UpdateProgress(int value) 27 | { 28 | progress = value; 29 | } 30 | 31 | public static void EnableProgress(int totalItems) 32 | { 33 | ProgressItems = totalItems; 34 | Console.CursorVisible = false; 35 | progressActive = true; 36 | progress = 0; 37 | stopwatch.Start(); 38 | updateThread = new Thread(() => 39 | { 40 | var sleepTime = updateInterval / 2; 41 | while (progressActive) 42 | { 43 | lock (consoleLock) 44 | { 45 | DrawProgressBar(); 46 | } 47 | Thread.Sleep(16); 48 | } 49 | }); 50 | updateThread.Start(); 51 | 52 | } 53 | 54 | public static void DisableProgress(bool error = false) 55 | { 56 | if (!progressActive) 57 | return; 58 | DrawProgressBar(); 59 | Console.WriteLine(); 60 | progressActive = false; 61 | errorDisableOutput = error; 62 | ProgressItems = 0; 63 | Console.CursorVisible = true; 64 | 65 | Console.WriteLine("Total elapsed time: " + stopwatch.Elapsed.ToString()); 66 | stopwatch.Reset(); 67 | updateThread = null; 68 | } 69 | 70 | public static void Out(string message) 71 | { 72 | if (!progressActive) 73 | { 74 | if (!errorDisableOutput) 75 | Console.WriteLine(message); 76 | return; 77 | } 78 | 79 | lock (consoleLock) 80 | { 81 | var width = Console.BufferWidth; 82 | var firstNewLine = message.IndexOf('\n'); 83 | 84 | // Deal with line wrapping 85 | if (firstNewLine != -1 || message.Length > width) 86 | { 87 | // always handle first line with all spaces 88 | string prepOutput = new string(' ', width); 89 | 90 | int lineCount = 0; 91 | int messagePos = firstNewLine < width ? firstNewLine + 1 : 0; 92 | 93 | while (messagePos < message.Length) 94 | { 95 | var nextNewLine = message.IndexOf('\n', messagePos); 96 | 97 | // no more new lines to deal with calculate the remaining lines 98 | if (nextNewLine == -1) 99 | { 100 | lineCount += (int)Math.Ceiling((float)(message.Length - messagePos) / width); 101 | prepOutput += new string('\n', lineCount); 102 | messagePos = message.Length; 103 | break; 104 | } 105 | else 106 | { 107 | lineCount++; 108 | messagePos = nextNewLine + 1; 109 | } 110 | } 111 | Console.Write(prepOutput); 112 | Console.CursorTop -= lineCount; 113 | } 114 | else 115 | { 116 | // Simple case where there are no new lines, and the message 117 | // is shorter than the width of the console 118 | Console.WriteLine(); 119 | Console.CursorTop -= 1; 120 | var linePadding = width - message.Length; 121 | if (linePadding > 0) 122 | { 123 | message += new string(' ', linePadding); 124 | } 125 | } 126 | 127 | Console.WriteLine(message); 128 | DrawProgressBar(); 129 | } 130 | } 131 | 132 | private static TimeSpan lastSpinner = default; 133 | private static int spinIndex = 0; 134 | 135 | private static void DrawProgressBar() 136 | { 137 | if (!progressActive) 138 | { 139 | return; 140 | } 141 | 142 | var original = Console.GetCursorPosition(); 143 | Console.CursorTop = Console.WindowTop + Console.WindowHeight - 1; 144 | 145 | float percent = (float)progress / ProgressItems; 146 | 147 | var width = Console.BufferWidth - 25; 148 | 149 | int progressBarFilledLength = (int)(width * percent); 150 | int progressBarRemain = (int)Math.Round((width * percent) - progressBarFilledLength); 151 | int progressBarEmptyLength = width - progressBarFilledLength - progressBarRemain; 152 | 153 | string progressBarFilled = new string('=', progressBarFilledLength); 154 | string progressHalf = new string('-', progressBarRemain); 155 | string progressBarEmpty = new string(' ', progressBarEmptyLength); 156 | 157 | var spinnerElapsed = stopwatch.Elapsed - lastSpinner; 158 | if (spinnerElapsed.Milliseconds > updateInterval) 159 | { 160 | spinIndex = (spinIndex + 1) % SpinnerChars.Length; 161 | lastSpinner = stopwatch.Elapsed; 162 | } 163 | 164 | var time = stopwatch.Elapsed; 165 | 166 | string elapsedTime = string.Format("{0:D2}:{1:D2}:{2:D2}", time.Hours, time.Minutes, time.Seconds); 167 | 168 | Console.Write($"[{progressBarFilled}{progressHalf}{progressBarEmpty}] {percent * 100: 0}% {SpinnerChars[spinIndex]} {elapsedTime}"); 169 | 170 | Console.SetCursorPosition(original.Left, original.Top); 171 | } 172 | 173 | private static readonly char[] SpinnerChars = new[] { '|', '/', '-', '\\' }; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /SngTool/SngCli/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SngCli": { 4 | "commandName": "Project", 5 | "commandLineArgs": "encode --input H:\\OrganizedSongs\\staging\\ --output .\\out" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /SngTool/SngCli/SngCli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | win-x64 9 | true 10 | true 11 | false 12 | true 13 | true 14 | partial 15 | true 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SngTool/SngCli/SngDecode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Cysharp.Collections; 6 | using SngLib; 7 | using SongLib; 8 | 9 | namespace SngCli 10 | { 11 | public static class SngDecode 12 | { 13 | private static string[] FindAllSngFiles(string rootFolder) 14 | { 15 | return Directory.GetFiles(rootFolder, "*.sng", SearchOption.AllDirectories); 16 | } 17 | 18 | private static void SerializeMetadata(SngFile sngFile, string savePath) 19 | { 20 | KnownKeys.ValidateKeys(sngFile.Metadata); 21 | 22 | IniFile iniFile = new IniFile(); 23 | foreach (var (key, value) in sngFile.Metadata) 24 | { 25 | if (!KnownKeys.IsKnownKey(key) && Program.Verbose) 26 | { 27 | ConMan.Out($"Unknown metadata. Key: {key} Value: {value}"); 28 | } 29 | iniFile.SetString("song", key, value); 30 | } 31 | 32 | iniFile.Save(savePath); 33 | } 34 | 35 | private static async Task DecodeSong(string sngPath) 36 | { 37 | var conf = SngDecodingOptions.Instance; 38 | var folderName = Path.GetFileNameWithoutExtension(sngPath); 39 | var parentFolder = Path.GetDirectoryName(sngPath); 40 | 41 | var relative = Path.GetRelativePath(conf.InputPath!, parentFolder!); 42 | var outputFolder = Path.Combine(Path.GetFullPath(conf.OutputPath!), relative, folderName); 43 | ConMan.Out(outputFolder); 44 | SngFile sngFile = SngSerializer.LoadSngFile(sngPath); 45 | 46 | if (!Directory.Exists(outputFolder)) 47 | { 48 | Directory.CreateDirectory(outputFolder); 49 | } 50 | 51 | // create ini file from metadata 52 | SerializeMetadata(sngFile, Path.Combine(outputFolder, "song.ini")); 53 | 54 | // iterate through files and save them to disk 55 | foreach ((var name, var data) in sngFile.Files) 56 | { 57 | var filePath = Path.Combine(outputFolder, Path.Combine(name.Split("/"))); 58 | var folder = Path.GetDirectoryName(filePath)!; 59 | if (!Directory.Exists(folder)) 60 | { 61 | Directory.CreateDirectory(folder); 62 | } 63 | await data!.WriteToFileAsync(filePath); 64 | } 65 | ConMan.UpdateProgress(Interlocked.Increment(ref completedSongs)); 66 | } 67 | 68 | private static int completedSongs; 69 | private static int erroredSongs; 70 | 71 | 72 | public async static Task ProcessSongs() 73 | { 74 | var conf = SngDecodingOptions.Instance; 75 | 76 | if (!Directory.Exists(conf.InputPath)) 77 | { 78 | ConMan.Out("Input folder does not exist"); 79 | Program.DisplayHelp(); 80 | Environment.Exit(1); 81 | return; 82 | } 83 | 84 | var songs = FindAllSngFiles(conf.InputPath!); 85 | if (songs.Length == 0) 86 | { 87 | ConMan.Out($"No valid songs found at: {conf.InputPath!}"); 88 | Environment.Exit(1); 89 | return; 90 | } 91 | 92 | if (conf.StatusBar) 93 | { 94 | ConMan.EnableProgress(1); 95 | } 96 | 97 | ConMan.ProgressItems = songs.Length; 98 | ConMan.Out($"Song count: {songs.Length}"); 99 | await Utils.ForEachAsync(songs, async (sngFile, token) => 100 | { 101 | try 102 | { 103 | await DecodeSong(sngFile); 104 | } 105 | catch (Exception e) 106 | { 107 | ConMan.Out($"{sngFile} ERROR! \n{e}"); 108 | erroredSongs++; 109 | } 110 | }, conf.Threads); 111 | 112 | if (conf.StatusBar) 113 | { 114 | ConMan.DisableProgress(); 115 | } 116 | 117 | Console.WriteLine($"Extracted Songs: {completedSongs}"); 118 | Console.WriteLine($"Errored Songs: {erroredSongs}"); 119 | Console.WriteLine($"Total Songs Processed: {songs.Length}"); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /SngTool/SngCli/SngDecodingOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SngCli 7 | { 8 | 9 | 10 | public class SngDecodingOptions 11 | { 12 | private static SngDecodingOptions? _instance; 13 | public static SngDecodingOptions Instance => _instance ?? throw new InvalidOperationException("Not initialized"); 14 | 15 | public string? InputPath; 16 | public string? OutputPath; 17 | 18 | public short Threads; 19 | public bool StatusBar; 20 | 21 | public SngDecodingOptions(Dictionary args) 22 | { 23 | _instance = this; 24 | // Validate command line arguments 25 | if (!(args.TryGetValue("in", out InputPath) || args.TryGetValue("i", out InputPath))) 26 | { 27 | Console.WriteLine("Input folder argument not found:"); 28 | Program.DisplayHelp(); 29 | Environment.Exit(1); 30 | return; 31 | } 32 | if (!(args.TryGetValue("out", out OutputPath) || args.TryGetValue("o", out OutputPath))) 33 | { 34 | Console.WriteLine("Output folder argument not found:"); 35 | Program.DisplayHelp(); 36 | Environment.Exit(1); 37 | return; 38 | } 39 | 40 | string? count; 41 | if (!((args.TryGetValue("threads", out count) || args.TryGetValue("t", out count)) && short.TryParse(count, out Threads))) 42 | { 43 | Threads = -1; 44 | } 45 | 46 | StatusBar = !args.TryGetValue("noStatusBar", out _); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /SngTool/SngCli/SngEncodingOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using SongLib; 6 | 7 | namespace SngCli 8 | { 9 | internal class SngEncodingConfig 10 | { 11 | public string? InputPath; 12 | public string? OutputPath; 13 | 14 | // Program options 15 | public short Threads; 16 | public bool VideoExclude; 17 | public bool SkipUnknown; 18 | public bool SkipExisting; 19 | 20 | public bool StatusBar; 21 | 22 | public bool EncodeUnknown; 23 | 24 | // JPEG options 25 | public bool JpegEncode; 26 | public bool AlbumUpscale; 27 | public JpegEncoding.SizeTiers AlbumSize = JpegEncoding.SizeTiers.Size512x512; 28 | public int JpegQuality = 75; 29 | 30 | // OPUS options 31 | public bool OpusEncode; 32 | public int OpusBitrate = 80; 33 | 34 | private static SngEncodingConfig? _instance; 35 | public static SngEncodingConfig Instance => _instance ?? throw new InvalidOperationException("Not initialized"); 36 | 37 | private bool ValidSize(string sizeInput) 38 | { 39 | switch (sizeInput) 40 | { 41 | case "None": 42 | case "Nearest": 43 | case "256x256": 44 | case "384x384": 45 | case "512x512": 46 | case "768x768": 47 | case "1024x1024": 48 | case "1536x1536": 49 | case "2048x2048": 50 | return true; 51 | default: 52 | return false; 53 | } 54 | } 55 | 56 | private JpegEncoding.SizeTiers SizeStrToEnum(string sizeInput) 57 | { 58 | switch (sizeInput) 59 | { 60 | case "Nearest": 61 | return JpegEncoding.SizeTiers.Nearest; 62 | case "256x256": 63 | return JpegEncoding.SizeTiers.Size256x256; 64 | case "384x384": 65 | return JpegEncoding.SizeTiers.Size384x384; 66 | case "512x512": 67 | return JpegEncoding.SizeTiers.Size512x512; 68 | case "768x768": 69 | return JpegEncoding.SizeTiers.Size768x768; 70 | case "1024x1024": 71 | return JpegEncoding.SizeTiers.Size1024x1024; 72 | case "1536x1536": 73 | return JpegEncoding.SizeTiers.Size1536x1536; 74 | case "2048x2048": 75 | return JpegEncoding.SizeTiers.Size2048x2048; 76 | default: 77 | return JpegEncoding.SizeTiers.None; 78 | } 79 | } 80 | 81 | public SngEncodingConfig(Dictionary args) 82 | { 83 | _instance = this; 84 | // Validate command line arguments 85 | if (!(args.TryGetValue("in", out InputPath) || args.TryGetValue("i", out InputPath))) 86 | { 87 | Console.WriteLine("Input folder argument not found:"); 88 | Program.DisplayHelp(); 89 | Environment.Exit(1); 90 | return; 91 | } 92 | if (!(args.TryGetValue("out", out OutputPath) || args.TryGetValue("o", out OutputPath))) 93 | { 94 | Console.WriteLine("Output folder argument not found:"); 95 | Program.DisplayHelp(); 96 | Environment.Exit(1); 97 | return; 98 | } 99 | 100 | string? count; 101 | if (!((args.TryGetValue("threads", out count) || args.TryGetValue("t", out count)) && short.TryParse(count, out Threads))) 102 | { 103 | Threads = -1; 104 | } 105 | 106 | // Bool flags we just need to make sure the keys exist 107 | VideoExclude = args.TryGetValue("videoExclude", out _); 108 | OpusEncode = args.TryGetValue("opusEncode", out _); 109 | JpegEncode = args.TryGetValue("jpegEncode", out _); 110 | AlbumUpscale = args.TryGetValue("albumUpscale", out _); 111 | SkipUnknown = args.TryGetValue("skipUnknown", out _); 112 | SkipExisting = args.TryGetValue("skipExisting", out _); 113 | StatusBar = !args.TryGetValue("noStatusBar", out _); 114 | EncodeUnknown = args.TryGetValue("encodeUnknown", out _); 115 | 116 | if (args.TryGetValue("opusBitrate", out string? bitrateStr) && bitrateStr != null) 117 | { 118 | if (!int.TryParse(bitrateStr, out int opusBitrate)) 119 | { 120 | Console.WriteLine($"Value for opusBitrate is not valid {bitrateStr}"); 121 | } 122 | else 123 | { 124 | OpusBitrate = opusBitrate; 125 | } 126 | } 127 | 128 | if (args.TryGetValue("jpegQuality", out string? jpegQualityStr) && jpegQualityStr != null) 129 | { 130 | if (!int.TryParse(jpegQualityStr, out int jpegQuality)) 131 | { 132 | Console.WriteLine($"Value for jpegQuality is not valid {jpegQualityStr}"); 133 | } 134 | else 135 | { 136 | JpegQuality = jpegQuality; 137 | } 138 | } 139 | 140 | if (args.TryGetValue("albumResize", out string? albumSize) && albumSize != null) 141 | { 142 | if (!ValidSize(albumSize)) 143 | { 144 | Console.WriteLine($"Value for albumResize is not valid {albumSize}"); 145 | } 146 | else 147 | { 148 | AlbumSize = SizeStrToEnum(albumSize); 149 | } 150 | } 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /SngTool/SngCli/build.bat: -------------------------------------------------------------------------------- 1 | cd Server 2 | dotnet publish /p:DebugType=None -c Release --self-contained -r win-x64 --output .\bin\build\win-x64 3 | dotnet publish /p:DebugType=None -c Release --self-contained -r linux-x64 --output .\bin\build\linux-x64 4 | @REM dotnet publish /p:DebugType=None -c Release --self-contained -r osx-x64 --output .\bin\build\osx-x64 5 | pause -------------------------------------------------------------------------------- /SngTool/SngLib/LargeFile.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using Cysharp.Collections; 3 | 4 | public static class LargeFile 5 | { 6 | public static async Task ReadAllBytesAsync(string path) 7 | { 8 | using (FileStream f = File.OpenRead(path)) 9 | { 10 | var fileLength = f.Length; 11 | var arr = new NativeByteArray(fileLength, skipZeroClear: true); 12 | await arr.ReadFromAsync(f); 13 | return arr; 14 | } 15 | } 16 | 17 | public static void ReadToNativeArray(this Stream stream, NativeByteArray arr, long readCount) 18 | { 19 | var writer = arr.CreateBufferWriter(); 20 | 21 | long readTotal = 0; 22 | int read; 23 | 24 | Span GetWriteSpan() 25 | { 26 | long remaining = readCount - readTotal; 27 | 28 | Span spanVal; 29 | 30 | if (remaining > int.MaxValue) 31 | { 32 | spanVal = writer.GetSpan(int.MaxValue); 33 | } 34 | else if (remaining > 0) 35 | { 36 | spanVal = writer.GetSpan((int)remaining).Slice(0, (int)remaining); 37 | } 38 | else 39 | { 40 | spanVal = Array.Empty(); 41 | } 42 | 43 | return spanVal; 44 | } 45 | 46 | while ((read = stream.Read(GetWriteSpan())) != 0) 47 | { 48 | readTotal += read; 49 | writer.Advance(read); 50 | } 51 | // set size to the total number of bytes actually read 52 | // this won't reallocate the array, 53 | arr.Resize(readCount); 54 | } 55 | 56 | public static void WriteFromNativeArray(this Stream stream, NativeByteArray arr, int chunkSize = int.MaxValue) 57 | { 58 | foreach (var item in arr.AsReadOnlyMemoryList(chunkSize)) 59 | { 60 | stream.Write(item.Span); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /SngTool/SngLib/NativeByteArray/NativeByteArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | #if !NETSTANDARD2_0 && !UNITY_2019_1_OR_NEWER 4 | 5 | using System; 6 | using System.IO; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Cysharp.Collections 11 | { 12 | public static class NativeMemoryArrayExtensions 13 | { 14 | public static async Task ReadFromAsync(this NativeByteArray buffer, Stream stream, IProgress? progress = null, CancellationToken cancellationToken = default) 15 | { 16 | var writer = buffer.CreateBufferWriter(); 17 | 18 | long readTotal = 0; 19 | int read; 20 | int GetRemainingStreamChunkLength() 21 | { 22 | if (!stream.CanSeek) 23 | { 24 | return 0x1000000; // just request a 16mb chunk if we cannot get a length 25 | } 26 | else 27 | { 28 | var totalRemaining = stream.Length - readTotal; 29 | if (totalRemaining > int.MaxValue) 30 | { 31 | return int.MaxValue; 32 | } 33 | else if (totalRemaining < 0) 34 | { 35 | return 0x1000000; // just request a 16mb chunk if we don't have any remaining 36 | } 37 | else 38 | { 39 | return (int)totalRemaining; 40 | } 41 | } 42 | } 43 | 44 | while ((read = await stream.ReadAsync(writer.GetMemory(GetRemainingStreamChunkLength()), cancellationToken).ConfigureAwait(false)) != 0) 45 | { 46 | readTotal += read; 47 | progress?.Report(readTotal); 48 | writer.Advance(read); 49 | } 50 | // set size to the total number of bytes actually read 51 | // this won't reallocate the array, 52 | buffer.Resize(readTotal); 53 | } 54 | 55 | public static async Task WriteToFileAsync(this NativeByteArray buffer, string path, FileMode mode = FileMode.Create, IProgress? progress = null, CancellationToken cancellationToken = default) 56 | { 57 | using (var fs = new FileStream(path, mode, FileAccess.Write, FileShare.ReadWrite, 1, useAsync: true)) 58 | { 59 | await buffer.WriteToAsync(fs, progress: progress, cancellationToken: cancellationToken); 60 | } 61 | } 62 | 63 | public static async Task WriteToAsync(this NativeByteArray buffer, Stream stream, int chunkSize = int.MaxValue, IProgress? progress = null, CancellationToken cancellationToken = default) 64 | { 65 | foreach (var item in buffer.AsReadOnlyMemoryList(chunkSize)) 66 | { 67 | await stream.WriteAsync(item, cancellationToken); 68 | progress?.Report(item.Length); 69 | } 70 | } 71 | } 72 | } 73 | 74 | #endif -------------------------------------------------------------------------------- /SngTool/SngLib/NativeByteArray/PointerMemoryManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Cysharp.Collections 6 | { 7 | internal sealed unsafe class PointerMemoryManager : MemoryManager 8 | { 9 | byte* pointer; 10 | int length; 11 | bool usingMemory; 12 | 13 | internal PointerMemoryManager(byte* pointer, int length) 14 | { 15 | this.pointer = pointer; 16 | this.length = length; 17 | usingMemory = false; 18 | } 19 | 20 | protected override void Dispose(bool disposing) 21 | { 22 | } 23 | 24 | public override Span GetSpan() 25 | { 26 | usingMemory = true; 27 | return new Span(pointer, length); 28 | } 29 | 30 | public override MemoryHandle Pin(int elementIndex = 0) 31 | { 32 | if ((uint)elementIndex >= (uint)length) ThrowHelper.ThrowIndexOutOfRangeException(); 33 | return new MemoryHandle(pointer + elementIndex, default, this); 34 | } 35 | 36 | public override void Unpin() 37 | { 38 | } 39 | 40 | public void AllowReuse() 41 | { 42 | usingMemory = false; 43 | } 44 | 45 | public void Reset(byte* pointer, int length) 46 | { 47 | if (usingMemory) throw new InvalidOperationException("Memory is using, can not reset."); 48 | this.pointer = pointer; 49 | this.length = length; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /SngTool/SngLib/NativeByteArray/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Cysharp.Collections 6 | { 7 | internal static class ThrowHelper 8 | { 9 | [MethodImpl(MethodImplOptions.NoInlining)] 10 | #if !NETSTANDARD2_0 && !UNITY_2019_1_OR_NEWER 11 | [DoesNotReturn] 12 | #endif 13 | public static void ThrowIndexOutOfRangeException() 14 | { 15 | throw new IndexOutOfRangeException(); 16 | } 17 | 18 | [MethodImpl(MethodImplOptions.NoInlining)] 19 | #if !NETSTANDARD2_0 && !UNITY_2019_1_OR_NEWER 20 | [DoesNotReturn] 21 | #endif 22 | public static void ThrowArgumentOutOfRangeException(string paramName) 23 | { 24 | throw new ArgumentOutOfRangeException(paramName); 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SngTool/SngLib/SngFile.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Cysharp.Collections; 3 | 4 | namespace SngLib 5 | { 6 | public class SngFile : IDisposable 7 | { 8 | public const uint CurrentVersion = 1; 9 | public uint Version = CurrentVersion; 10 | public byte[] XorMask = new byte[16]; 11 | 12 | public bool metadataAvailable; 13 | 14 | public Dictionary Metadata = new(); 15 | public Dictionary Files = new(); 16 | 17 | public void AddFile(string fileName, NativeByteArray? data) 18 | { 19 | if (Files.TryAdd(fileName, data)) 20 | { 21 | Files[fileName] = data; 22 | } 23 | } 24 | 25 | public bool TryGetString(string key, out string value) 26 | { 27 | if (Metadata.TryGetValue(key, out var strVal)) 28 | { 29 | value = strVal!; 30 | return true; 31 | } 32 | else 33 | { 34 | value = string.Empty; 35 | return false; 36 | } 37 | } 38 | 39 | public bool TryGetInt(string key, out int value) 40 | { 41 | if (Metadata.TryGetValue(key, out var stringValue) && int.TryParse(stringValue, out int intValue)) 42 | { 43 | value = intValue; 44 | return true; 45 | } 46 | 47 | value = 0; 48 | return false; 49 | } 50 | 51 | public bool TryGetFloat(string key, out float value) 52 | { 53 | if (Metadata.TryGetValue(key, out var stringValue) && float.TryParse(stringValue, out float floatValue)) 54 | { 55 | value = floatValue; 56 | return true; 57 | } 58 | 59 | value = 0; 60 | return false; 61 | } 62 | 63 | public bool TryGetBool(string key, out bool value) 64 | { 65 | if (Metadata.TryGetValue(key, out var stringValue) && bool.TryParse(stringValue, out bool boolValue)) 66 | { 67 | value = boolValue; 68 | return true; 69 | } 70 | 71 | value = false; 72 | return false; 73 | } 74 | 75 | public string GetString(string key, string defaultValue) 76 | { 77 | if (TryGetString(key, out var value) && value != null) 78 | { 79 | return value; 80 | } 81 | else 82 | { 83 | return defaultValue; 84 | } 85 | } 86 | 87 | public int GetInt(string key, int defaultValue) 88 | { 89 | if (TryGetInt(key, out var value)) 90 | { 91 | return value; 92 | } 93 | else 94 | { 95 | return defaultValue; 96 | } 97 | } 98 | 99 | public float GetFloat(string key, float defaultValue) 100 | { 101 | if (TryGetFloat(key, out var value)) 102 | { 103 | return value; 104 | } 105 | else 106 | { 107 | return defaultValue; 108 | } 109 | } 110 | 111 | public bool GetBool(string key, bool defaultValue) 112 | { 113 | if (TryGetBool(key, out var value)) 114 | { 115 | return value; 116 | } 117 | else 118 | { 119 | return defaultValue; 120 | } 121 | } 122 | 123 | public void SetString(string key, string value) 124 | { 125 | if (!Metadata.TryAdd(key, value)) 126 | { 127 | Metadata[key] = value; 128 | } 129 | } 130 | 131 | public void SetInt(string key, int value) 132 | { 133 | var newVal = value.ToString(); 134 | if (!Metadata.TryAdd(key, newVal)) 135 | { 136 | Metadata[key] = newVal; 137 | } 138 | } 139 | 140 | public void SetFloat(string key, float value) 141 | { 142 | var newVal = value.ToString(); 143 | if (!Metadata.TryAdd(key, newVal)) 144 | { 145 | Metadata[key] = newVal; 146 | } 147 | } 148 | 149 | public void SetBool(string key, bool value) 150 | { 151 | var newVal = value.ToString(); 152 | if (!Metadata.TryAdd(key, newVal)) 153 | { 154 | Metadata[key] = newVal; 155 | } 156 | } 157 | 158 | public Dictionary GetRawMetadata() 159 | { 160 | // Make a copy of the internal dictionary 161 | return new Dictionary(Metadata); 162 | } 163 | 164 | public void Dispose() 165 | { 166 | foreach (var file in Files) 167 | { 168 | file.Value?.Dispose(); 169 | } 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /SngTool/SngLib/SngLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SngTool/SngTool.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.32228.343 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SngLib", "SngLib\SngLib.csproj", "{AB81A1E5-9044-485E-9B0D-6FFE1B5F424F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SngCli", "SngCli\SngCli.csproj", "{4B6B2C84-C516-47A1-B78F-EB465C006128}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SongLib", "SongLib\SongLib.csproj", "{F8CFEC77-2454-482C-9613-7C55935270F3}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLayer", "NLayer\NLayer.csproj", "{0D789C98-4957-473A-9753-BB97D0DC743D}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVorbis", "NVorbis\NVorbis.csproj", "{018993E2-EB63-4D66-84B0-0B5D0EC1D7C1}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {AB81A1E5-9044-485E-9B0D-6FFE1B5F424F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {AB81A1E5-9044-485E-9B0D-6FFE1B5F424F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {AB81A1E5-9044-485E-9B0D-6FFE1B5F424F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {AB81A1E5-9044-485E-9B0D-6FFE1B5F424F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {4B6B2C84-C516-47A1-B78F-EB465C006128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {4B6B2C84-C516-47A1-B78F-EB465C006128}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {4B6B2C84-C516-47A1-B78F-EB465C006128}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {4B6B2C84-C516-47A1-B78F-EB465C006128}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {F8CFEC77-2454-482C-9613-7C55935270F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {F8CFEC77-2454-482C-9613-7C55935270F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {F8CFEC77-2454-482C-9613-7C55935270F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {F8CFEC77-2454-482C-9613-7C55935270F3}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {0D789C98-4957-473A-9753-BB97D0DC743D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {0D789C98-4957-473A-9753-BB97D0DC743D}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {0D789C98-4957-473A-9753-BB97D0DC743D}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {0D789C98-4957-473A-9753-BB97D0DC743D}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {018993E2-EB63-4D66-84B0-0B5D0EC1D7C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {018993E2-EB63-4D66-84B0-0B5D0EC1D7C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {018993E2-EB63-4D66-84B0-0B5D0EC1D7C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {018993E2-EB63-4D66-84B0-0B5D0EC1D7C1}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {9DA3557A-98F2-429A-9C79-29C0B7371CC6} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /SngTool/SongLib/FormatDetection/OggParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using BinaryEx; 4 | using System.Text; 5 | 6 | public enum OggEncoding 7 | { 8 | Vorbis, 9 | Opus, 10 | Flac 11 | } 12 | 13 | public static class OggParser 14 | { 15 | private const string OggStr = "OggS"; 16 | private const string VorbisStr = "vorbis"; 17 | private const string OpusHeadStr = "OpusHead"; 18 | private const string FlacStr = "FLAC"; 19 | 20 | /// 21 | /// Determines whether the given stream is an Ogg file by comparing the file header with the Ogg magic number. 22 | /// 23 | /// The stream to check. 24 | /// true if the stream is an Ogg file; otherwise, false. 25 | public static bool IsOggFile(Stream stream) 26 | { 27 | var position = stream.Position; 28 | Span headerBytes = stackalloc byte[4]; 29 | stream.Read(headerBytes); 30 | 31 | Span oggSBytes = stackalloc byte[4]; 32 | Encoding.ASCII.GetBytes(OggStr, oggSBytes); 33 | 34 | return headerBytes.SequenceEqual(oggSBytes); 35 | } 36 | 37 | /// 38 | /// Determines whether the given stream is encoded in the specified Ogg format. 39 | /// 40 | /// The stream to check. 41 | /// The Ogg encoding format to compare with. 42 | /// true if the stream is encoded in the specified Ogg format; otherwise, false. 43 | public static bool IsOggEncoding(Stream stream, OggEncoding format, string fileName) 44 | { 45 | var position = stream.Position; 46 | try 47 | { 48 | var isOgg = IsOggFile(stream); 49 | if (isOgg) 50 | { 51 | var version = stream.ReadByte(); 52 | var headerType = stream.ReadByte(); 53 | var granulePosition = stream.ReadInt64LE(); 54 | var bitStreamSerial = stream.ReadInt32LE(); 55 | var pageSequenceNumber = stream.ReadInt32LE(); 56 | var checksum = stream.ReadUInt32LE(); 57 | var pageSegments = stream.ReadByte(); 58 | 59 | Span segmentLengthTable = stackalloc byte[pageSegments]; 60 | stream.ReadCountLE(segmentLengthTable); 61 | 62 | var totalPageLength = 0; 63 | 64 | for (int i = 0; i < pageSegments; i++) 65 | { 66 | totalPageLength += segmentLengthTable[i]; 67 | } 68 | 69 | if (format == OggEncoding.Vorbis) 70 | { 71 | var packType = stream.ReadByte(); 72 | Span vorbisBytes = stackalloc byte[6]; 73 | Encoding.ASCII.GetBytes(VorbisStr, vorbisBytes); 74 | 75 | Span vorbisIdBytes = stackalloc byte[6]; 76 | stream.ReadCountLE(vorbisIdBytes); 77 | 78 | return vorbisIdBytes.SequenceEqual(vorbisBytes); 79 | } 80 | else if (format == OggEncoding.Opus) 81 | { 82 | Span opusHeadBytes = stackalloc byte[8]; 83 | Encoding.ASCII.GetBytes(OpusHeadStr, opusHeadBytes); 84 | 85 | Span opusIdBytes = stackalloc byte[8]; 86 | stream.ReadCountLE(opusIdBytes); 87 | 88 | return opusIdBytes.SequenceEqual(opusHeadBytes); 89 | } 90 | else if (format == OggEncoding.Flac) 91 | { 92 | var typeId = stream.ReadByte(); 93 | Span flacBytes = stackalloc byte[4]; 94 | Encoding.ASCII.GetBytes(FlacStr, flacBytes); 95 | 96 | Span flacIdBytes = stackalloc byte[4]; 97 | stream.ReadCountLE(flacIdBytes); 98 | 99 | return typeId == 0x7F && flacIdBytes.SequenceEqual(flacBytes); 100 | } 101 | else 102 | { 103 | return false; 104 | } 105 | 106 | } 107 | else 108 | { 109 | return false; 110 | } 111 | } 112 | finally 113 | { 114 | stream.Position = position; 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /SngTool/SongLib/FormatDetection/WavParser.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using BinaryEx; 6 | 7 | public class WavParser 8 | { 9 | private const int HeaderSize = 44 + 40; // header size plus extra for junk sections 10 | private const string RiffIdentifier = "RIFF"; 11 | private const string WaveIdentifier = "WAVE"; 12 | private const string FmtIdentifier = "fmt "; 13 | private const string JunkIdentifier = "JUNK"; 14 | private const string BextIdentifier = "bext"; 15 | 16 | private readonly static byte[] RiffIdentifierBytes = Encoding.ASCII.GetBytes(RiffIdentifier); 17 | private readonly static byte[] WaveIdentifierBytes = Encoding.ASCII.GetBytes(WaveIdentifier); 18 | private readonly static byte[] FmtIdentifierBytes = Encoding.ASCII.GetBytes(FmtIdentifier); 19 | private readonly static byte[] JunkIdentifierBytes = Encoding.ASCII.GetBytes(JunkIdentifier); 20 | private readonly static byte[] BextIdentifierBytes = Encoding.ASCII.GetBytes(BextIdentifier); 21 | 22 | 23 | private static void SkipChunk(ref int pos, byte[] header) 24 | { 25 | int junkChunkSize = header.ReadInt32LE(ref pos); 26 | // Odd chunk sizes are padded to even 27 | if ((junkChunkSize & 1) != 0) 28 | { 29 | junkChunkSize++; 30 | } 31 | pos += junkChunkSize; 32 | } 33 | 34 | public static bool IsWav(Stream stream, string filePath) 35 | { 36 | try 37 | { 38 | byte[] header = new byte[HeaderSize]; 39 | int bytesRead = stream.Read(header, 0, HeaderSize); 40 | 41 | int pos = 0; 42 | 43 | // Check if the file starts with the "RIFF" chunk identifier 44 | Span riffHeaderBytes = header.AsSpan(0, RiffIdentifier.Length); 45 | Span riffIdentifierBytes = RiffIdentifierBytes; 46 | if (!riffIdentifierBytes.SequenceEqual(riffHeaderBytes)) 47 | { 48 | return false; 49 | } 50 | 51 | // Parse the RIFF header 52 | int fileSize = header.ReadInt32LE(ref pos); 53 | 54 | Span wavIdBytes = stackalloc byte[WaveIdentifier.Length]; 55 | header.ReadCountLE(ref pos, wavIdBytes); 56 | 57 | Span waveIdentifierBytes = WaveIdentifierBytes; 58 | if (!waveIdentifierBytes.SequenceEqual(wavIdBytes)) 59 | { 60 | return false; 61 | } 62 | 63 | Span fmtIdBytes = stackalloc byte[FmtIdentifier.Length]; 64 | header.ReadCountLE(ref pos, fmtIdBytes); 65 | 66 | Span junkIdentifierBytes = JunkIdentifierBytes; 67 | if (fmtIdBytes.SequenceEqual(junkIdentifierBytes)) 68 | { 69 | // Skip the junk chunk 70 | SkipChunk(ref pos, header); 71 | header.ReadCountLE(ref pos, fmtIdBytes); 72 | } 73 | 74 | Span bextIdentifierBytes = BextIdentifierBytes; 75 | if (fmtIdBytes.SequenceEqual(bextIdentifierBytes)) 76 | { 77 | // Skip the bext chunk 78 | SkipChunk(ref pos, header); 79 | header.ReadCountLE(ref pos, fmtIdBytes); 80 | } 81 | 82 | // Parse the fmt chunk 83 | Span fmtIdentifierBytes = FmtIdentifierBytes; 84 | if (!fmtIdBytes.SequenceEqual(fmtIdentifierBytes)) 85 | { 86 | return false; 87 | } 88 | 89 | int fmtChunkSize = header.ReadInt32LE(ref pos); 90 | 91 | // opusenc only supports 16 byte chunk sizes and larger 92 | // While there is technically a 14 byte chunk in older files, it is not supported by opusenc 93 | if (fmtChunkSize < 16) 94 | { 95 | return false; 96 | } 97 | 98 | // Parse the WAV header 99 | ushort audioFormat = header.ReadUInt16LE(ref pos); 100 | ushort numChannels = header.ReadUInt16LE(ref pos); 101 | uint sampleRate = header.ReadUInt32LE(ref pos); 102 | uint byteRate = header.ReadUInt32LE(ref pos); 103 | ushort blockAlign = header.ReadUInt16LE(ref pos); 104 | ushort bitsPerSample = header.ReadUInt16LE(ref pos); 105 | ushort cbSize = 0; 106 | 107 | if (fmtChunkSize >= 18) 108 | { 109 | cbSize = header.ReadUInt16LE(ref pos); 110 | } 111 | 112 | if (audioFormat == 0xFFFEu && fmtChunkSize >= 40) // WAVE_FORMAT_EXTENSIBLE 113 | { 114 | var samples = header.ReadUInt16LE(ref pos); 115 | var channelMask = header.ReadUInt32LE(ref pos); 116 | int a = header.ReadInt32LE(ref pos); 117 | short b = header.ReadInt16LE(ref pos); 118 | short c = header.ReadInt16LE(ref pos); 119 | 120 | Span guidDtoEBytes = stackalloc byte[8]; 121 | header.ReadCountLE(ref pos, guidDtoEBytes); 122 | 123 | audioFormat = (ushort)a; // first segment of GUID is the audio format 124 | } 125 | 126 | if (audioFormat == 1 || audioFormat == 3) 127 | { 128 | return true; 129 | } 130 | 131 | return false; 132 | } 133 | finally 134 | { 135 | stream.Seek(0, SeekOrigin.Begin); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /SngTool/SongLib/JpegEncoding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cysharp.Collections; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.Formats.Jpeg; 5 | using SixLabors.ImageSharp.Processing; 6 | 7 | namespace SongLib 8 | { 9 | public static class JpegEncoding 10 | { 11 | public enum SizeTiers 12 | { 13 | None = 0, 14 | Nearest = 1, 15 | Size256x256 = 256, 16 | Size384x384 = 384, 17 | Size512x512 = 512, 18 | Size768x768 = 768, 19 | Size1024x1024 = 1024, 20 | Size1536x1536 = 1536, 21 | Size2048x2048 = 2048, 22 | } 23 | 24 | private static SizeTiers FindNewSize(int originalSize) 25 | { 26 | if (originalSize < 256) 27 | { 28 | return SizeTiers.None; 29 | } 30 | else if (originalSize < 384) 31 | { 32 | return SizeTiers.Size256x256; 33 | } 34 | else if (originalSize < 512) 35 | { 36 | return SizeTiers.Size384x384; 37 | } 38 | else if (originalSize < 768) 39 | { 40 | return SizeTiers.Size512x512; 41 | } 42 | else if (originalSize < 1024) 43 | { 44 | return SizeTiers.Size768x768; 45 | } 46 | else if (originalSize < 1536) 47 | { 48 | return SizeTiers.Size1024x1024; 49 | } 50 | else if (originalSize < 2048) 51 | { 52 | return SizeTiers.Size1536x1536; 53 | } 54 | else // anything larger clamp to 2048 55 | { 56 | return SizeTiers.Size2048x2048; 57 | } 58 | } 59 | 60 | private static int CalculateFinalSize(SizeTiers sizeOption, bool upscale, int imageSize) 61 | { 62 | // rescaling forces the use of a specific resolution 63 | if (upscale && sizeOption > SizeTiers.Nearest) 64 | { 65 | return (int)sizeOption; 66 | } 67 | 68 | if (sizeOption == SizeTiers.Nearest) 69 | { 70 | var nearestSize = FindNewSize(imageSize); 71 | 72 | if (nearestSize == SizeTiers.None) 73 | { 74 | return imageSize; 75 | } 76 | 77 | return (int)nearestSize; 78 | } 79 | else if (sizeOption > SizeTiers.Nearest) 80 | { 81 | // clamp size to max specified 82 | if (imageSize >= (int)sizeOption) 83 | { 84 | return (int)sizeOption; 85 | } 86 | // If we are less than the requested size just use the original size 87 | else 88 | { 89 | return imageSize; 90 | } 91 | } 92 | else 93 | { 94 | return imageSize; 95 | } 96 | } 97 | 98 | /// 99 | /// Encodes image to jpeg with resizing to nearest supported resolution 100 | /// 101 | /// The supported resolutions have been chosen to be divisible 102 | /// by 4 and mostly are power of twos with a few between them to even out the range. 103 | /// resizing is automatically disabled if the image is not the same width/height 104 | /// 105 | /// File path of input image 106 | /// Image quality level 107 | /// Enables image rescaling 108 | /// Resize images to specific sizes or the nearest option lower 109 | /// byte array of new image 110 | public async static Task<(string fileName, NativeByteArray?)> EncodeImageToJpeg(string filePath, int quality = 75, bool upscale = false, SizeTiers size = SizeTiers.Size512x512) 111 | { 112 | var output = new NativeByteArray(skipZeroClear: true); 113 | 114 | try 115 | { 116 | using (var file = File.OpenRead(filePath)) 117 | using (var image = await Image.LoadAsync(file)) 118 | { 119 | // Don't resize if it's not square 120 | if (image.Height == image.Width && size != SizeTiers.None) 121 | { 122 | var sizeVal = CalculateFinalSize(size, upscale, image.Height); 123 | image.Mutate(x => x.Resize(sizeVal, sizeVal, KnownResamplers.CatmullRom)); 124 | } 125 | 126 | JpegEncoder encoder = new JpegEncoder 127 | { 128 | Quality = quality, 129 | ColorType = JpegEncodingColor.Rgb, 130 | SkipMetadata = true 131 | }; 132 | var jpgStream = output.AsStream(FileAccess.ReadWrite); 133 | await image.SaveAsJpegAsync(jpgStream, encoder); 134 | output.Resize(jpgStream.Position); 135 | 136 | if (upscale || output.Length < file.Length) 137 | { 138 | var name = Path.GetFileNameWithoutExtension(filePath); 139 | return ($"{name}.jpg", output); 140 | } 141 | } 142 | } 143 | catch (Exception e) 144 | { 145 | Console.WriteLine($"Error encoding {filePath}, falling back to original image. {e}"); 146 | } 147 | 148 | // Return original file if it's smaller in size or if we encountered an error 149 | return (Path.GetFileName(filePath), await LargeFile.ReadAllBytesAsync(filePath)); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /SngTool/SongLib/SongLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | true 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /SngTool/SongLib/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SongLib 7 | { 8 | public static class Utils 9 | { 10 | private static int defaultThreads = Math.Max(Environment.ProcessorCount - 3, 1); 11 | public async static Task ForEachAsync(IEnumerable source, Func body, int threads = -1) 12 | { 13 | if (threads < 0) 14 | { 15 | threads = defaultThreads; 16 | } 17 | 18 | var options = new ParallelOptions 19 | { 20 | MaxDegreeOfParallelism = threads 21 | }; 22 | 23 | await Parallel.ForEachAsync(source, options, body); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /SngTool/SongLib/WavFileWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Buffers.Binary; 4 | using System.Text; 5 | using BinaryEx; 6 | 7 | namespace SongLib 8 | { 9 | public class WavFileWriter : Stream 10 | { 11 | public const ushort BitsPerSample = 32; 12 | private const ushort ChannelSize = BitsPerSample / 8; // 8 bits per byte 13 | public readonly int SampleRate; 14 | public readonly ushort Channels; 15 | public readonly long TotalSamples; 16 | public readonly long TotalSize; 17 | public uint SamplesWritten = 0; 18 | 19 | public delegate int IngestSamplesDelegate(Span audioSamples); 20 | 21 | private IngestSamplesDelegate ingestCallback; 22 | private Func remainingCallback; 23 | 24 | private const int HeaderSize = 44; 25 | 26 | private byte[] header = new byte[HeaderSize]; 27 | private long streamPos = 0; 28 | 29 | public WavFileWriter(IngestSamplesDelegate ingestCallback, Func remainingCallback, int sampleRate, ushort channels, long totalSamples) 30 | { 31 | Channels = channels; 32 | TotalSamples = totalSamples; 33 | SampleRate = sampleRate; 34 | this.ingestCallback = ingestCallback; 35 | this.remainingCallback = remainingCallback; 36 | 37 | var sampleSize = (int)(totalSamples * ChannelSize); 38 | TotalSize = sampleSize + HeaderSize; 39 | 40 | Span headerSpan = new Span(header); 41 | 42 | // write file header 43 | CreateWavHeader(headerSpan, sampleRate, BitsPerSample, channels, sampleSize); 44 | } 45 | 46 | private static void WriteFourCC(Span buffer, ref int offset, string fourCC) 47 | { 48 | if (fourCC.Length != 4) 49 | { 50 | throw new ArgumentException("The length of the fourCC string must be exactly 4 characters."); 51 | } 52 | 53 | if (buffer.Length < offset + 4) 54 | { 55 | throw new ArgumentException("The buffer is too small to write the fourCC string."); 56 | } 57 | 58 | offset += Encoding.ASCII.GetBytes(fourCC, buffer.Slice(offset, 4)); 59 | } 60 | 61 | private void CreateWavHeader(Span buffer, int sampleRate, int bitsPerSample, int channels, int dataSize) 62 | { 63 | int byteRate = sampleRate * channels * (bitsPerSample / 8); 64 | 65 | int offset = 0; 66 | 67 | // RIFF chunk 68 | WriteFourCC(buffer, ref offset, "RIFF"); 69 | buffer.WriteInt32LE(ref offset, (HeaderSize - 8) + dataSize); // Chunk size 70 | WriteFourCC(buffer, ref offset, "WAVE"); 71 | 72 | // fmt sub-chunk 73 | WriteFourCC(buffer, ref offset, "fmt "); // Sub-chunk ID 74 | buffer.WriteInt32LE(ref offset, 16); // Sub-chunk size 75 | buffer.WriteInt16LE(ref offset, 3); // Audio format (3 for float) 76 | buffer.WriteInt16LE(ref offset, (short)channels); 77 | buffer.WriteInt32LE(ref offset, sampleRate); 78 | buffer.WriteInt32LE(ref offset, byteRate); 79 | buffer.WriteInt16LE(ref offset, (short)(channels * (bitsPerSample / 8))); // Block align 80 | buffer.WriteInt16LE(ref offset, (short)bitsPerSample); 81 | 82 | // data sub-chunk 83 | WriteFourCC(buffer, ref offset, "data"); 84 | buffer.WriteInt32LE(ref offset, dataSize); // Sub-chunk size 85 | } 86 | 87 | public bool Completed => SamplesWritten >= TotalSamples; 88 | public int RemainingSamples => (int)(TotalSamples - SamplesWritten); 89 | 90 | public override bool CanRead => true; 91 | 92 | public override bool CanSeek => false; 93 | 94 | public override bool CanWrite => false; 95 | 96 | public override long Length => TotalSize; 97 | 98 | public override long Position { get => streamPos; set => throw new NotImplementedException(); } 99 | 100 | private float[] sampleBuffer = new float[8192]; 101 | 102 | private bool HasExaustedSouce => remainingCallback() == 0; 103 | 104 | public override int Read(byte[] buffer, int offset, int count) 105 | { 106 | if (streamPos < 44) 107 | { 108 | int headerBytes = Math.Min(44, count); 109 | Array.Copy(header, streamPos, buffer, offset, headerBytes); 110 | streamPos += headerBytes; 111 | return headerBytes; 112 | } 113 | else if (!HasExaustedSouce && SamplesWritten < TotalSamples) 114 | { 115 | int samplesToRead = Math.Min(RemainingSamples, count / ChannelSize); 116 | 117 | int maxBufferSamples = sampleBuffer.Length; 118 | 119 | int injestCount = samplesToRead / maxBufferSamples; 120 | 121 | int samplesRemaining = samplesToRead; 122 | while (samplesRemaining > 0 && !HasExaustedSouce) 123 | { 124 | int sampleCount = Math.Min(maxBufferSamples, samplesRemaining); 125 | int samplesRead = ingestCallback.Invoke(sampleBuffer.AsSpan(0, sampleCount)); 126 | 127 | if (samplesRead == 0) 128 | { 129 | // Console.WriteLine($"No samples read {samplesRemaining} samples but {remainingCallback()} samples remaining in the source."); 130 | break; 131 | } 132 | 133 | var endPos = streamPos + samplesRead; 134 | 135 | // If end pos too long clamp to max size 136 | // typically the last samples will be empty for most music anyways 137 | if (endPos > TotalSize) 138 | { 139 | Console.WriteLine($"End pos {endPos} is greater than total size {TotalSize} clamping to total size."); 140 | var validSampleCount = (TotalSize - streamPos) / ChannelSize; 141 | samplesRead = (int)validSampleCount; 142 | } 143 | samplesRemaining -= samplesRead; 144 | SamplesWritten += (uint)samplesRead; 145 | // Console.WriteLine($"Writing {samplesRead} samples of total {count / ChannelSize} to output"); 146 | 147 | buffer.WriteCountLE(ref offset, sampleBuffer.AsSpan(0, samplesRead)); 148 | 149 | if (endPos > TotalSize) 150 | { 151 | break; 152 | } 153 | } 154 | 155 | if (HasExaustedSouce && RemainingSamples > 0) 156 | { 157 | Console.WriteLine($"Exausted source but still have {samplesRemaining} samples remaining in this read but {RemainingSamples} samples overall. Filling with 0s."); 158 | for (int i = 0; i < samplesRemaining; i++) 159 | { 160 | buffer.WriteFloatLE(ref offset, 0); 161 | } 162 | } 163 | else if (HasExaustedSouce) 164 | { 165 | return 0; 166 | } 167 | 168 | int byteCount = (samplesToRead - samplesRemaining) * ChannelSize; 169 | streamPos += byteCount; 170 | 171 | return byteCount; 172 | } 173 | return 0; 174 | } 175 | 176 | // Required stream methods that are not implemented 177 | public override void Flush() 178 | { 179 | } 180 | 181 | public override long Seek(long offset, SeekOrigin origin) 182 | { 183 | throw new NotImplementedException(); 184 | } 185 | 186 | public override void SetLength(long value) 187 | { 188 | throw new NotImplementedException(); 189 | } 190 | 191 | public override void Write(byte[] buffer, int offset, int count) 192 | { 193 | throw new NotImplementedException(); 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /SngTool/SongLib/deps/opusenc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdsitton/SngFileFormat/668379942a3790c1b47ddf56b71eb3a7ada449c2/SngTool/SongLib/deps/opusenc.exe --------------------------------------------------------------------------------