├── .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
--------------------------------------------------------------------------------