From 79a7437cd84fa1d3cb0d54e0abc5ed4958754d62 Mon Sep 17 00:00:00 2001 From: Berk Date: Sat, 11 Jul 2026 19:14:08 +0300 Subject: [PATCH] [GUI] Added Atrac9 audio decoder and improved GUI with audio preview and controller support (#64) * [GUI] Added Atrac9 audio decoder and improved GUI with audio preview and controller support * fix: package.lock.json for SharpEmu.CLI to match the other projects * fix: packages.lock.json file to include new dependencies for GUI improvements * rollForward: "disable" --- LICENSES/MIT.txt | 21 + REUSE.toml | 6 + global.json | 2 +- src/SharpEmu.CLI/Program.cs | 43 + src/SharpEmu.CLI/packages.lock.json | 9 +- src/SharpEmu.GUI/Atrac9/Atrac9Config.cs | 120 ++ src/SharpEmu.GUI/Atrac9/Atrac9Decoder.cs | 128 ++ src/SharpEmu.GUI/Atrac9/Atrac9Rng.cs | 34 + src/SharpEmu.GUI/Atrac9/BandExtension.cs | 373 +++++ src/SharpEmu.GUI/Atrac9/BitAllocation.cs | 141 ++ src/SharpEmu.GUI/Atrac9/Block.cs | 92 ++ src/SharpEmu.GUI/Atrac9/Channel.cs | 49 + src/SharpEmu.GUI/Atrac9/ChannelConfig.cs | 34 + src/SharpEmu.GUI/Atrac9/Frame.cs | 21 + src/SharpEmu.GUI/Atrac9/HuffmanCodebook.cs | 63 + src/SharpEmu.GUI/Atrac9/HuffmanCodebooks.cs | 1353 +++++++++++++++++ src/SharpEmu.GUI/Atrac9/LICENSE.txt | 21 + src/SharpEmu.GUI/Atrac9/Quantization.cs | 58 + src/SharpEmu.GUI/Atrac9/ScaleFactors.cs | 172 +++ src/SharpEmu.GUI/Atrac9/Stereo.cs | 34 + src/SharpEmu.GUI/Atrac9/Tables.cs | 116 ++ src/SharpEmu.GUI/Atrac9/Unpack.cs | 426 ++++++ src/SharpEmu.GUI/Atrac9/Utilities/Bit.cs | 23 + .../Atrac9/Utilities/BitReader.cs | 133 ++ src/SharpEmu.GUI/Atrac9/Utilities/Helpers.cs | 51 + src/SharpEmu.GUI/Atrac9/Utilities/Mdct.cs | 178 +++ src/SharpEmu.GUI/GuiSettings.cs | 6 + src/SharpEmu.GUI/MainWindow.axaml | 10 +- src/SharpEmu.GUI/MainWindow.axaml.cs | 211 ++- src/SharpEmu.GUI/SharpEmu.GUI.csproj | 15 +- src/SharpEmu.GUI/SndPreviewPlayer.cs | 301 ++++ src/SharpEmu.GUI/packages.lock.json | 3 + src/SharpEmu.Libs/Pad/DualSenseReader.cs | 83 +- src/SharpEmu.Libs/Pad/PadExports.cs | 18 +- src/SharpEmu.Libs/Pad/PadState.cs | 40 + src/SharpEmu.Libs/Pad/XInputReader.cs | 246 +++ src/SharpEmu.Logging/SharpEmuLog.cs | 44 +- 37 files changed, 4601 insertions(+), 77 deletions(-) create mode 100644 LICENSES/MIT.txt create mode 100644 src/SharpEmu.GUI/Atrac9/Atrac9Config.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Atrac9Decoder.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Atrac9Rng.cs create mode 100644 src/SharpEmu.GUI/Atrac9/BandExtension.cs create mode 100644 src/SharpEmu.GUI/Atrac9/BitAllocation.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Block.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Channel.cs create mode 100644 src/SharpEmu.GUI/Atrac9/ChannelConfig.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Frame.cs create mode 100644 src/SharpEmu.GUI/Atrac9/HuffmanCodebook.cs create mode 100644 src/SharpEmu.GUI/Atrac9/HuffmanCodebooks.cs create mode 100644 src/SharpEmu.GUI/Atrac9/LICENSE.txt create mode 100644 src/SharpEmu.GUI/Atrac9/Quantization.cs create mode 100644 src/SharpEmu.GUI/Atrac9/ScaleFactors.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Stereo.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Tables.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Unpack.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Utilities/Bit.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Utilities/BitReader.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Utilities/Helpers.cs create mode 100644 src/SharpEmu.GUI/Atrac9/Utilities/Mdct.cs create mode 100644 src/SharpEmu.GUI/SndPreviewPlayer.cs create mode 100644 src/SharpEmu.Libs/Pad/PadState.cs create mode 100644 src/SharpEmu.Libs/Pad/XInputReader.cs diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..2b5cc1f --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Alex Barney + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/REUSE.toml b/REUSE.toml index c1b2aac..09a59a9 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -15,3 +15,9 @@ path = [ precedence = "aggregate" SPDX-FileCopyrightText = "SharpEmu Emulator Project" SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "src/SharpEmu.GUI/Atrac9/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2018 Alex Barney" +SPDX-License-Identifier = "MIT" diff --git a/global.json b/global.json index abe5820..363e974 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "version": "10.0.103", - "rollForward": "latestFeature" + "rollForward": "disable" } } diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs index 14229b3..43ae5e5 100644 --- a/src/SharpEmu.CLI/Program.cs +++ b/src/SharpEmu.CLI/Program.cs @@ -60,6 +60,7 @@ internal static partial class Program // The executable uses the GUI subsystem, so CLI mode has to connect // itself to a console before the first write. EnsureCliConsole(); + UseUtf8ConsoleOutput(); Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args"); @@ -162,6 +163,48 @@ internal static partial class Program RebindStdHandleToConsole(STD_ERROR_HANDLE); } + /// + /// Makes console writes UTF-8 so the GUI's pipe reader (and any modern + /// terminal) decodes non-ASCII text correctly. Without this, redirected + /// output falls back to the OS ANSI code page and characters like "—" + /// arrive mangled. + /// + private static void UseUtf8ConsoleOutput() + { + try + { + // Also recreates the redirected Console.Out/Error writers with + // the new encoding. + Console.OutputEncoding = Encoding.UTF8; + return; + } + catch (Exception) + { + // No attached console (GUI-subsystem child with piped output): + // wrap the raw handles instead. + } + + if (Console.IsOutputRedirected) + { + Console.SetOut(new StreamWriter( + Console.OpenStandardOutput(), + new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) + { + AutoFlush = true, + }); + } + + if (Console.IsErrorRedirected) + { + Console.SetError(new StreamWriter( + Console.OpenStandardError(), + new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) + { + AutoFlush = true, + }); + } + } + private static void RebindStdHandleToConsole(int stdHandle) { if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0) diff --git a/src/SharpEmu.CLI/packages.lock.json b/src/SharpEmu.CLI/packages.lock.json index b4deaea..a06b7f9 100644 --- a/src/SharpEmu.CLI/packages.lock.json +++ b/src/SharpEmu.CLI/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.9, )", - "resolved": "10.0.9", - "contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg==" + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA==" }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", @@ -211,6 +211,7 @@ "Avalonia.Desktop": "[11.3.18, )", "Avalonia.Fonts.Inter": "[11.3.18, )", "Avalonia.Themes.Fluent": "[11.3.18, )", + "SharpEmu.Logging": "[1.0.0, )", "Tmds.DBus.Protocol": "[0.21.3, )" } }, @@ -484,4 +485,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/SharpEmu.GUI/Atrac9/Atrac9Config.cs b/src/SharpEmu.GUI/Atrac9/Atrac9Config.cs new file mode 100644 index 0000000..8498fd8 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Atrac9Config.cs @@ -0,0 +1,120 @@ +#nullable disable +using System.IO; +using LibAtrac9.Utilities; + +namespace LibAtrac9 +{ + /// + /// Stores the configuration data needed to decode or encode an ATRAC9 stream. + /// + public class Atrac9Config + { + /// + /// The 4-byte ATRAC9 configuration data. + /// + public byte[] ConfigData { get; } + + /// + /// A 4-bit value specifying one of 16 sample rates. + /// + public int SampleRateIndex { get; } + /// + /// A 3-bit value specifying one of 6 substream channel mappings. + /// + public int ChannelConfigIndex { get; } + /// + /// An 11-bit value containing the average size of a single frame. + /// + public int FrameBytes { get; } + /// + /// A 2-bit value indicating how many frames are in each superframe. + /// + public int SuperframeIndex { get; } + + /// + /// The channel mapping used by the ATRAC9 stream. + /// + public ChannelConfig ChannelConfig { get; } + /// + /// The total number of channels in the ATRAC9 stream. + /// + public int ChannelCount { get; } + /// + /// The sample rate of the ATRAC9 stream. + /// + public int SampleRate { get; } + /// + /// Indicates whether the ATRAC9 stream has a of 8 or above. + /// + public bool HighSampleRate { get; } + + /// + /// The number of frames in each superframe. + /// + public int FramesPerSuperframe { get; } + /// + /// The number of samples in one frame as an exponent of 2. + /// = 2^. + /// + public int FrameSamplesPower { get; } + /// + /// The number of samples in one frame. + /// + public int FrameSamples { get; } + /// + /// The number of bytes in one superframe. + /// + public int SuperframeBytes { get; } + /// + /// The number of samples in one superframe. + /// + public int SuperframeSamples { get; } + + /// + /// Reads ATRAC9 configuration data and calculates the stream parameters from it. + /// + /// The processed ATRAC9 configuration. + public Atrac9Config(byte[] configData) + { + if (configData == null || configData.Length != 4) + { + throw new InvalidDataException("Config data must be 4 bytes long"); + } + + ReadConfigData(configData, out int a, out int b, out int c, out int d); + SampleRateIndex = a; + ChannelConfigIndex = b; + FrameBytes = c; + SuperframeIndex = d; + ConfigData = configData; + + FramesPerSuperframe = 1 << SuperframeIndex; + SuperframeBytes = FrameBytes << SuperframeIndex; + ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex]; + + ChannelCount = ChannelConfig.ChannelCount; + SampleRate = Tables.SampleRates[SampleRateIndex]; + HighSampleRate = SampleRateIndex > 7; + FrameSamplesPower = Tables.SamplingRateIndexToFrameSamplesPower[SampleRateIndex]; + FrameSamples = 1 << FrameSamplesPower; + SuperframeSamples = FrameSamples * FramesPerSuperframe; + } + + private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex) + { + var reader = new BitReader(configData); + + int header = reader.ReadInt(8); + sampleRateIndex = reader.ReadInt(4); + channelConfigIndex = reader.ReadInt(3); + int validationBit = reader.ReadInt(1); + frameBytes = reader.ReadInt(11) + 1; + superframeIndex = reader.ReadInt(2); + + if (header != 0xFE || validationBit != 0) + { + throw new InvalidDataException("ATRAC9 Config Data is invalid"); + } + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Atrac9Decoder.cs b/src/SharpEmu.GUI/Atrac9/Atrac9Decoder.cs new file mode 100644 index 0000000..e99d451 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Atrac9Decoder.cs @@ -0,0 +1,128 @@ +#nullable disable +using System; +using LibAtrac9.Utilities; + +namespace LibAtrac9 +{ + /// + /// Decodes an ATRAC9 stream into 16-bit PCM. + /// + public class Atrac9Decoder + { + /// + /// The config data for the current ATRAC9 stream. + /// + public Atrac9Config Config { get; private set; } + + private Frame Frame { get; set; } + private BitReader Reader { get; set; } + private bool _initialized; + + /// + /// Sets up the decoder to decode an ATRAC9 stream based on the information in . + /// + /// A 4-byte value containing information about the ATRAC9 stream. + public void Initialize(byte[] configData) + { + Config = new Atrac9Config(configData); + Frame = new Frame(Config); + Reader = new BitReader(null); + _initialized = true; + } + + /// + /// Decodes one superframe of ATRAC9 data. + /// + /// The ATRAC9 data to decode. The array must be at least + /// . bytes long. + /// A buffer that the decoded PCM data will be placed in. + /// The array must have dimensions of at least [.] + /// [.]. + public void Decode(byte[] atrac9Data, short[][] pcmOut) + { + if (!_initialized) throw new InvalidOperationException("Decoder must be initialized before decoding."); + + ValidateDecodeBuffers(atrac9Data, pcmOut); + Reader.SetBuffer(atrac9Data); + DecodeSuperFrame(pcmOut); + } + + private void ValidateDecodeBuffers(byte[] atrac9Buffer, short[][] pcmBuffer) + { + if (atrac9Buffer == null) throw new ArgumentNullException(nameof(atrac9Buffer)); + if (pcmBuffer == null) throw new ArgumentNullException(nameof(pcmBuffer)); + + if (atrac9Buffer.Length < Config.SuperframeBytes) + { + throw new ArgumentException("ATRAC9 buffer is too small"); + } + + if (pcmBuffer.Length < Config.ChannelCount) + { + throw new ArgumentException("PCM buffer is too small"); + } + + for (int i = 0; i < Config.ChannelCount; i++) + { + if (pcmBuffer[i]?.Length < Config.SuperframeSamples) + { + throw new ArgumentException("PCM buffer is too small"); + } + } + } + + private void DecodeSuperFrame(short[][] pcmOut) + { + for (int i = 0; i < Config.FramesPerSuperframe; i++) + { + Frame.FrameIndex = i; + DecodeFrame(Reader, Frame); + PcmFloatToShort(pcmOut, i * Config.FrameSamples); + Reader.AlignPosition(8); + } + } + + private void PcmFloatToShort(short[][] pcmOut, int start) + { + int endSample = start + Config.FrameSamples; + int channelNum = 0; + foreach (Block block in Frame.Blocks) + { + foreach (Channel channel in block.Channels) + { + double[] pcmSrc = channel.Pcm; + short[] pcmDest = pcmOut[channelNum++]; + for (int d = 0, s = start; s < endSample; d++, s++) + { + double sample = pcmSrc[d]; + // Not using Math.Round because it's ~20x slower on 64-bit + int roundedSample = (int)Math.Floor(sample + 0.5); + pcmDest[s] = Helpers.Clamp16(roundedSample); + } + } + } + } + + private static void DecodeFrame(BitReader reader, Frame frame) + { + Unpack.UnpackFrame(reader, frame); + + foreach (Block block in frame.Blocks) + { + Quantization.DequantizeSpectra(block); + Stereo.ApplyIntensityStereo(block); + Quantization.ScaleSpectrum(block); + BandExtension.ApplyBandExtension(block); + ImdctBlock(block); + } + } + + private static void ImdctBlock(Block block) + { + foreach (Channel channel in block.Channels) + { + channel.Mdct.RunImdct(channel.Spectra, channel.Pcm); + } + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Atrac9Rng.cs b/src/SharpEmu.GUI/Atrac9/Atrac9Rng.cs new file mode 100644 index 0000000..90b13c9 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Atrac9Rng.cs @@ -0,0 +1,34 @@ +#nullable disable +namespace LibAtrac9 +{ + /// + /// An Xorshift RNG used by the ATRAC9 codec + /// + internal class Atrac9Rng + { + private ushort _stateA; + private ushort _stateB; + private ushort _stateC; + private ushort _stateD; + + public Atrac9Rng(ushort seed) + { + int startValue = 0x4D93 * (seed ^ (seed >> 14)); + + _stateA = (ushort)(3 - startValue); + _stateB = (ushort)(2 - startValue); + _stateC = (ushort)(1 - startValue); + _stateD = (ushort)(0 - startValue); + } + + public ushort Next() + { + ushort t = (ushort)(_stateD ^ (_stateD << 5)); + _stateD = _stateC; + _stateC = _stateB; + _stateB = _stateA; + _stateA = (ushort)(t ^ _stateA ^ ((t ^ (_stateA >> 5)) >> 4)); + return _stateA; + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/BandExtension.cs b/src/SharpEmu.GUI/Atrac9/BandExtension.cs new file mode 100644 index 0000000..57cc5b4 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/BandExtension.cs @@ -0,0 +1,373 @@ +#nullable disable +using System; + +namespace LibAtrac9 +{ + internal static class BandExtension + { + public static void ApplyBandExtension(Block block) + { + if (!block.BandExtensionEnabled || !block.HasExtensionData) return; + + foreach (Channel channel in block.Channels) + { + ApplyBandExtensionChannel(channel); + } + } + + private static void ApplyBandExtensionChannel(Channel channel) + { + int groupAUnit = channel.Block.QuantizationUnitCount; + int[] scaleFactors = channel.ScaleFactors; + double[] spectra = channel.Spectra; + double[] scales = channel.BexScales; + int[] values = channel.BexValues; + + GetBexBandInfo(out int bandCount, out int groupBUnit, out int groupCUnit, groupAUnit); + int totalUnits = Math.Max(groupCUnit, 22); + + int groupABin = Tables.QuantUnitToCoeffIndex[groupAUnit]; + int groupBBin = Tables.QuantUnitToCoeffIndex[groupBUnit]; + int groupCBin = Tables.QuantUnitToCoeffIndex[groupCUnit]; + int totalBins = Tables.QuantUnitToCoeffIndex[totalUnits]; + + FillHighFrequencies(spectra, groupABin, groupBBin, groupCBin, totalBins); + + switch (channel.BexMode) + { + case 0: + int bexQuantUnits = totalUnits - groupAUnit; + + switch (bandCount) + { + case 3: + scales[0] = BexMode0Bands3[0][values[0]]; + scales[1] = BexMode0Bands3[1][values[0]]; + scales[2] = BexMode0Bands3[2][values[1]]; + scales[3] = BexMode0Bands3[3][values[2]]; + scales[4] = BexMode0Bands3[4][values[3]]; + break; + case 4: + scales[0] = BexMode0Bands4[0][values[0]]; + scales[1] = BexMode0Bands4[1][values[0]]; + scales[2] = BexMode0Bands4[2][values[1]]; + scales[3] = BexMode0Bands4[3][values[2]]; + scales[4] = BexMode0Bands4[4][values[3]]; + break; + case 5: + scales[0] = BexMode0Bands5[0][values[0]]; + scales[1] = BexMode0Bands5[1][values[1]]; + scales[2] = BexMode0Bands5[2][values[1]]; + break; + } + + scales[bexQuantUnits - 1] = Tables.SpectrumScale[scaleFactors[groupAUnit]]; + + AddNoiseToSpectrum(channel, Tables.QuantUnitToCoeffIndex[totalUnits - 1], + Tables.QuantUnitToCoeffCount[totalUnits - 1]); + ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits); + break; + case 1: + for (int i = groupAUnit; i < totalUnits; i++) + { + scales[i - groupAUnit] = Tables.SpectrumScale[scaleFactors[i]]; + } + + AddNoiseToSpectrum(channel, groupABin, totalBins - groupABin); + ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits); + break; + case 2: + double groupAScale2 = BexMode2Scale[values[0]]; + double groupBScale2 = BexMode2Scale[values[1]]; + + for (int i = groupABin; i < groupBBin; i++) + { + spectra[i] *= groupAScale2; + } + + for (int i = groupBBin; i < groupCBin; i++) + { + spectra[i] *= groupBScale2; + } + return; + case 3: + double rate = Math.Pow(2, BexMode3Rate[values[1]]); + double scale = BexMode3Initial[values[0]]; + for (int i = groupABin; i < totalBins; i++) + { + scale *= rate; + spectra[i] *= scale; + } + return; + case 4: + double mult = BexMode4Multiplier[values[0]]; + double groupAScale4 = 0.7079468 * mult; + double groupBScale4 = 0.5011902 * mult; + double groupCScale4 = 0.3548279 * mult; + + for (int i = groupABin; i < groupBBin; i++) + { + spectra[i] *= groupAScale4; + } + + for (int i = groupBBin; i < groupCBin; i++) + { + spectra[i] *= groupBScale4; + } + + for (int i = groupCBin; i < totalBins; i++) + { + spectra[i] *= groupCScale4; + } + return; + } + } + + private static void ScaleBexQuantUnits(double[] spectra, double[] scales, int startUnit, int totalUnits) + { + for (int i = startUnit; i < totalUnits; i++) + { + for (int k = Tables.QuantUnitToCoeffIndex[i]; k < Tables.QuantUnitToCoeffIndex[i + 1]; k++) + { + spectra[k] *= scales[i - startUnit]; + } + } + } + + private static void FillHighFrequencies(double[] spectra, int groupABin, int groupBBin, int groupCBin, int totalBins) + { + for (int i = 0; i < groupBBin - groupABin; i++) + { + spectra[groupABin + i] = spectra[groupABin - i - 1]; + } + + for (int i = 0; i < groupCBin - groupBBin; i++) + { + spectra[groupBBin + i] = spectra[groupBBin - i - 1]; + } + + for (int i = 0; i < totalBins - groupCBin; i++) + { + spectra[groupCBin + i] = spectra[groupCBin - i - 1]; + } + } + + private static void AddNoiseToSpectrum(Channel channel, int index, int count) + { + if (channel.Rng == null) + { + int[] sf = channel.ScaleFactors; + ushort seed = (ushort)(543 * (sf[8] + sf[12] + sf[15] + 1)); + channel.Rng = new Atrac9Rng(seed); + } + for (int i = 0; i < count; i++) + { + channel.Spectra[i + index] = channel.Rng.Next() / 65535.0 * 2.0 - 1.0; + } + } + + public static void GetBexBandInfo(out int bandCount, out int groupAUnit, out int groupBUnit, int quantUnits) + { + groupAUnit = BexGroupInfo[quantUnits - 13][0]; + groupBUnit = BexGroupInfo[quantUnits - 13][1]; + bandCount = BexGroupInfo[quantUnits - 13][2]; + } + + public static readonly byte[][] BexGroupInfo = + { + new byte[] {16, 21, 0}, + new byte[] {18, 22, 1}, + new byte[] {20, 22, 2}, + new byte[] {21, 22, 3}, + new byte[] {21, 22, 3}, + new byte[] {23, 24, 4}, + new byte[] {23, 24, 4}, + new byte[] {24, 24, 5} + }; + + // [mode][bands] + + public static readonly byte[][] BexEncodedValueCounts = + { + new byte[] {0, 0, 0, 4, 4, 2}, + new byte[] {0, 0, 0, 0, 0, 0}, + new byte[] {0, 0, 0, 2, 2, 1}, + new byte[] {0, 0, 0, 2, 2, 2}, + new byte[] {1, 1, 1, 0, 0, 0} + }; + + // [mode][bands][valueIndex] + + public static readonly byte[][][] BexDataLengths = + { + new[] { + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {5, 4, 3, 3}, + new byte[] {4, 4, 3, 4}, + new byte[] {4, 5, 0, 0} + }, new[] { + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0} + }, new[] { + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {6, 6, 0, 0}, + new byte[] {6, 6, 0, 0}, + new byte[] {6, 0, 0, 0} + }, new[] { + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {4, 4, 0, 0}, + new byte[] {4, 4, 0, 0}, + new byte[] {4, 4, 0, 0} + }, new[] { + new byte[] {3, 0, 0, 0}, + new byte[] {3, 0, 0, 0}, + new byte[] {3, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0} + } + }; + + public static readonly double[][] BexMode0Bands3 = + { + new[] { + 0.000000e+0, 1.988220e-1, 2.514343e-1, 2.960510e-1, + 3.263550e-1, 3.771362e-1, 3.786926e-1, 4.540405e-1, + 4.877625e-1, 5.262451e-1, 5.447083e-1, 5.737000e-1, + 6.212158e-1, 6.222839e-1, 6.560974e-1, 6.896667e-1, + 7.555542e-1, 7.677917e-1, 7.918091e-1, 7.971497e-1, + 8.188171e-1, 8.446045e-1, 9.790649e-1, 9.822083e-1, + 9.846191e-1, 9.859314e-1, 9.863586e-1, 9.863892e-1, + 9.873352e-1, 9.881287e-1, 9.898682e-1, 9.913330e-1 + }, new[] { + 0.000000e+0, 9.982910e-1, 7.592773e-2, 7.179565e-1, + 9.851379e-1, 5.340271e-1, 9.013672e-1, 6.349182e-1, + 7.226257e-1, 1.948547e-1, 7.628174e-1, 9.873657e-1, + 8.112183e-1, 2.715454e-1, 9.734192e-1, 1.443787e-1, + 4.640198e-1, 3.249207e-1, 3.790894e-1, 8.276367e-2, + 5.954590e-1, 2.864380e-1, 9.806824e-1, 7.929077e-1, + 6.292114e-1, 4.887085e-1, 2.905273e-1, 1.301880e-1, + 3.140869e-1, 5.482483e-1, 4.210815e-1, 1.182861e-1 + }, new[] { + 0.000000e+0, 3.155518e-2, 8.581543e-2, 1.364746e-1, + 1.858826e-1, 2.368469e-1, 2.888184e-1, 3.432617e-1, + 4.012451e-1, 4.623108e-1, 5.271301e-1, 5.954895e-1, + 6.681213e-1, 7.448425e-1, 8.245239e-1, 9.097290e-1 + }, new[] { + 0.000000e+0, 4.418945e-2, 1.303711e-1, 2.273560e-1, + 3.395996e-1, 4.735718e-1, 6.267090e-1, 8.003845e-1 + }, new[] { + 0.000000e+0, 2.804565e-2, 9.683228e-2, 1.849976e-1, + 3.005981e-1, 4.470520e-1, 6.168518e-1, 8.007813e-1 + } + }; + + public static readonly double[][] BexMode0Bands4 = + { + new[] { + 0.000000e+0, 2.708740e-1, 3.479614e-1, 3.578186e-1, + 5.083618e-1, 5.299072e-1, 5.819092e-1, 6.381836e-1, + 7.276917e-1, 7.595520e-1, 7.878723e-1, 9.707336e-1, + 9.713135e-1, 9.736023e-1, 9.759827e-1, 9.832458e-1 + }, new[] { + 0.000000e+0, 2.330627e-1, 5.891418e-1, 7.170410e-1, + 2.036438e-1, 1.613464e-1, 6.668701e-1, 9.481201e-1, + 9.769897e-1, 5.111694e-1, 3.522644e-1, 8.209534e-1, + 2.933960e-1, 9.757690e-1, 5.289917e-1, 4.372253e-1 + }, new[] { + 0.000000e+0, 4.360962e-2, 1.056519e-1, 1.590576e-1, + 2.078857e-1, 2.572937e-1, 3.082581e-1, 3.616028e-1, + 4.191589e-1, 4.792175e-1, 5.438538e-1, 6.125183e-1, + 6.841125e-1, 7.589417e-1, 8.365173e-1, 9.148254e-1 + }, new[] { + 0.000000e+0, 4.074097e-2, 1.164551e-1, 2.077026e-1, + 3.184509e-1, 4.532166e-1, 6.124268e-1, 7.932129e-1 + }, new[] { + 0.000000e+0, 8.880615e-3, 2.932739e-2, 5.593872e-2, + 8.825684e-2, 1.259155e-1, 1.721497e-1, 2.270813e-1, + 2.901611e-1, 3.579712e-1, 4.334106e-1, 5.147095e-1, + 6.023254e-1, 6.956177e-1, 7.952881e-1, 8.977356e-1 + } + }; + + public static readonly double[][] BexMode0Bands5 = + { + new[] { + 0.000000e+0, 7.379150e-2, 1.806335e-1, 2.687073e-1, + 3.407898e-1, 4.047546e-1, 4.621887e-1, 5.168762e-1, + 5.703125e-1, 6.237488e-1, 6.763611e-1, 7.288208e-1, + 7.808533e-1, 8.337708e-1, 8.874512e-1, 9.418030e-1 + }, new[] { + 0.000000e+0, 7.980347e-2, 1.615295e-1, 1.665649e-1, + 1.822205e-1, 2.185669e-1, 2.292175e-1, 2.456665e-1, + 2.666321e-1, 3.306580e-1, 3.330688e-1, 3.765259e-1, + 4.085083e-1, 4.400024e-1, 4.407654e-1, 4.817505e-1, + 4.924011e-1, 5.320740e-1, 5.893860e-1, 6.131287e-1, + 6.212463e-1, 6.278076e-1, 6.308899e-1, 7.660828e-1, + 7.850647e-1, 7.910461e-1, 7.929382e-1, 8.038330e-1, + 9.834900e-1, 9.846191e-1, 9.852295e-1, 9.862671e-1 + }, new[] { + 0.000000e+0, 6.084290e-1, 3.672791e-1, 3.151855e-1, + 1.488953e-1, 2.571716e-1, 5.103455e-1, 3.311157e-1, + 5.426025e-2, 4.254456e-1, 7.998352e-1, 7.873230e-1, + 5.418701e-1, 2.925110e-1, 8.468628e-2, 1.410522e-1, + 9.819641e-1, 9.609070e-1, 3.530884e-2, 9.729004e-2, + 5.758362e-1, 9.941711e-1, 7.215576e-1, 7.183228e-1, + 2.028809e-1, 9.588623e-2, 2.032166e-1, 1.338806e-1, + 5.003357e-1, 1.874390e-1, 9.804993e-1, 1.107788e-1 + } + }; + + public static readonly double[] BexMode2Scale = + { + 4.272461e-4, 1.312256e-3, 2.441406e-3, 3.692627e-3, + 4.913330e-3, 6.134033e-3, 7.507324e-3, 8.972168e-3, + 1.049805e-2, 1.223755e-2, 1.406860e-2, 1.599121e-2, + 1.800537e-2, 2.026367e-2, 2.264404e-2, 2.517700e-2, + 2.792358e-2, 3.073120e-2, 3.344727e-2, 3.631592e-2, + 3.952026e-2, 4.275513e-2, 4.608154e-2, 4.968262e-2, + 5.355835e-2, 5.783081e-2, 6.195068e-2, 6.677246e-2, + 7.196045e-2, 7.745361e-2, 8.319092e-2, 8.993530e-2, + 9.759521e-2, 1.056213e-1, 1.138916e-1, 1.236267e-1, + 1.348267e-1, 1.470337e-1, 1.603394e-1, 1.755676e-1, + 1.905823e-1, 2.071228e-1, 2.245178e-1, 2.444153e-1, + 2.658997e-1, 2.897644e-1, 3.146057e-1, 3.450012e-1, + 3.766174e-1, 4.122620e-1, 4.505615e-1, 4.893799e-1, + 5.305481e-1, 5.731201e-1, 6.157837e-1, 6.580811e-1, + 6.985168e-1, 7.435303e-1, 7.865906e-1, 8.302612e-1, + 8.718567e-1, 9.125671e-1, 9.575806e-1, 9.996643e-1 + }; + + public static readonly double[] BexMode3Initial = + { + 3.491211e-1, 5.371094e-1, 6.782227e-1, 7.910156e-1, + 9.057617e-1, 1.024902e+0, 1.156250e+0, 1.290527e+0, + 1.458984e+0, 1.664551e+0, 1.929688e+0, 2.278320e+0, + 2.831543e+0, 3.659180e+0, 5.257813e+0, 8.373047e+0 + }; + + public static readonly double[] BexMode3Rate = + { + -2.913818e-1, -2.541504e-1, -1.664429e-1, -1.476440e-1, + -1.342163e-1, -1.220703e-1, -1.117554e-1, -1.026611e-1, + -9.436035e-2, -8.483887e-2, -7.476807e-2, -6.304932e-2, + -4.492188e-2, -2.447510e-2, +1.831055e-4, +4.174805e-2 + }; + + public static readonly double[] BexMode4Multiplier = + { + 3.610229e-2, 1.260681e-1, 2.227478e-1, 3.338318e-1, + 4.662170e-1, 6.221313e-1, 7.989197e-1, 9.939575e-1 + }; + } +} diff --git a/src/SharpEmu.GUI/Atrac9/BitAllocation.cs b/src/SharpEmu.GUI/Atrac9/BitAllocation.cs new file mode 100644 index 0000000..f178d8c --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/BitAllocation.cs @@ -0,0 +1,141 @@ +#nullable disable +using System; + +namespace LibAtrac9 +{ + internal static class BitAllocation + { + public static void CreateGradient(Block block) + { + int valueCount = block.GradientEndValue - block.GradientStartValue; + int unitCount = block.GradientEndUnit - block.GradientStartUnit; + + for (int i = 0; i < block.GradientEndUnit; i++) + { + block.Gradient[i] = block.GradientStartValue; + } + + for (int i = block.GradientEndUnit; i <= block.QuantizationUnitCount; i++) + { + block.Gradient[i] = block.GradientEndValue; + } + if (unitCount <= 0) return; + if (valueCount == 0) return; + + byte[] curve = Tables.GradientCurves[unitCount - 1]; + if (valueCount <= 0) + { + double scale = (-valueCount - 1) / 31.0; + int baseVal = block.GradientStartValue - 1; + for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++) + { + block.Gradient[i] = baseVal - (int)(curve[i - block.GradientStartUnit] * scale); + } + } + else + { + double scale = (valueCount - 1) / 31.0; + int baseVal = block.GradientStartValue + 1; + for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++) + { + block.Gradient[i] = baseVal + (int)(curve[i - block.GradientStartUnit] * scale); + } + } + } + + public static void CalculateMask(Channel channel) + { + Array.Clear(channel.PrecisionMask, 0, channel.PrecisionMask.Length); + for (int i = 1; i < channel.Block.QuantizationUnitCount; i++) + { + int delta = channel.ScaleFactors[i] - channel.ScaleFactors[i - 1]; + if (delta > 1) + { + channel.PrecisionMask[i] += Math.Min(delta - 1, 5); + } + else if (delta < -1) + { + channel.PrecisionMask[i - 1] += Math.Min(delta * -1 - 1, 5); + } + } + } + + public static void CalculatePrecisions(Channel channel) + { + Block block = channel.Block; + + if (block.GradientMode != 0) + { + for (int i = 0; i < block.QuantizationUnitCount; i++) + { + channel.Precisions[i] = channel.ScaleFactors[i] + channel.PrecisionMask[i] - block.Gradient[i]; + if (channel.Precisions[i] > 0) + { + switch (block.GradientMode) + { + case 1: + channel.Precisions[i] /= 2; + break; + case 2: + channel.Precisions[i] = 3 * channel.Precisions[i] / 8; + break; + case 3: + channel.Precisions[i] /= 4; + break; + } + } + } + } + else + { + for (int i = 0; i < block.QuantizationUnitCount; i++) + { + channel.Precisions[i] = channel.ScaleFactors[i] - block.Gradient[i]; + } + } + + for (int i = 0; i < block.QuantizationUnitCount; i++) + { + if (channel.Precisions[i] < 1) + { + channel.Precisions[i] = 1; + } + } + + for (int i = 0; i < block.GradientBoundary; i++) + { + channel.Precisions[i]++; + } + + for (int i = 0; i < block.QuantizationUnitCount; i++) + { + channel.PrecisionsFine[i] = 0; + if (channel.Precisions[i] > 15) + { + channel.PrecisionsFine[i] = channel.Precisions[i] - 15; + channel.Precisions[i] = 15; + } + } + } + + public static byte[][] GenerateGradientCurves() + { + byte[] main = + { + 01, 01, 01, 01, 02, 02, 02, 02, 03, 03, 03, 04, 04, 05, 05, 06, 07, 08, 09, 10, 11, 12, 13, 15, + 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30 + }; + var curves = new byte[main.Length][]; + + for (int length = 1; length <= main.Length; length++) + { + curves[length - 1] = new byte[length]; + for (int i = 0; i < length; i++) + { + curves[length - 1][i] = main[i * main.Length / length]; + } + } + return curves; + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Block.cs b/src/SharpEmu.GUI/Atrac9/Block.cs new file mode 100644 index 0000000..e267578 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Block.cs @@ -0,0 +1,92 @@ +#nullable disable +namespace LibAtrac9 +{ + internal class Block + { + public Atrac9Config Config { get; } + public BlockType BlockType { get; } + public int BlockIndex { get; } + public Frame Frame { get; } + + public Channel[] Channels { get; } + public int ChannelCount { get; } + + public bool FirstInSuperframe { get; set; } + public bool ReuseBandParams { get; set; } + + public int BandCount { get; set; } + public int StereoBand { get; set; } + public int ExtensionBand { get; set; } + public int QuantizationUnitCount { get; set; } + public int StereoQuantizationUnit { get; set; } + public int ExtensionUnit { get; set; } + public int QuantizationUnitsPrev { get; set; } + + public int[] Gradient { get; } = new int[31]; + public int GradientMode { get; set; } + public int GradientStartUnit { get; set; } + public int GradientStartValue { get; set; } + public int GradientEndUnit { get; set; } + public int GradientEndValue { get; set; } + public int GradientBoundary { get; set; } + + public int PrimaryChannelIndex { get; set; } + public int[] JointStereoSigns { get; } = new int[30]; + public bool HasJointStereoSigns { get; set; } + public Channel PrimaryChannel => Channels[PrimaryChannelIndex == 0 ? 0 : 1]; + public Channel SecondaryChannel => Channels[PrimaryChannelIndex == 0 ? 1 : 0]; + + public bool BandExtensionEnabled { get; set; } + public bool HasExtensionData { get; set; } + public int BexDataLength { get; set; } + public int BexMode { get; set; } + + public Block(Frame parentFrame, int blockIndex) + { + Frame = parentFrame; + BlockIndex = blockIndex; + Config = parentFrame.Config; + BlockType = Config.ChannelConfig.BlockTypes[blockIndex]; + ChannelCount = BlockTypeToChannelCount(BlockType); + Channels = new Channel[ChannelCount]; + for (int i = 0; i < ChannelCount; i++) + { + Channels[i] = new Channel(this, i); + } + } + + public static int BlockTypeToChannelCount(BlockType blockType) + { + switch (blockType) + { + case BlockType.Mono: + return 1; + case BlockType.Stereo: + return 2; + case BlockType.LFE: + return 1; + default: + return 0; + } + } + } + + /// + /// An ATRAC9 block (substream) type + /// + public enum BlockType + { + /// + /// Mono ATRAC9 block + /// + Mono = 0, + /// + /// Stereo ATRAC9 block + /// + Stereo = 1, + /// + /// Low-frequency effects ATRAC9 block + /// + LFE = 2 + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Channel.cs b/src/SharpEmu.GUI/Atrac9/Channel.cs new file mode 100644 index 0000000..80d80b1 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Channel.cs @@ -0,0 +1,49 @@ +#nullable disable +using LibAtrac9.Utilities; + +namespace LibAtrac9 +{ + internal class Channel + { + public Atrac9Config Config { get; } + public int ChannelIndex { get; } + public bool IsPrimary => Block.PrimaryChannelIndex == ChannelIndex; + public Block Block { get; } + public Mdct Mdct { get; } + + public double[] Pcm { get; } = new double[256]; + public double[] Spectra { get; } = new double[256]; + + public int CodedQuantUnits { get; set; } + public int ScaleFactorCodingMode { get; set; } + public int[] ScaleFactors { get; } = new int[31]; + public int[] ScaleFactorsPrev { get; } = new int[31]; + + public int[] Precisions { get; } = new int[30]; + public int[] PrecisionsFine { get; } = new int[30]; + public int[] PrecisionMask { get; } = new int[30]; + + public int[] SpectraValuesBuffer { get; } = new int[16]; + public int[] CodebookSet { get; } = new int[30]; + + public int[] QuantizedSpectra { get; } = new int[256]; + public int[] QuantizedSpectraFine { get; } = new int[256]; + + public int BexMode { get; set; } + public int BexValueCount { get; set; } + public int[] BexValues { get; } = new int[4]; + public double[] BexScales { get; } = new double[6]; + public Atrac9Rng Rng { get; set; } + + public Channel(Block parentBlock, int channelIndex) + { + Block = parentBlock; + ChannelIndex = channelIndex; + Config = parentBlock.Config; + Mdct = new Mdct(Config.FrameSamplesPower, Tables.ImdctWindow[Config.FrameSamplesPower - 6]); + } + + public void UpdateCodedUnits() => + CodedQuantUnits = IsPrimary ? Block.QuantizationUnitCount : Block.StereoQuantizationUnit; + } +} diff --git a/src/SharpEmu.GUI/Atrac9/ChannelConfig.cs b/src/SharpEmu.GUI/Atrac9/ChannelConfig.cs new file mode 100644 index 0000000..b3de145 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/ChannelConfig.cs @@ -0,0 +1,34 @@ +#nullable disable +namespace LibAtrac9 +{ + /// + /// Describes the channel mapping for an ATRAC9 stream + /// + public class ChannelConfig + { + internal ChannelConfig(params BlockType[] blockTypes) + { + BlockCount = blockTypes.Length; + BlockTypes = blockTypes; + foreach (BlockType type in blockTypes) + { + ChannelCount += Block.BlockTypeToChannelCount(type); + } + } + + /// + /// The number of blocks or substreams in the ATRAC9 stream + /// + public int BlockCount { get; } + + /// + /// The type of each block or substream in the ATRAC9 stream + /// + public BlockType[] BlockTypes { get; } + + /// + /// The number of channels in the ATRAC9 stream + /// + public int ChannelCount { get; } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Frame.cs b/src/SharpEmu.GUI/Atrac9/Frame.cs new file mode 100644 index 0000000..73e0b77 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Frame.cs @@ -0,0 +1,21 @@ +#nullable disable +namespace LibAtrac9 +{ + internal class Frame + { + public Atrac9Config Config { get; } + public int FrameIndex { get; set; } + public Block[] Blocks { get; } + + public Frame(Atrac9Config config) + { + Config = config; + Blocks = new Block[config.ChannelConfig.BlockCount]; + + for (int i = 0; i < config.ChannelConfig.BlockCount; i++) + { + Blocks[i] = new Block(this, i); + } + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/HuffmanCodebook.cs b/src/SharpEmu.GUI/Atrac9/HuffmanCodebook.cs new file mode 100644 index 0000000..dcd8a40 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/HuffmanCodebook.cs @@ -0,0 +1,63 @@ +#nullable disable +using System; +using LibAtrac9.Utilities; + +namespace LibAtrac9 +{ + internal class HuffmanCodebook + { + public HuffmanCodebook(short[] codes, byte[] bits, byte valueCountPower) + { + Codes = codes; + Bits = bits; + if (Codes == null || Bits == null) return; + + ValueCount = 1 << valueCountPower; + ValueCountPower = valueCountPower; + ValueBits = Helpers.Log2(codes.Length) >> valueCountPower; + ValueMax = 1 << ValueBits; + + int max = 0; + foreach (byte bitSize in bits) + { + max = Math.Max(max, bitSize); + } + + MaxBitSize = max; + Lookup = CreateLookupTable(); + } + + private byte[] CreateLookupTable() + { + if (Codes == null || Bits == null) return null; + + int tableSize = 1 << MaxBitSize; + var dest = new byte[tableSize]; + + for (int i = 0; i < Bits.Length; i++) + { + if (Bits[i] == 0) continue; + int unusedBits = MaxBitSize - Bits[i]; + + int start = Codes[i] << unusedBits; + int length = 1 << unusedBits; + int end = start + length; + + for (int j = start; j < end; j++) + { + dest[j] = (byte)i; + } + } + return dest; + } + + public short[] Codes { get; } + public byte[] Bits { get; } + public byte[] Lookup { get; } + public int ValueCount { get; } + public int ValueCountPower { get; } + public int ValueBits { get; } + public int ValueMax { get; } + public int MaxBitSize { get; } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/HuffmanCodebooks.cs b/src/SharpEmu.GUI/Atrac9/HuffmanCodebooks.cs new file mode 100644 index 0000000..80c78a1 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/HuffmanCodebooks.cs @@ -0,0 +1,1353 @@ +#nullable disable +namespace LibAtrac9 +{ + internal static class HuffmanCodebooks + { + public static HuffmanCodebook[][] GenerateHuffmanCodebooks(short[][][] codes, byte[][][] bits, byte[][] groupCounts) + { + var tables = new HuffmanCodebook[bits.Length][]; + for (int i = 0; i < tables.Length; i++) + { + if (codes[i] != null) + { + tables[i] = GenerateHuffmanCodebooks(codes[i], bits[i], groupCounts[i]); + } + } + return tables; + } + + public static HuffmanCodebook[] GenerateHuffmanCodebooks(short[][] codes, byte[][] bits, byte[] groupCounts) + { + var tables = new HuffmanCodebook[bits.Length]; + for (int i = 0; i < tables.Length; i++) + { + if (codes[i] != null) + { + tables[i] = new HuffmanCodebook(codes[i], bits[i], groupCounts[i]); + } + } + return tables; + } + + // For scale factor table names, {letter}{number} correspond to the signedness and word length + private static readonly byte[] ScaleFactorsA1Bits = + { + 1, 1 + }; + + private static readonly short[] ScaleFactorsA1Codes = + { + 0x00, 0x01 + }; + + private static readonly byte[] ScaleFactorsA2Bits = + { + 1, 3, 3, 2 + }; + + private static readonly short[] ScaleFactorsA2Codes = + { + 0x00, 0x06, 0x07, 0x02 + }; + + private static readonly byte[] ScaleFactorsA3Bits = + { + 2, 2, 4, 6, 6, 5, 3, 2 + }; + + private static readonly short[] ScaleFactorsA3Codes = + { + 0x00, 0x01, 0x0E, 0x3E, 0x3F, 0x1E, 0x06, 0x02 + }; + + private static readonly byte[] ScaleFactorsA4Bits = + { + 2, 2, 4, 5, 6, 7, 8, 8, 8, 8, 8, 8, 6, 5, 4, 2 + }; + + private static readonly short[] ScaleFactorsA4Codes = + { + 0x01, 0x02, 0x00, 0x06, 0x0F, 0x13, 0x23, 0x24, 0x25, 0x22, 0x21, 0x20, 0x0E, 0x05, 0x01, 0x03 + }; + + private static readonly byte[] ScaleFactorsA5Bits = + { + 2, 3, 3, 4, 5, 5, 6, 7, 7, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 6, 5, 5, 4, 3 + }; + + private static readonly short[] ScaleFactorsA5Codes = + { + 0x02, 0x01, 0x07, 0x0D, 0x0C, 0x18, 0x1B, 0x21, 0x3F, 0x6A, 0x6B, 0x68, 0x73, 0x79, 0x7C, 0x7D, + 0x7A, 0x7B, 0x78, 0x72, 0x44, 0x45, 0x47, 0x46, 0x69, 0x38, 0x20, 0x1D, 0x19, 0x09, 0x05, 0x00 + }; + + private static readonly byte[] ScaleFactorsA6Bits = + { + 3, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 5, 5, 4, 4, 4 + }; + + private static readonly short[] ScaleFactorsA6Codes = + { + 0x00, 0x01, 0x04, 0x05, 0x12, 0x13, 0x2E, 0x2F, 0x30, 0x66, 0x67, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, + 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, + 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, + 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x68, 0x69, 0x6A, 0x31, 0x32, 0x14, 0x15, 0x16, 0x06, 0x07, 0x08 + }; + + private static readonly byte[] ScaleFactorsB2Bits = + { + 1, 2, 0, 2 + }; + + private static readonly short[] ScaleFactorsB2Codes = + { + 0x00, 0x03, 0x00, 0x02 + }; + + private static readonly byte[] ScaleFactorsB3Bits = + { + 1, 3, 5, 6, 0, 6, 4, 2 + }; + + private static readonly short[] ScaleFactorsB3Codes = + { + 0x01, 0x00, 0x04, 0x0B, 0x00, 0x0A, 0x03, 0x01 + }; + + private static readonly byte[] ScaleFactorsB4Bits = + { + 1, 3, 4, 5, 5, 7, 8, 8, 0, 8, 8, 7, 6, 6, 4, 3 + }; + + private static readonly short[] ScaleFactorsB4Codes = + { + 0x01, 0x01, 0x04, 0x0E, 0x0F, 0x2C, 0x5A, 0x5D, 0x00, 0x5C, 0x5B, 0x2F, 0x15, 0x14, 0x06, 0x00 + }; + + private static readonly byte[] ScaleFactorsB5Bits = + { + 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 6, 7, 7, 7, 8, 8, + 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 3 + }; + + private static readonly short[] ScaleFactorsB5Codes = + { + 0x00, 0x05, 0x07, 0x0C, 0x04, 0x02, 0x03, 0x05, 0x09, 0x10, 0x23, 0x33, 0x36, 0x6E, 0x60, 0x65, + 0x62, 0x61, 0x63, 0x64, 0x6F, 0x6D, 0x6C, 0x6B, 0x6A, 0x68, 0x69, 0x45, 0x44, 0x37, 0x1A, 0x07 + }; + + // For spectrum table names, {letter}{number}{number} correspond to the + // codebook set, word length, and band group + private static readonly byte[] SpectrumA21Bits = + { + 0, 3, 0, 3, 3, 3, 0, 3, 0, 0, 0, 0, 3, 3, 0, 3 + }; + + private static readonly short[] SpectrumA21Codes = + { + 0x00, 0x00, 0x00, 0x01, 0x03, 0x07, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x06 + }; + + private static readonly byte[] SpectrumA22Bits = + { + 0, 4, 0, 4, 5, 6, 0, 6, 0, 0, 0, 0, 5, 6, 0, 6, + 5, 6, 0, 6, 6, 7, 0, 7, 0, 0, 0, 0, 6, 7, 0, 7, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 5, 6, 0, 6, 6, 7, 0, 7, 0, 0, 0, 0, 6, 7, 0, 7, + 5, 6, 0, 6, 7, 7, 0, 7, 0, 0, 0, 0, 6, 7, 0, 7, + 6, 7, 0, 7, 7, 8, 0, 8, 0, 0, 0, 0, 7, 8, 0, 7, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 7, 0, 7, 7, 8, 0, 8, 0, 0, 0, 0, 7, 7, 0, 8, + 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, + 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, + 5, 6, 0, 6, 6, 7, 0, 7, 0, 0, 0, 0, 7, 7, 0, 7, + 6, 7, 0, 7, 7, 8, 0, 7, 0, 0, 0, 0, 7, 8, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 7, 0, 7, 7, 7, 0, 8, 0, 0, 0, 0, 7, 8, 0, 8 + }; + + private static readonly short[] SpectrumA22Codes = + { + 0x00, 0x02, 0x00, 0x03, 0x10, 0x3C, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x11, 0x3E, 0x00, 0x3D, + 0x0E, 0x00, 0x00, 0x39, 0x18, 0x26, 0x00, 0x75, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x24, 0x00, 0x6D, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x38, 0x00, 0x01, 0x1A, 0x6C, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x19, 0x74, 0x00, 0x27, + 0x16, 0x14, 0x00, 0x17, 0x76, 0x06, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x35, 0x64, 0x00, 0x6F, + 0x26, 0x04, 0x00, 0x63, 0x22, 0xA2, 0x00, 0x97, 0x00, 0x00, 0x00, 0x00, 0x67, 0xA0, 0x00, 0x0D, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2B, 0x52, 0x00, 0x0B, 0x20, 0x92, 0x00, 0x91, 0x00, 0x00, 0x00, 0x00, 0x61, 0x0E, 0x00, 0x95, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x17, 0x16, 0x00, 0x15, 0x34, 0x6E, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00, 0x77, 0x08, 0x00, 0x07, + 0x2A, 0x0A, 0x00, 0x53, 0x60, 0x94, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x21, 0x90, 0x00, 0x93, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x27, 0x62, 0x00, 0x05, 0x66, 0x0C, 0x00, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x23, 0x96, 0x00, 0xA3 + }; + + private static readonly byte[] SpectrumA23Bits = + { + 3, 4, 0, 4, 5, 6, 0, 6, 0, 0, 0, 0, 5, 6, 0, 6, + 5, 7, 0, 6, 6, 8, 0, 7, 0, 0, 0, 0, 6, 8, 0, 7, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 5, 6, 0, 7, 6, 7, 0, 8, 0, 0, 0, 0, 6, 7, 0, 8, + 5, 6, 0, 6, 7, 8, 0, 8, 0, 0, 0, 0, 6, 7, 0, 7, + 6, 8, 0, 7, 8, 9, 0, 9, 0, 0, 0, 0, 7, 9, 0, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 8, 0, 8, 8, 9, 0, 9, 0, 0, 0, 0, 7, 8, 0, 9, + 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, + 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, + 5, 6, 0, 6, 6, 7, 0, 7, 0, 0, 0, 0, 7, 8, 0, 8, + 6, 8, 0, 8, 7, 9, 0, 8, 0, 0, 0, 0, 8, 9, 0, 9, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 7, 0, 8, 7, 8, 0, 9, 0, 0, 0, 0, 8, 9, 0, 9 + }; + + private static readonly short[] SpectrumA23Codes = + { + 0x006, 0x002, 0x000, 0x003, 0x016, 0x01E, 0x000, 0x021, 0x000, 0x000, 0x000, 0x000, + 0x017, 0x020, 0x000, 0x01F, 0x01C, 0x054, 0x000, 0x027, 0x010, 0x0A6, 0x000, 0x027, + 0x000, 0x000, 0x000, 0x000, 0x015, 0x0A4, 0x000, 0x02D, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x01D, 0x026, 0x000, 0x055, 0x014, 0x02C, 0x000, 0x0A5, 0x000, 0x000, 0x000, 0x000, + 0x011, 0x026, 0x000, 0x0A7, 0x01E, 0x000, 0x000, 0x003, 0x04A, 0x074, 0x000, 0x071, + 0x000, 0x000, 0x000, 0x000, 0x023, 0x00A, 0x000, 0x009, 0x018, 0x072, 0x000, 0x00D, + 0x0A2, 0x15A, 0x000, 0x123, 0x000, 0x000, 0x000, 0x000, 0x00F, 0x158, 0x000, 0x05D, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x01B, 0x0AE, 0x000, 0x077, 0x092, 0x140, 0x000, 0x121, + 0x000, 0x000, 0x000, 0x000, 0x025, 0x05E, 0x000, 0x143, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x01F, 0x002, 0x000, 0x001, 0x022, 0x008, 0x000, 0x00B, 0x000, 0x000, 0x000, 0x000, + 0x04B, 0x070, 0x000, 0x075, 0x01A, 0x076, 0x000, 0x0AF, 0x024, 0x142, 0x000, 0x05F, + 0x000, 0x000, 0x000, 0x000, 0x093, 0x120, 0x000, 0x141, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x019, 0x00C, 0x000, 0x073, 0x00E, 0x05C, 0x000, 0x159, 0x000, 0x000, 0x000, 0x000, + 0x0A3, 0x122, 0x000, 0x15B + }; + + private static readonly byte[] SpectrumA24Bits = + { + 02, 04, 00, 04, 05, 06, 00, 06, 00, 00, 00, 00, 05, 06, 00, 06, + 05, 07, 00, 06, 06, 08, 00, 08, 00, 00, 00, 00, 06, 08, 00, 08, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 05, 06, 00, 07, 06, 08, 00, 08, 00, 00, 00, 00, 06, 08, 00, 08, + 05, 07, 00, 07, 07, 09, 00, 09, 00, 00, 00, 00, 06, 08, 00, 08, + 06, 09, 00, 08, 08, 10, 00, 10, 00, 00, 00, 00, 08, 10, 00, 09, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 06, 08, 00, 09, 09, 10, 00, 10, 00, 00, 00, 00, 08, 09, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 05, 07, 00, 07, 06, 08, 00, 08, 00, 00, 00, 00, 07, 09, 00, 09, + 06, 09, 00, 08, 08, 10, 00, 09, 00, 00, 00, 00, 09, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 06, 08, 00, 09, 08, 09, 00, 10, 00, 00, 00, 00, 08, 10, 00, 10 + }; + + private static readonly short[] SpectrumA24Codes = + { + 0x002, 0x002, 0x000, 0x003, 0x01E, 0x010, 0x000, 0x013, 0x000, 0x000, 0x000, 0x000, + 0x01F, 0x012, 0x000, 0x011, 0x01A, 0x030, 0x000, 0x01B, 0x000, 0x064, 0x000, 0x0C1, + 0x000, 0x000, 0x000, 0x000, 0x003, 0x052, 0x000, 0x07D, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x01B, 0x01A, 0x000, 0x031, 0x002, 0x07C, 0x000, 0x053, 0x000, 0x000, 0x000, 0x000, + 0x001, 0x0C0, 0x000, 0x065, 0x01C, 0x062, 0x000, 0x065, 0x02A, 0x198, 0x000, 0x19B, + 0x000, 0x000, 0x000, 0x000, 0x017, 0x078, 0x000, 0x07B, 0x004, 0x0FE, 0x000, 0x077, + 0x050, 0x33A, 0x000, 0x1F9, 0x000, 0x000, 0x000, 0x000, 0x073, 0x338, 0x000, 0x0E1, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x007, 0x066, 0x000, 0x187, 0x19E, 0x308, 0x000, 0x30B, + 0x000, 0x000, 0x000, 0x000, 0x075, 0x0E2, 0x000, 0x1FB, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x01D, 0x064, 0x000, 0x063, 0x016, 0x07A, 0x000, 0x079, 0x000, 0x000, 0x000, 0x000, + 0x02B, 0x19A, 0x000, 0x199, 0x006, 0x186, 0x000, 0x067, 0x074, 0x1FA, 0x000, 0x0E3, + 0x000, 0x000, 0x000, 0x000, 0x19F, 0x30A, 0x000, 0x309, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x005, 0x076, 0x000, 0x0FF, 0x072, 0x0E0, 0x000, 0x339, 0x000, 0x000, 0x000, 0x000, + 0x051, 0x1F8, 0x000, 0x33B + }; + + private static readonly byte[] SpectrumA31Bits = + { + 0, 0, 4, 5, 0, 5, 4, 0, 0, 0, 5, 5, 0, 5, 5, 0, + 5, 5, 6, 6, 0, 6, 5, 5, 5, 6, 6, 7, 0, 7, 6, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 6, 7, 0, 7, 6, 6, + 5, 5, 5, 6, 0, 6, 6, 5, 0, 0, 5, 5, 0, 5, 5, 0 + }; + + private static readonly short[] SpectrumA31Codes = + { + 0x00, 0x00, 0x02, 0x18, 0x00, 0x19, 0x03, 0x00, 0x00, 0x00, 0x12, 0x02, 0x00, 0x09, 0x15, 0x00, + 0x1A, 0x0A, 0x3E, 0x2C, 0x00, 0x2F, 0x01, 0x0D, 0x0E, 0x38, 0x20, 0x78, 0x00, 0x7B, 0x23, 0x3B, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x3A, 0x22, 0x7A, 0x00, 0x79, 0x21, 0x39, + 0x1B, 0x0C, 0x00, 0x2E, 0x00, 0x2D, 0x3F, 0x0B, 0x00, 0x00, 0x14, 0x08, 0x00, 0x03, 0x13, 0x00 + }; + + private static readonly byte[] SpectrumA32Bits = + { + 4, 5, 5, 6, 0, 6, 5, 5, 5, 6, 5, 6, 0, 6, 5, 5, + 5, 5, 6, 7, 0, 7, 6, 5, 6, 6, 7, 7, 0, 7, 7, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 7, 7, 0, 7, 7, 6, + 5, 5, 6, 7, 0, 7, 6, 5, 5, 5, 5, 6, 0, 6, 5, 6 + }; + + private static readonly short[] SpectrumA32Codes = + { + 0x0D, 0x18, 0x16, 0x3A, 0x00, 0x3B, 0x17, 0x19, 0x12, 0x3E, 0x08, 0x1C, 0x00, 0x1B, 0x07, 0x01, + 0x10, 0x02, 0x28, 0x78, 0x00, 0x7B, 0x1F, 0x05, 0x2A, 0x16, 0x72, 0x2A, 0x00, 0x29, 0x71, 0x19, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2B, 0x18, 0x70, 0x28, 0x00, 0x2B, 0x73, 0x17, + 0x11, 0x04, 0x1E, 0x7A, 0x00, 0x79, 0x29, 0x03, 0x13, 0x00, 0x06, 0x1A, 0x00, 0x1D, 0x09, 0x3F + }; + + private static readonly byte[] SpectrumA33Bits = + { + 3, 4, 5, 6, 0, 6, 5, 4, 4, 5, 6, 7, 0, 7, 6, 5, + 5, 6, 6, 7, 0, 7, 6, 6, 6, 7, 8, 8, 0, 8, 8, 7, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 8, 0, 8, 8, 7, + 5, 6, 6, 7, 0, 7, 6, 6, 4, 5, 6, 7, 0, 7, 6, 5 + }; + + private static readonly short[] SpectrumA33Codes = + { + 0x05, 0x06, 0x10, 0x08, 0x00, 0x09, 0x11, 0x07, 0x04, 0x12, 0x3E, 0x6A, 0x00, 0x6D, 0x3D, 0x19, + 0x06, 0x3A, 0x06, 0x02, 0x00, 0x01, 0x05, 0x39, 0x02, 0x16, 0xDC, 0x2A, 0x00, 0x29, 0xDF, 0x69, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x68, 0xDE, 0x28, 0x00, 0x2B, 0xDD, 0x17, + 0x07, 0x38, 0x04, 0x00, 0x00, 0x03, 0x07, 0x3B, 0x05, 0x18, 0x3C, 0x6C, 0x00, 0x6B, 0x3F, 0x13 + }; + + private static readonly byte[] SpectrumA34Bits = + { + 02, 04, 05, 07, 00, 07, 05, 04, 04, 05, 06, 08, 00, 08, 06, 05, + 05, 06, 07, 08, 00, 08, 07, 06, 07, 08, 08, 10, 00, 10, 09, 08, + 00, 00, 00, 00, 00, 00, 00, 00, 07, 08, 09, 10, 00, 10, 08, 08, + 05, 06, 07, 08, 00, 08, 07, 06, 04, 05, 06, 08, 00, 08, 06, 05 + }; + + private static readonly short[] SpectrumA34Codes = + { + 0x000, 0x00A, 0x00A, 0x034, 0x000, 0x035, 0x00B, 0x00B, 0x008, 0x01C, 0x032, 0x0DA, + 0x000, 0x0DD, 0x035, 0x01F, 0x008, 0x01E, 0x03A, 0x06C, 0x000, 0x063, 0x039, 0x031, + 0x032, 0x06E, 0x060, 0x37A, 0x000, 0x379, 0x1BF, 0x0D9, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x033, 0x0D8, 0x1BE, 0x378, 0x000, 0x37B, 0x061, 0x06F, + 0x009, 0x030, 0x038, 0x062, 0x000, 0x06D, 0x03B, 0x01F, 0x009, 0x01E, 0x034, 0x0DC, + 0x000, 0x0DB, 0x033, 0x01D + }; + + private static readonly byte[] SpectrumA41Bits = + { + 0, 0, 0, 0, 6, 6, 7, 7, 0, 7, 7, 6, 6, 0, 0, 0, + 0, 0, 0, 0, 7, 7, 7, 7, 0, 7, 7, 7, 6, 0, 0, 0, + 0, 0, 0, 0, 7, 7, 7, 8, 0, 8, 7, 7, 7, 0, 0, 0, + 0, 0, 0, 0, 7, 7, 8, 8, 0, 8, 8, 7, 7, 0, 0, 0, + 7, 7, 7, 8, 7, 8, 8, 8, 0, 8, 8, 8, 7, 8, 7, 7, + 7, 7, 7, 7, 8, 8, 8, 9, 0, 8, 8, 8, 8, 7, 7, 7, + 7, 7, 8, 8, 8, 8, 9, 9, 0, 9, 8, 8, 8, 8, 8, 7, + 8, 8, 8, 8, 8, 9, 9, 9, 0, 9, 9, 9, 8, 8, 8, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 8, 8, 8, 8, 8, 9, 9, 9, 0, 9, 9, 9, 8, 8, 8, 8, + 7, 7, 8, 8, 8, 8, 8, 9, 0, 9, 9, 8, 8, 8, 8, 7, + 7, 7, 7, 7, 8, 8, 8, 8, 0, 9, 8, 8, 8, 7, 7, 7, + 7, 7, 7, 8, 7, 8, 8, 8, 0, 8, 8, 8, 7, 8, 7, 7, + 0, 0, 0, 0, 7, 7, 8, 8, 0, 8, 8, 7, 7, 0, 0, 0, + 0, 0, 0, 0, 7, 7, 7, 8, 0, 8, 7, 7, 7, 0, 0, 0, + 0, 0, 0, 0, 6, 7, 7, 7, 0, 7, 7, 7, 7, 0, 0, 0 + }; + + private static readonly short[] SpectrumA41Codes = + { + 0x000, 0x000, 0x000, 0x000, 0x018, 0x00E, 0x05E, 0x028, 0x000, 0x029, 0x05F, 0x00F, + 0x019, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x076, 0x06E, 0x03E, 0x004, + 0x000, 0x017, 0x045, 0x07B, 0x013, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x04A, 0x048, 0x010, 0x0CE, 0x000, 0x0E1, 0x023, 0x055, 0x053, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x008, 0x018, 0x0D6, 0x09E, 0x000, 0x09D, 0x0E5, 0x02B, + 0x01B, 0x000, 0x000, 0x000, 0x07C, 0x05C, 0x038, 0x0FC, 0x002, 0x0D2, 0x09A, 0x05C, + 0x000, 0x06B, 0x0A3, 0x0D9, 0x00F, 0x0FF, 0x03D, 0x061, 0x074, 0x056, 0x036, 0x000, + 0x0CC, 0x08C, 0x058, 0x1E2, 0x000, 0x00F, 0x05F, 0x0A1, 0x0D5, 0x00D, 0x03B, 0x059, + 0x040, 0x014, 0x0DA, 0x0B6, 0x084, 0x040, 0x1E0, 0x196, 0x000, 0x1A1, 0x00D, 0x043, + 0x087, 0x0C7, 0x0E3, 0x00B, 0x0F2, 0x0C4, 0x08E, 0x05A, 0x024, 0x1CC, 0x194, 0x168, + 0x000, 0x16B, 0x1A3, 0x1CF, 0x027, 0x069, 0x099, 0x0C9, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x0F3, 0x0C8, 0x098, 0x068, 0x026, 0x1CE, 0x1A2, 0x16A, 0x000, 0x169, 0x195, 0x1CD, + 0x025, 0x05B, 0x08F, 0x0C5, 0x041, 0x00A, 0x0E2, 0x0C6, 0x086, 0x042, 0x00C, 0x1A0, + 0x000, 0x197, 0x1E1, 0x041, 0x085, 0x0B7, 0x0DB, 0x015, 0x075, 0x058, 0x03A, 0x00C, + 0x0D4, 0x0A0, 0x05E, 0x00E, 0x000, 0x1E3, 0x059, 0x08D, 0x0CD, 0x001, 0x037, 0x057, + 0x07D, 0x060, 0x03C, 0x0FE, 0x00E, 0x0D8, 0x0A2, 0x06A, 0x000, 0x05D, 0x09B, 0x0D3, + 0x003, 0x0FD, 0x039, 0x05D, 0x000, 0x000, 0x000, 0x000, 0x01A, 0x02A, 0x0E4, 0x09C, + 0x000, 0x09F, 0x0D7, 0x019, 0x009, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x052, 0x054, 0x022, 0x0E0, 0x000, 0x0CF, 0x011, 0x049, 0x04B, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x012, 0x07A, 0x044, 0x016, 0x000, 0x005, 0x03F, 0x06F, + 0x077, 0x000, 0x000, 0x000 + }; + + private static readonly byte[] SpectrumA42Bits = + { + 05, 06, 07, 07, 07, 07, 08, 08, 00, 08, 08, 07, 07, 07, 07, 06, + 06, 07, 07, 08, 07, 07, 08, 08, 00, 08, 08, 07, 07, 08, 07, 07, + 07, 07, 08, 08, 07, 08, 08, 09, 00, 09, 08, 08, 07, 08, 08, 07, + 08, 08, 08, 08, 08, 08, 08, 09, 00, 09, 08, 08, 08, 08, 08, 08, + 07, 07, 07, 08, 08, 08, 09, 09, 00, 09, 09, 08, 08, 08, 07, 07, + 07, 07, 08, 08, 08, 09, 09, 09, 00, 09, 09, 09, 08, 08, 08, 07, + 08, 08, 08, 08, 09, 09, 09, 10, 00, 10, 09, 09, 09, 08, 08, 08, + 08, 08, 09, 09, 09, 09, 10, 10, 00, 10, 10, 09, 09, 09, 09, 09, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 08, 09, 09, 09, 09, 09, 10, 10, 00, 10, 10, 09, 09, 09, 09, 08, + 08, 08, 08, 08, 09, 09, 09, 10, 00, 10, 09, 09, 09, 08, 08, 08, + 07, 07, 08, 08, 08, 09, 09, 09, 00, 09, 09, 09, 08, 08, 08, 07, + 07, 07, 07, 08, 08, 08, 09, 09, 00, 09, 09, 08, 08, 08, 07, 07, + 08, 08, 08, 08, 08, 08, 08, 09, 00, 09, 08, 08, 08, 08, 08, 08, + 07, 07, 08, 08, 07, 08, 08, 09, 00, 09, 08, 08, 07, 08, 08, 07, + 06, 07, 07, 08, 07, 07, 08, 08, 00, 08, 08, 07, 07, 08, 07, 07 + }; + + private static readonly short[] SpectrumA42Codes = + { + 0x003, 0x018, 0x058, 0x000, 0x066, 0x03C, 0x0D6, 0x07C, 0x000, 0x07D, 0x0D7, 0x03D, + 0x067, 0x001, 0x059, 0x019, 0x002, 0x064, 0x036, 0x0DA, 0x04C, 0x01C, 0x0BE, 0x02C, + 0x000, 0x037, 0x0C5, 0x029, 0x04B, 0x0E7, 0x03B, 0x069, 0x044, 0x02E, 0x0FA, 0x092, + 0x020, 0x0F8, 0x086, 0x1FC, 0x000, 0x1E7, 0x07F, 0x0F5, 0x023, 0x0AD, 0x0FD, 0x02D, + 0x0F6, 0x0DC, 0x09C, 0x03E, 0x0F0, 0x0B6, 0x026, 0x186, 0x000, 0x18D, 0x02F, 0x0B5, + 0x0E1, 0x03D, 0x0AF, 0x0D9, 0x054, 0x040, 0x014, 0x0EC, 0x0BC, 0x054, 0x1C6, 0x108, + 0x000, 0x10B, 0x1C5, 0x069, 0x0B9, 0x0DF, 0x019, 0x047, 0x026, 0x008, 0x0E4, 0x0A2, + 0x056, 0x1DC, 0x142, 0x06A, 0x000, 0x091, 0x123, 0x1DF, 0x04B, 0x0A7, 0x0EB, 0x00B, + 0x0C0, 0x09E, 0x06A, 0x022, 0x1AA, 0x140, 0x092, 0x3CA, 0x000, 0x3A7, 0x04B, 0x121, + 0x18F, 0x007, 0x071, 0x0A5, 0x020, 0x004, 0x1A8, 0x174, 0x0E4, 0x068, 0x3A4, 0x2EE, + 0x000, 0x2ED, 0x3C9, 0x049, 0x0E7, 0x185, 0x1D1, 0x1FF, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x021, 0x1FE, 0x1D0, 0x184, 0x0E6, 0x048, 0x3C8, 0x2EC, 0x000, 0x2EF, 0x3A5, 0x069, + 0x0E5, 0x175, 0x1A9, 0x005, 0x0C1, 0x0A4, 0x070, 0x006, 0x18E, 0x120, 0x04A, 0x3A6, + 0x000, 0x3CB, 0x093, 0x141, 0x1AB, 0x023, 0x06B, 0x09F, 0x027, 0x00A, 0x0EA, 0x0A6, + 0x04A, 0x1DE, 0x122, 0x090, 0x000, 0x06B, 0x143, 0x1DD, 0x057, 0x0A3, 0x0E5, 0x009, + 0x055, 0x046, 0x018, 0x0DE, 0x0B8, 0x068, 0x1C4, 0x10A, 0x000, 0x109, 0x1C7, 0x055, + 0x0BD, 0x0ED, 0x015, 0x041, 0x0F7, 0x0D8, 0x0AE, 0x03C, 0x0E0, 0x0B4, 0x02E, 0x18C, + 0x000, 0x187, 0x027, 0x0B7, 0x0F1, 0x03F, 0x09D, 0x0DD, 0x045, 0x02C, 0x0FC, 0x0AC, + 0x022, 0x0F4, 0x07E, 0x1E6, 0x000, 0x1FD, 0x087, 0x0F9, 0x021, 0x093, 0x0FB, 0x02F, + 0x003, 0x068, 0x03A, 0x0E6, 0x04A, 0x028, 0x0C4, 0x036, 0x000, 0x02D, 0x0BF, 0x01D, + 0x04D, 0x0DB, 0x037, 0x065 + }; + + private static readonly byte[] SpectrumA43Bits = + { + 04, 06, 06, 07, 07, 08, 08, 09, 00, 09, 08, 08, 07, 07, 06, 06, + 05, 06, 07, 07, 07, 08, 08, 09, 00, 09, 08, 08, 07, 07, 07, 06, + 06, 07, 07, 07, 08, 08, 09, 09, 00, 09, 09, 08, 08, 07, 07, 07, + 07, 07, 07, 08, 08, 08, 09, 10, 00, 10, 09, 09, 08, 08, 07, 07, + 07, 07, 08, 08, 08, 09, 10, 10, 00, 10, 10, 09, 08, 08, 08, 07, + 08, 08, 08, 09, 09, 09, 10, 10, 00, 10, 10, 09, 09, 09, 08, 08, + 08, 09, 09, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 09, 09, + 09, 09, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 09, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 09, 09, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 09, + 08, 09, 09, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 09, 09, + 08, 08, 08, 09, 09, 09, 10, 10, 00, 10, 10, 09, 09, 09, 08, 08, + 07, 07, 08, 08, 08, 09, 10, 10, 00, 10, 10, 09, 08, 08, 08, 07, + 07, 07, 07, 08, 08, 09, 09, 10, 00, 10, 09, 08, 08, 08, 07, 07, + 06, 07, 07, 07, 08, 08, 09, 09, 00, 09, 09, 08, 08, 07, 07, 07, + 05, 06, 07, 07, 07, 08, 08, 09, 00, 09, 08, 08, 07, 07, 07, 06 + }; + + private static readonly short[] SpectrumA43Codes = + { + 0x002, 0x03E, 0x016, 0x060, 0x04E, 0x0DC, 0x04A, 0x130, 0x000, 0x131, 0x04B, 0x0DD, + 0x04F, 0x061, 0x017, 0x03F, 0x002, 0x02C, 0x076, 0x042, 0x034, 0x0CE, 0x002, 0x0E8, + 0x000, 0x0CF, 0x001, 0x0D1, 0x037, 0x045, 0x07B, 0x02F, 0x014, 0x072, 0x052, 0x01A, + 0x0E0, 0x080, 0x198, 0x01E, 0x000, 0x01D, 0x19B, 0x083, 0x0DF, 0x019, 0x055, 0x079, + 0x050, 0x03C, 0x004, 0x0C4, 0x096, 0x00C, 0x0EA, 0x34A, 0x000, 0x34F, 0x0ED, 0x1D7, + 0x095, 0x0AF, 0x003, 0x03F, 0x046, 0x026, 0x0D6, 0x092, 0x046, 0x15A, 0x3A8, 0x108, + 0x000, 0x10F, 0x3A3, 0x135, 0x039, 0x091, 0x0D9, 0x031, 0x0D4, 0x0CA, 0x072, 0x1C6, + 0x136, 0x090, 0x2B2, 0x104, 0x000, 0x103, 0x111, 0x08B, 0x133, 0x1D3, 0x071, 0x0C9, + 0x03E, 0x1B4, 0x18C, 0x0CC, 0x38A, 0x2B0, 0x106, 0x0F2, 0x000, 0x0EF, 0x101, 0x113, + 0x3A1, 0x0CB, 0x18F, 0x1B7, 0x0EE, 0x092, 0x388, 0x348, 0x10A, 0x0F4, 0x0F0, 0x0EA, + 0x000, 0x0E9, 0x0ED, 0x0F7, 0x10D, 0x34D, 0x3AB, 0x0C9, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x0EF, 0x0C8, 0x3AA, 0x34C, 0x10C, 0x0F6, 0x0EC, 0x0E8, 0x000, 0x0EB, 0x0F1, 0x0F5, + 0x10B, 0x349, 0x389, 0x093, 0x03F, 0x1B6, 0x18E, 0x0CA, 0x3A0, 0x112, 0x100, 0x0EE, + 0x000, 0x0F3, 0x107, 0x2B1, 0x38B, 0x0CD, 0x18D, 0x1B5, 0x0D5, 0x0C8, 0x070, 0x1D2, + 0x132, 0x08A, 0x110, 0x102, 0x000, 0x105, 0x2B3, 0x091, 0x137, 0x1C7, 0x073, 0x0CB, + 0x047, 0x030, 0x0D8, 0x090, 0x038, 0x134, 0x3A2, 0x10E, 0x000, 0x109, 0x3A9, 0x15B, + 0x047, 0x093, 0x0D7, 0x027, 0x051, 0x03E, 0x002, 0x0AE, 0x094, 0x1D6, 0x0EC, 0x34E, + 0x000, 0x34B, 0x0EB, 0x00D, 0x097, 0x0C5, 0x005, 0x03D, 0x015, 0x078, 0x054, 0x018, + 0x0DE, 0x082, 0x19A, 0x01C, 0x000, 0x01F, 0x199, 0x081, 0x0E1, 0x01B, 0x053, 0x073, + 0x003, 0x02E, 0x07A, 0x044, 0x036, 0x0D0, 0x000, 0x0CE, 0x000, 0x0E9, 0x003, 0x0CF, + 0x035, 0x043, 0x077, 0x02D + }; + + private static readonly byte[] SpectrumA44Bits = + { + 04, 05, 06, 07, 07, 08, 09, 10, 00, 10, 09, 08, 07, 07, 06, 05, + 05, 06, 06, 07, 07, 08, 09, 10, 00, 10, 09, 08, 07, 07, 06, 06, + 06, 06, 07, 07, 08, 09, 10, 10, 00, 10, 10, 09, 08, 07, 07, 06, + 07, 07, 07, 08, 08, 09, 10, 10, 00, 10, 10, 09, 08, 08, 07, 07, + 07, 08, 08, 08, 09, 10, 10, 10, 00, 10, 10, 10, 09, 08, 08, 07, + 08, 08, 09, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 09, 08, + 09, 09, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 09, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 09, 09, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 09, + 08, 08, 09, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 09, 08, + 07, 07, 08, 08, 09, 10, 10, 10, 00, 10, 10, 10, 09, 08, 08, 08, + 07, 07, 07, 08, 08, 09, 10, 10, 00, 10, 10, 09, 08, 08, 07, 07, + 06, 06, 07, 07, 08, 09, 10, 10, 00, 10, 10, 09, 08, 07, 07, 06, + 05, 06, 06, 07, 07, 08, 09, 10, 00, 10, 09, 08, 07, 07, 06, 06 + }; + + private static readonly short[] SpectrumA44Codes = + { + 0x00A, 0x012, 0x030, 0x06E, 0x024, 0x074, 0x0EC, 0x07E, 0x000, 0x07F, 0x0ED, 0x075, + 0x025, 0x06F, 0x031, 0x013, 0x010, 0x03C, 0x018, 0x05A, 0x002, 0x046, 0x09E, 0x07C, + 0x000, 0x079, 0x0E5, 0x04D, 0x007, 0x065, 0x01B, 0x03F, 0x02E, 0x016, 0x072, 0x01A, + 0x0D6, 0x1C6, 0x3B4, 0x066, 0x000, 0x06B, 0x3B7, 0x1D9, 0x0D5, 0x021, 0x075, 0x015, + 0x06C, 0x03E, 0x01E, 0x0CC, 0x044, 0x0F2, 0x082, 0x05C, 0x000, 0x05F, 0x087, 0x0F5, + 0x031, 0x0CF, 0x017, 0x059, 0x01C, 0x0EE, 0x0D0, 0x024, 0x1C0, 0x08E, 0x06E, 0x048, + 0x000, 0x04D, 0x06D, 0x089, 0x0F7, 0x033, 0x0D3, 0x001, 0x070, 0x028, 0x1C2, 0x0F0, + 0x08A, 0x074, 0x054, 0x040, 0x000, 0x043, 0x053, 0x073, 0x099, 0x0EF, 0x1C5, 0x02B, + 0x0E6, 0x04E, 0x08C, 0x080, 0x068, 0x058, 0x046, 0x02A, 0x000, 0x029, 0x045, 0x051, + 0x065, 0x085, 0x09B, 0x09D, 0x07A, 0x076, 0x060, 0x056, 0x04E, 0x02C, 0x024, 0x022, + 0x000, 0x021, 0x027, 0x02F, 0x04B, 0x05B, 0x063, 0x071, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x07B, 0x070, 0x062, 0x05A, 0x04A, 0x02E, 0x026, 0x020, 0x000, 0x023, 0x025, 0x02D, + 0x04F, 0x057, 0x061, 0x077, 0x0E7, 0x09C, 0x09A, 0x084, 0x064, 0x050, 0x044, 0x028, + 0x000, 0x02B, 0x047, 0x059, 0x069, 0x081, 0x08D, 0x04F, 0x071, 0x02A, 0x1C4, 0x0EE, + 0x098, 0x072, 0x052, 0x042, 0x000, 0x041, 0x055, 0x075, 0x08B, 0x0F1, 0x1C3, 0x029, + 0x01D, 0x000, 0x0D2, 0x032, 0x0F6, 0x088, 0x06C, 0x04C, 0x000, 0x049, 0x06F, 0x08F, + 0x1C1, 0x025, 0x0D1, 0x0EF, 0x06D, 0x058, 0x016, 0x0CE, 0x030, 0x0F4, 0x086, 0x05E, + 0x000, 0x05D, 0x083, 0x0F3, 0x045, 0x0CD, 0x01F, 0x03F, 0x02F, 0x014, 0x074, 0x020, + 0x0D4, 0x1D8, 0x3B6, 0x06A, 0x000, 0x067, 0x3B5, 0x1C7, 0x0D7, 0x01B, 0x073, 0x017, + 0x011, 0x03E, 0x01A, 0x064, 0x006, 0x04C, 0x0E4, 0x078, 0x000, 0x07D, 0x09F, 0x047, + 0x003, 0x05B, 0x019, 0x03D + }; + + private static readonly byte[] SpectrumA51Bits = + { + 5, 5, 5, 5, 5, 6, 6, 6, 4, 4, 5, 5, 5, 5, 5, 5, + 0, 5, 5, 5, 5, 5, 5, 4, 4, 6, 6, 6, 5, 5, 5, 5 + }; + + private static readonly short[] SpectrumA51Codes = + { + 0x19, 0x16, 0x12, 0x0E, 0x06, 0x3A, 0x38, 0x30, 0x00, 0x04, 0x1E, 0x1A, 0x14, 0x10, 0x0C, 0x04, + 0x00, 0x05, 0x0D, 0x11, 0x15, 0x1B, 0x1F, 0x05, 0x01, 0x31, 0x39, 0x3B, 0x07, 0x0F, 0x13, 0x17 + }; + + private static readonly byte[] SpectrumA52Bits = + { + 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, + 0, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4 + }; + + private static readonly short[] SpectrumA52Codes = + { + 0x09, 0x04, 0x00, 0x1E, 0x1A, 0x14, 0x0C, 0x06, 0x18, 0x16, 0x0E, 0x04, 0x3A, 0x38, 0x22, 0x20, + 0x00, 0x21, 0x23, 0x39, 0x3B, 0x05, 0x0F, 0x17, 0x19, 0x07, 0x0D, 0x15, 0x1B, 0x1F, 0x01, 0x05 + }; + + private static readonly byte[] SpectrumA53Bits = + { + 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 7, 7, 7, 7, + 0, 7, 7, 7, 7, 6, 6, 5, 5, 5, 5, 5, 5, 4, 4, 4 + }; + + private static readonly short[] SpectrumA53Codes = + { + 0x00, 0x0C, 0x08, 0x04, 0x1E, 0x16, 0x14, 0x06, 0x0C, 0x04, 0x38, 0x1E, 0x76, 0x74, 0x3A, 0x38, + 0x00, 0x39, 0x3B, 0x75, 0x77, 0x1F, 0x39, 0x05, 0x0D, 0x07, 0x15, 0x17, 0x1F, 0x05, 0x09, 0x0D + }; + + private static readonly byte[] SpectrumA54Bits = + { + 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 7, 7, 8, 8, + 0, 8, 8, 7, 7, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4 + }; + + private static readonly short[] SpectrumA54Codes = + { + 0x02, 0x0E, 0x0A, 0x08, 0x02, 0x1A, 0x0E, 0x02, 0x00, 0x30, 0x18, 0x66, 0x36, 0x34, 0xCA, 0xC8, + 0x00, 0xC9, 0xCB, 0x35, 0x37, 0x67, 0x19, 0x31, 0x01, 0x03, 0x0F, 0x1B, 0x03, 0x09, 0x0B, 0x0F + }; + + private static readonly byte[] SpectrumA61Bits = + { + 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, + 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, + 5, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6 + }; + + private static readonly short[] SpectrumA61Codes = + { + 0x35, 0x30, 0x2A, 0x28, 0x24, 0x20, 0x18, 0x0E, 0x0C, 0x7E, 0x7C, 0x72, 0x70, 0x68, 0x5E, 0x5C, + 0x04, 0x0E, 0x08, 0x00, 0x3C, 0x3A, 0x36, 0x32, 0x2C, 0x26, 0x22, 0x1A, 0x16, 0x14, 0x06, 0x04, + 0x00, 0x05, 0x07, 0x15, 0x17, 0x1B, 0x23, 0x27, 0x2D, 0x33, 0x37, 0x3B, 0x3D, 0x01, 0x09, 0x0F, + 0x05, 0x5D, 0x5F, 0x69, 0x71, 0x73, 0x7D, 0x7F, 0x0D, 0x0F, 0x19, 0x21, 0x25, 0x29, 0x2B, 0x31 + }; + + private static readonly byte[] SpectrumA62Bits = + { + 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5 + }; + + private static readonly short[] SpectrumA62Codes = + { + 0x14, 0x0E, 0x08, 0x04, 0x02, 0x3E, 0x3C, 0x38, 0x34, 0x30, 0x2A, 0x24, 0x1A, 0x18, 0x0E, 0x02, + 0x32, 0x36, 0x2C, 0x26, 0x20, 0x16, 0x0C, 0x00, 0x76, 0x74, 0x5E, 0x5C, 0x46, 0x44, 0x2A, 0x28, + 0x00, 0x29, 0x2B, 0x45, 0x47, 0x5D, 0x5F, 0x75, 0x77, 0x01, 0x0D, 0x17, 0x21, 0x27, 0x2D, 0x37, + 0x33, 0x03, 0x0F, 0x19, 0x1B, 0x25, 0x2B, 0x31, 0x35, 0x39, 0x3D, 0x3F, 0x03, 0x05, 0x09, 0x0F + }; + + private static readonly byte[] SpectrumA63Bits = + { + 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, + 0, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 7, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5 + }; + + private static readonly short[] SpectrumA63Codes = + { + 0x00, 0x1C, 0x18, 0x14, 0x10, 0x0A, 0x08, 0x02, 0x3E, 0x36, 0x2E, 0x2C, 0x24, 0x1C, 0x0E, 0x08, + 0x1E, 0x1A, 0x0C, 0x7A, 0x6A, 0x68, 0x4C, 0x32, 0x16, 0x14, 0xF2, 0xF0, 0x9E, 0x9C, 0x62, 0x60, + 0x00, 0x61, 0x63, 0x9D, 0x9F, 0xF1, 0xF3, 0x15, 0x17, 0x33, 0x4D, 0x69, 0x6B, 0x7B, 0x0D, 0x1B, + 0x1F, 0x09, 0x0F, 0x1D, 0x25, 0x2D, 0x2F, 0x37, 0x3F, 0x03, 0x09, 0x0B, 0x11, 0x15, 0x19, 0x1D + }; + + private static readonly byte[] SpectrumA64Bits = + { + 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7, + 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, + 0, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, + 6, 7, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 4 + }; + + private static readonly short[] SpectrumA64Codes = + { + 0x006, 0x002, 0x01C, 0x01A, 0x016, 0x012, 0x00E, 0x00A, 0x002, 0x03E, 0x032, 0x02A, + 0x022, 0x020, 0x010, 0x07A, 0x000, 0x078, 0x060, 0x050, 0x024, 0x006, 0x0C6, 0x0C4, + 0x0A4, 0x04E, 0x00A, 0x008, 0x14E, 0x14C, 0x09A, 0x098, 0x000, 0x099, 0x09B, 0x14D, + 0x14F, 0x009, 0x00B, 0x04F, 0x0A5, 0x0C5, 0x0C7, 0x007, 0x025, 0x051, 0x061, 0x079, + 0x001, 0x07B, 0x011, 0x021, 0x023, 0x02B, 0x033, 0x03F, 0x003, 0x00B, 0x00F, 0x013, + 0x017, 0x01B, 0x01D, 0x003 + }; + + private static readonly byte[] SpectrumA71Bits = + { + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, + 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 + }; + + private static readonly short[] SpectrumA71Codes = + { + 0x6C, 0x66, 0x62, 0x5C, 0x56, 0x50, 0x52, 0x4E, 0x48, 0x3E, 0x36, 0x34, 0x2A, 0x26, 0x1E, 0x16, + 0x0E, 0x08, 0x00, 0xF6, 0xF4, 0xEE, 0xEC, 0xE2, 0xE0, 0xDA, 0xD2, 0xD0, 0xBE, 0xBC, 0xB2, 0xB0, + 0x0C, 0x20, 0x1C, 0x16, 0x10, 0x08, 0x02, 0x7E, 0x7C, 0x78, 0x74, 0x72, 0x6E, 0x6A, 0x64, 0x60, + 0x5A, 0x54, 0x4C, 0x4A, 0x46, 0x44, 0x3C, 0x32, 0x30, 0x28, 0x24, 0x1C, 0x14, 0x0C, 0x0A, 0x02, + 0x00, 0x03, 0x0B, 0x0D, 0x15, 0x1D, 0x25, 0x29, 0x31, 0x33, 0x3D, 0x45, 0x47, 0x4B, 0x4D, 0x55, + 0x5B, 0x61, 0x65, 0x6B, 0x6F, 0x73, 0x75, 0x79, 0x7D, 0x7F, 0x03, 0x09, 0x11, 0x17, 0x1D, 0x21, + 0x0D, 0xB1, 0xB3, 0xBD, 0xBF, 0xD1, 0xD3, 0xDB, 0xE1, 0xE3, 0xED, 0xEF, 0xF5, 0xF7, 0x01, 0x09, + 0x0F, 0x17, 0x1F, 0x27, 0x2B, 0x35, 0x37, 0x3F, 0x49, 0x4F, 0x53, 0x51, 0x57, 0x5D, 0x63, 0x67 + }; + + private static readonly byte[] SpectrumA72Bits = + { + 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6 + }; + + private static readonly short[] SpectrumA72Codes = + { + 0x2A, 0x24, 0x1C, 0x18, 0x12, 0x0E, 0x0A, 0x06, 0x02, 0x7E, 0x7C, 0x7A, 0x76, 0x72, 0x70, 0x6A, + 0x68, 0x62, 0x5C, 0x5A, 0x52, 0x4E, 0x46, 0x42, 0x3C, 0x34, 0x2A, 0x28, 0x20, 0x12, 0x10, 0x08, + 0x66, 0x74, 0x6C, 0x64, 0x5E, 0x58, 0x50, 0x44, 0x40, 0x36, 0x2C, 0x22, 0x1A, 0x0A, 0x02, 0x00, + 0xF2, 0xF0, 0xDE, 0xDC, 0xC2, 0xC0, 0xAE, 0xAC, 0x9A, 0x98, 0x7E, 0x7C, 0x5E, 0x5C, 0x32, 0x30, + 0x00, 0x31, 0x33, 0x5D, 0x5F, 0x7D, 0x7F, 0x99, 0x9B, 0xAD, 0xAF, 0xC1, 0xC3, 0xDD, 0xDF, 0xF1, + 0xF3, 0x01, 0x03, 0x0B, 0x1B, 0x23, 0x2D, 0x37, 0x41, 0x45, 0x51, 0x59, 0x5F, 0x65, 0x6D, 0x75, + 0x67, 0x09, 0x11, 0x13, 0x21, 0x29, 0x2B, 0x35, 0x3D, 0x43, 0x47, 0x4F, 0x53, 0x5B, 0x5D, 0x63, + 0x69, 0x6B, 0x71, 0x73, 0x77, 0x7B, 0x7D, 0x7F, 0x03, 0x07, 0x0B, 0x0F, 0x13, 0x19, 0x1D, 0x25 + }; + + private static readonly byte[] SpectrumA73Bits = + { + 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 + }; + + private static readonly short[] SpectrumA73Codes = + { + 0x003, 0x03E, 0x038, 0x034, 0x030, 0x02C, 0x028, 0x024, 0x020, 0x01C, 0x016, 0x014, + 0x00E, 0x00A, 0x004, 0x000, 0x07A, 0x076, 0x06E, 0x06C, 0x064, 0x05E, 0x056, 0x04E, + 0x04C, 0x044, 0x036, 0x030, 0x022, 0x018, 0x012, 0x004, 0x03C, 0x03E, 0x032, 0x024, + 0x020, 0x010, 0x0F2, 0x0F0, 0x0E8, 0x0CE, 0x0BA, 0x0B8, 0x0A8, 0x08C, 0x06A, 0x04E, + 0x04C, 0x034, 0x00E, 0x00C, 0x1D6, 0x1D4, 0x19A, 0x198, 0x156, 0x154, 0x11E, 0x11C, + 0x0D2, 0x0D0, 0x06E, 0x06C, 0x000, 0x06D, 0x06F, 0x0D1, 0x0D3, 0x11D, 0x11F, 0x155, + 0x157, 0x199, 0x19B, 0x1D5, 0x1D7, 0x00D, 0x00F, 0x035, 0x04D, 0x04F, 0x06B, 0x08D, + 0x0A9, 0x0B9, 0x0BB, 0x0CF, 0x0E9, 0x0F1, 0x0F3, 0x011, 0x021, 0x025, 0x033, 0x03F, + 0x03D, 0x005, 0x013, 0x019, 0x023, 0x031, 0x037, 0x045, 0x04D, 0x04F, 0x057, 0x05F, + 0x065, 0x06D, 0x06F, 0x077, 0x07B, 0x001, 0x005, 0x00B, 0x00F, 0x015, 0x017, 0x01D, + 0x021, 0x025, 0x029, 0x02D, 0x031, 0x035, 0x039, 0x03F + }; + + private static readonly byte[] SpectrumA74Bits = + { + 05, 05, 05, 05, 06, 06, 06, 06, 06, 06, 06, 06, 06, 06, 06, 06, + 06, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 08, 08, + 07, 08, 08, 08, 08, 08, 08, 08, 08, 08, 08, 09, 09, 09, 09, 09, + 09, 09, 09, 09, 09, 09, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 00, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 09, 09, 09, 09, 09, + 09, 09, 09, 09, 09, 09, 08, 08, 08, 08, 08, 08, 08, 08, 08, 08, + 07, 08, 08, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, + 06, 06, 06, 06, 06, 06, 06, 06, 06, 06, 06, 06, 06, 05, 05, 05 + }; + + private static readonly short[] SpectrumA74Codes = + { + 0x00D, 0x00A, 0x004, 0x000, 0x03A, 0x036, 0x032, 0x030, 0x02C, 0x028, 0x026, 0x022, + 0x01E, 0x018, 0x012, 0x00E, 0x006, 0x07E, 0x07A, 0x070, 0x06A, 0x05E, 0x056, 0x054, + 0x048, 0x040, 0x038, 0x022, 0x01A, 0x00A, 0x0F8, 0x0E6, 0x008, 0x0FA, 0x0F0, 0x0D2, + 0x0BA, 0x0B8, 0x094, 0x084, 0x074, 0x042, 0x032, 0x1E6, 0x1CA, 0x1C8, 0x1A2, 0x12E, + 0x10E, 0x10C, 0x0EC, 0x082, 0x062, 0x060, 0x3CA, 0x3C8, 0x342, 0x340, 0x25A, 0x258, + 0x1DE, 0x1DC, 0x102, 0x100, 0x000, 0x101, 0x103, 0x1DD, 0x1DF, 0x259, 0x25B, 0x341, + 0x343, 0x3C9, 0x3CB, 0x061, 0x063, 0x083, 0x0ED, 0x10D, 0x10F, 0x12F, 0x1A3, 0x1C9, + 0x1CB, 0x1E7, 0x033, 0x043, 0x075, 0x085, 0x095, 0x0B9, 0x0BB, 0x0D3, 0x0F1, 0x0FB, + 0x009, 0x0E7, 0x0F9, 0x00B, 0x01B, 0x023, 0x039, 0x041, 0x049, 0x055, 0x057, 0x05F, + 0x06B, 0x071, 0x07B, 0x07F, 0x007, 0x00F, 0x013, 0x019, 0x01F, 0x023, 0x027, 0x029, + 0x02D, 0x031, 0x033, 0x037, 0x03B, 0x001, 0x005, 0x00B + }; + + private static readonly byte[] SpectrumB22Bits = + { + 00, 04, 00, 04, 04, 05, 00, 05, 00, 00, 00, 00, 04, 05, 00, 05, + 04, 07, 00, 06, 06, 09, 00, 07, 00, 00, 00, 00, 06, 09, 00, 07, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 04, 06, 00, 07, 06, 07, 00, 09, 00, 00, 00, 00, 06, 07, 00, 09, + 04, 08, 00, 08, 08, 10, 00, 10, 00, 00, 00, 00, 06, 09, 00, 09, + 05, 10, 00, 09, 09, 10, 00, 10, 00, 00, 00, 00, 07, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 06, 09, 00, 10, 09, 10, 00, 10, 00, 00, 00, 00, 07, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 04, 08, 00, 08, 06, 09, 00, 09, 00, 00, 00, 00, 08, 10, 00, 10, + 06, 10, 00, 09, 07, 10, 00, 10, 00, 00, 00, 00, 09, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 05, 09, 00, 10, 07, 10, 00, 10, 00, 00, 00, 00, 09, 10, 00, 10 + }; + + private static readonly short[] SpectrumB22Codes = + { + 0x000, 0x00E, 0x000, 0x00F, 0x008, 0x006, 0x000, 0x00B, 0x000, 0x000, 0x000, 0x000, + 0x009, 0x00A, 0x000, 0x007, 0x006, 0x00A, 0x000, 0x029, 0x006, 0x158, 0x000, 0x023, + 0x000, 0x000, 0x000, 0x000, 0x013, 0x174, 0x000, 0x021, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x007, 0x028, 0x000, 0x00B, 0x012, 0x020, 0x000, 0x175, 0x000, 0x000, 0x000, 0x000, + 0x007, 0x022, 0x000, 0x159, 0x00C, 0x0BC, 0x000, 0x0BF, 0x022, 0x2B8, 0x000, 0x2BB, + 0x000, 0x000, 0x000, 0x000, 0x00B, 0x170, 0x000, 0x15B, 0x000, 0x04E, 0x000, 0x15F, + 0x042, 0x04A, 0x000, 0x041, 0x000, 0x000, 0x000, 0x000, 0x055, 0x044, 0x000, 0x04D, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x02D, 0x172, 0x000, 0x2ED, 0x040, 0x042, 0x000, 0x047, + 0x000, 0x000, 0x000, 0x000, 0x013, 0x2EE, 0x000, 0x049, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x00D, 0x0BE, 0x000, 0x0BD, 0x00A, 0x15A, 0x000, 0x171, 0x000, 0x000, 0x000, 0x000, + 0x023, 0x2BA, 0x000, 0x2B9, 0x02C, 0x2EC, 0x000, 0x173, 0x012, 0x048, 0x000, 0x2EF, + 0x000, 0x000, 0x000, 0x000, 0x041, 0x046, 0x000, 0x043, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x001, 0x15E, 0x000, 0x04F, 0x054, 0x04C, 0x000, 0x045, 0x000, 0x000, 0x000, 0x000, + 0x043, 0x040, 0x000, 0x04B + }; + + private static readonly byte[] SpectrumB23Bits = + { + 02, 04, 00, 04, 04, 06, 00, 06, 00, 00, 00, 00, 04, 06, 00, 06, + 04, 09, 00, 07, 07, 09, 00, 08, 00, 00, 00, 00, 07, 09, 00, 08, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 04, 07, 00, 09, 07, 08, 00, 09, 00, 00, 00, 00, 07, 08, 00, 09, + 04, 08, 00, 08, 09, 10, 00, 10, 00, 00, 00, 00, 07, 10, 00, 10, + 07, 10, 00, 10, 10, 10, 00, 10, 00, 00, 00, 00, 09, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 07, 10, 00, 10, 10, 10, 00, 10, 00, 00, 00, 00, 08, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 04, 08, 00, 08, 07, 10, 00, 10, 00, 00, 00, 00, 09, 10, 00, 10, + 07, 10, 00, 10, 08, 10, 00, 10, 00, 00, 00, 00, 10, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 07, 10, 00, 10, 09, 10, 00, 10, 00, 00, 00, 00, 10, 10, 00, 10 + }; + + private static readonly short[] SpectrumB23Codes = + { + 0x003, 0x008, 0x000, 0x009, 0x002, 0x018, 0x000, 0x01B, 0x000, 0x000, 0x000, 0x000, + 0x003, 0x01A, 0x000, 0x019, 0x000, 0x17C, 0x000, 0x055, 0x056, 0x0E8, 0x000, 0x07D, + 0x000, 0x000, 0x000, 0x000, 0x059, 0x0F6, 0x000, 0x07F, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x001, 0x054, 0x000, 0x17D, 0x058, 0x07E, 0x000, 0x0F7, 0x000, 0x000, 0x000, 0x000, + 0x057, 0x07C, 0x000, 0x0E9, 0x004, 0x0A2, 0x000, 0x0A1, 0x17A, 0x1DA, 0x000, 0x1D9, + 0x000, 0x000, 0x000, 0x000, 0x053, 0x1E8, 0x000, 0x2F3, 0x05C, 0x1D6, 0x000, 0x1E7, + 0x1EA, 0x1E2, 0x000, 0x1CF, 0x000, 0x000, 0x000, 0x000, 0x17F, 0x1CA, 0x000, 0x1DD, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x05B, 0x2F0, 0x000, 0x1DF, 0x1E4, 0x1CC, 0x000, 0x1D5, + 0x000, 0x000, 0x000, 0x000, 0x071, 0x1E0, 0x000, 0x1C9, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x005, 0x0A0, 0x000, 0x0A3, 0x052, 0x2F2, 0x000, 0x1E9, 0x000, 0x000, 0x000, 0x000, + 0x17B, 0x1D8, 0x000, 0x1DB, 0x05A, 0x1DE, 0x000, 0x2F1, 0x070, 0x1C8, 0x000, 0x1E1, + 0x000, 0x000, 0x000, 0x000, 0x1E5, 0x1D4, 0x000, 0x1CD, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x05D, 0x1E6, 0x000, 0x1D7, 0x17E, 0x1DC, 0x000, 0x1CB, 0x000, 0x000, 0x000, 0x000, + 0x1EB, 0x1CE, 0x000, 0x1E3 + }; + + private static readonly byte[] SpectrumB24Bits = + { + 01, 04, 00, 04, 05, 07, 00, 07, 00, 00, 00, 00, 05, 07, 00, 07, + 05, 09, 00, 07, 08, 10, 00, 09, 00, 00, 00, 00, 07, 10, 00, 09, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 05, 07, 00, 09, 07, 09, 00, 10, 00, 00, 00, 00, 08, 09, 00, 10, + 05, 09, 00, 08, 09, 10, 00, 10, 00, 00, 00, 00, 07, 10, 00, 10, + 07, 10, 00, 10, 10, 10, 00, 10, 00, 00, 00, 00, 10, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 07, 10, 00, 10, 10, 10, 00, 10, 00, 00, 00, 00, 10, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 05, 08, 00, 09, 07, 10, 00, 10, 00, 00, 00, 00, 09, 10, 00, 10, + 07, 10, 00, 10, 10, 10, 00, 10, 00, 00, 00, 00, 10, 10, 00, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 07, 10, 00, 10, 10, 10, 00, 10, 00, 00, 00, 00, 10, 10, 00, 10 + }; + + private static readonly short[] SpectrumB24Codes = + { + 0x001, 0x000, 0x000, 0x001, 0x00A, 0x01C, 0x000, 0x033, 0x000, 0x000, 0x000, 0x000, + 0x00B, 0x032, 0x000, 0x01D, 0x008, 0x0D8, 0x000, 0x031, 0x06E, 0x0FA, 0x000, 0x0D7, + 0x000, 0x000, 0x000, 0x000, 0x011, 0x0F4, 0x000, 0x0D5, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x009, 0x030, 0x000, 0x0D9, 0x010, 0x0D4, 0x000, 0x0F5, 0x000, 0x000, 0x000, 0x000, + 0x06F, 0x0D6, 0x000, 0x0FB, 0x00E, 0x0DA, 0x000, 0x025, 0x0D2, 0x0D4, 0x000, 0x0DB, + 0x000, 0x000, 0x000, 0x000, 0x017, 0x0FE, 0x000, 0x0FD, 0x014, 0x0DC, 0x000, 0x0F9, + 0x0F2, 0x0D6, 0x000, 0x09B, 0x000, 0x000, 0x000, 0x000, 0x1A3, 0x09C, 0x000, 0x0D3, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x019, 0x0F6, 0x000, 0x0D9, 0x0F0, 0x09E, 0x000, 0x0D1, + 0x000, 0x000, 0x000, 0x000, 0x1A1, 0x0DE, 0x000, 0x099, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x00F, 0x024, 0x000, 0x0DB, 0x016, 0x0FC, 0x000, 0x0FF, 0x000, 0x000, 0x000, 0x000, + 0x0D3, 0x0DA, 0x000, 0x0D5, 0x018, 0x0D8, 0x000, 0x0F7, 0x1A0, 0x098, 0x000, 0x0DF, + 0x000, 0x000, 0x000, 0x000, 0x0F1, 0x0D0, 0x000, 0x09F, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x015, 0x0F8, 0x000, 0x0DD, 0x1A2, 0x0D2, 0x000, 0x09D, 0x000, 0x000, 0x000, 0x000, + 0x0F3, 0x09A, 0x000, 0x0D7 + }; + + private static readonly byte[] SpectrumB32Bits = + { + 2, 4, 5, 6, 0, 6, 5, 4, 5, 6, 6, 7, 0, 6, 5, 6, + 5, 6, 7, 7, 0, 8, 7, 6, 6, 7, 8, 9, 0, 9, 8, 7, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 0, 9, 8, 7, + 5, 6, 7, 8, 0, 7, 7, 6, 5, 6, 5, 6, 0, 7, 6, 6 + }; + + private static readonly short[] SpectrumB32Codes = + { + 0x001, 0x002, 0x01E, 0x02A, 0x000, 0x02B, 0x01F, 0x003, 0x016, 0x020, 0x03A, 0x064, + 0x000, 0x005, 0x001, 0x023, 0x01A, 0x026, 0x070, 0x00C, 0x000, 0x0CF, 0x073, 0x031, + 0x024, 0x00E, 0x0CC, 0x146, 0x000, 0x145, 0x0A1, 0x053, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x025, 0x052, 0x0A0, 0x144, 0x000, 0x147, 0x0CD, 0x00F, + 0x01B, 0x030, 0x072, 0x0CE, 0x000, 0x00D, 0x071, 0x027, 0x017, 0x022, 0x000, 0x004, + 0x000, 0x065, 0x03B, 0x021 + }; + + private static readonly byte[] SpectrumB33Bits = + { + 02, 04, 05, 07, 00, 07, 05, 04, 04, 05, 06, 08, 00, 07, 06, 05, + 05, 06, 07, 09, 00, 08, 07, 06, 07, 08, 09, 10, 00, 10, 09, 08, + 00, 00, 00, 00, 00, 00, 00, 00, 07, 08, 09, 10, 00, 10, 09, 08, + 05, 06, 07, 08, 00, 09, 07, 06, 04, 05, 06, 07, 00, 08, 06, 05 + }; + + private static readonly short[] SpectrumB33Codes = + { + 0x003, 0x008, 0x014, 0x05E, 0x000, 0x05F, 0x015, 0x009, 0x004, 0x002, 0x01C, 0x0BA, + 0x000, 0x011, 0x01F, 0x001, 0x00C, 0x00C, 0x014, 0x166, 0x000, 0x02D, 0x013, 0x00F, + 0x05A, 0x0B0, 0x05E, 0x0B8, 0x000, 0x0BB, 0x165, 0x0B9, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x05B, 0x0B8, 0x164, 0x0BA, 0x000, 0x0B9, 0x05F, 0x0B1, + 0x00D, 0x00E, 0x012, 0x02C, 0x000, 0x167, 0x015, 0x00D, 0x005, 0x000, 0x01E, 0x010, + 0x000, 0x0BB, 0x01D, 0x003 + }; + + private static readonly byte[] SpectrumB34Bits = + { + 01, 04, 06, 08, 00, 08, 06, 04, 04, 06, 07, 09, 00, 08, 07, 06, + 06, 07, 08, 10, 00, 10, 08, 07, 08, 09, 10, 10, 00, 10, 10, 09, + 00, 00, 00, 00, 00, 00, 00, 00, 08, 09, 10, 10, 00, 10, 10, 09, + 06, 07, 08, 10, 00, 10, 08, 07, 04, 06, 07, 08, 00, 09, 07, 06 + }; + + private static readonly short[] SpectrumB34Codes = + { + 0x000, 0x00A, 0x038, 0x0EE, 0x000, 0x0EF, 0x039, 0x00B, 0x008, 0x03C, 0x06E, 0x1D8, + 0x000, 0x0C1, 0x075, 0x03F, 0x032, 0x068, 0x0C4, 0x358, 0x000, 0x30F, 0x0C7, 0x06D, + 0x0D4, 0x1AE, 0x30C, 0x308, 0x000, 0x30B, 0x35B, 0x1DB, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x0D5, 0x1DA, 0x35A, 0x30A, 0x000, 0x309, 0x30D, 0x1AF, + 0x033, 0x06C, 0x0C6, 0x30E, 0x000, 0x359, 0x0C5, 0x069, 0x009, 0x03E, 0x074, 0x0C0, + 0x000, 0x1D9, 0x06F, 0x03D + }; + + private static readonly byte[] SpectrumB42Bits = + { + 04, 05, 06, 08, 06, 07, 08, 08, 00, 08, 08, 07, 06, 08, 06, 05, + 05, 06, 07, 08, 07, 07, 08, 09, 00, 08, 08, 07, 07, 08, 07, 06, + 07, 07, 08, 09, 07, 08, 09, 09, 00, 09, 09, 08, 07, 09, 08, 07, + 08, 09, 09, 10, 08, 08, 09, 10, 00, 10, 09, 08, 08, 10, 09, 08, + 06, 07, 08, 08, 09, 09, 10, 10, 00, 10, 10, 09, 09, 08, 08, 07, + 07, 07, 08, 09, 09, 10, 10, 10, 00, 10, 10, 10, 09, 09, 08, 07, + 08, 08, 09, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 09, 08, + 08, 09, 09, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 09, 09, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 08, 09, 09, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 09, 09, + 08, 08, 09, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 09, 08, + 07, 07, 08, 09, 09, 10, 10, 10, 00, 10, 10, 10, 09, 09, 08, 07, + 06, 07, 08, 08, 09, 09, 10, 10, 00, 10, 10, 09, 09, 08, 08, 07, + 08, 08, 09, 10, 08, 08, 09, 10, 00, 10, 09, 08, 08, 10, 09, 09, + 07, 07, 08, 09, 07, 08, 09, 09, 00, 09, 09, 08, 07, 09, 08, 07, + 05, 06, 07, 08, 07, 07, 08, 08, 00, 09, 08, 07, 07, 08, 07, 06 + }; + + private static readonly short[] SpectrumB42Codes = + { + 0x00E, 0x018, 0x010, 0x0F0, 0x024, 0x05A, 0x0F6, 0x078, 0x000, 0x079, 0x0F7, 0x05B, + 0x025, 0x0F1, 0x011, 0x019, 0x00C, 0x014, 0x01C, 0x036, 0x05C, 0x012, 0x09E, 0x1E4, + 0x000, 0x00B, 0x0A9, 0x03B, 0x05F, 0x071, 0x019, 0x017, 0x06E, 0x000, 0x03E, 0x114, + 0x002, 0x0B0, 0x1AA, 0x07A, 0x000, 0x099, 0x1E7, 0x0B3, 0x00B, 0x131, 0x07F, 0x00D, + 0x0D8, 0x1FE, 0x112, 0x22E, 0x086, 0x010, 0x134, 0x35C, 0x000, 0x35F, 0x133, 0x013, + 0x081, 0x22D, 0x119, 0x07B, 0x00A, 0x050, 0x0F8, 0x04E, 0x1B4, 0x154, 0x3EC, 0x0D2, + 0x000, 0x0D7, 0x3D7, 0x137, 0x1FD, 0x073, 0x0FD, 0x057, 0x052, 0x010, 0x08E, 0x1E8, + 0x11A, 0x3EE, 0x0F2, 0x03C, 0x000, 0x03F, 0x0F1, 0x3D5, 0x111, 0x1F5, 0x09D, 0x025, + 0x0D2, 0x082, 0x1A0, 0x0F8, 0x36E, 0x0D4, 0x072, 0x03A, 0x000, 0x027, 0x071, 0x07D, + 0x36D, 0x0FB, 0x1AD, 0x085, 0x00C, 0x1A8, 0x03C, 0x346, 0x0D0, 0x076, 0x024, 0x020, + 0x000, 0x023, 0x039, 0x075, 0x07F, 0x345, 0x09B, 0x157, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x00D, 0x156, 0x09A, 0x344, 0x07E, 0x074, 0x038, 0x022, 0x000, 0x021, 0x025, 0x077, + 0x0D1, 0x347, 0x03D, 0x1A9, 0x0D3, 0x084, 0x1AC, 0x0FA, 0x36C, 0x07C, 0x070, 0x026, + 0x000, 0x03B, 0x073, 0x0D5, 0x36F, 0x0F9, 0x1A1, 0x083, 0x053, 0x024, 0x09C, 0x1F4, + 0x110, 0x3D4, 0x0F0, 0x03E, 0x000, 0x03D, 0x0F3, 0x3EF, 0x11B, 0x1E9, 0x08F, 0x011, + 0x00B, 0x056, 0x0FC, 0x072, 0x1FC, 0x136, 0x3D6, 0x0D6, 0x000, 0x0D3, 0x3ED, 0x155, + 0x1B5, 0x04F, 0x0F9, 0x051, 0x0D9, 0x07A, 0x118, 0x22C, 0x080, 0x012, 0x132, 0x35E, + 0x000, 0x35D, 0x135, 0x011, 0x087, 0x22F, 0x113, 0x1FF, 0x06F, 0x00C, 0x07E, 0x130, + 0x00A, 0x0B2, 0x1E6, 0x098, 0x000, 0x07B, 0x1AB, 0x0B1, 0x003, 0x115, 0x03F, 0x001, + 0x00D, 0x016, 0x018, 0x070, 0x05E, 0x03A, 0x0A8, 0x00A, 0x000, 0x1E5, 0x09F, 0x013, + 0x05D, 0x037, 0x01D, 0x015 + }; + + private static readonly byte[] SpectrumB43Bits = + { + 02, 05, 06, 07, 07, 08, 08, 09, 00, 09, 08, 08, 07, 07, 06, 05, + 05, 06, 07, 08, 07, 08, 09, 10, 00, 10, 09, 08, 07, 08, 07, 06, + 06, 07, 08, 09, 08, 09, 10, 10, 00, 10, 10, 09, 08, 09, 08, 07, + 07, 08, 09, 10, 09, 09, 10, 10, 00, 10, 10, 10, 09, 10, 09, 08, + 07, 08, 08, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 08, 07, + 08, 08, 09, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 09, 08, + 09, 09, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 09, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 09, 09, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 09, + 08, 08, 09, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 09, 08, + 07, 07, 08, 09, 10, 10, 10, 10, 00, 10, 10, 10, 10, 09, 08, 08, + 07, 08, 09, 10, 09, 10, 10, 10, 00, 10, 10, 09, 09, 10, 09, 08, + 06, 07, 08, 09, 08, 09, 10, 10, 00, 10, 10, 09, 08, 09, 08, 07, + 05, 06, 07, 08, 07, 08, 09, 10, 00, 10, 09, 08, 07, 08, 07, 06 + }; + + private static readonly short[] SpectrumB43Codes = + { + 0x001, 0x01E, 0x022, 0x018, 0x064, 0x0EC, 0x008, 0x100, 0x000, 0x101, 0x009, 0x0ED, + 0x065, 0x019, 0x023, 0x01F, 0x01A, 0x030, 0x056, 0x09A, 0x00A, 0x090, 0x12C, 0x0A6, + 0x000, 0x0A9, 0x12F, 0x093, 0x00F, 0x09F, 0x059, 0x039, 0x00E, 0x054, 0x0BC, 0x19E, + 0x082, 0x176, 0x0AC, 0x088, 0x000, 0x08B, 0x0AF, 0x19D, 0x095, 0x1D1, 0x0BF, 0x051, + 0x002, 0x098, 0x1D4, 0x0B8, 0x170, 0x046, 0x090, 0x060, 0x000, 0x067, 0x095, 0x0BD, + 0x173, 0x0B5, 0x1D3, 0x09D, 0x052, 0x0EE, 0x034, 0x174, 0x0BA, 0x09C, 0x080, 0x044, + 0x000, 0x047, 0x06D, 0x099, 0x0BF, 0x16F, 0x085, 0x001, 0x0CC, 0x036, 0x16C, 0x0B0, + 0x09A, 0x084, 0x04E, 0x03E, 0x000, 0x037, 0x04B, 0x06B, 0x0A1, 0x0B3, 0x16B, 0x087, + 0x1D6, 0x102, 0x0A4, 0x092, 0x068, 0x04C, 0x034, 0x030, 0x000, 0x02D, 0x03D, 0x049, + 0x083, 0x097, 0x0AB, 0x169, 0x0B6, 0x09E, 0x06E, 0x064, 0x040, 0x038, 0x02E, 0x02A, + 0x000, 0x029, 0x033, 0x03B, 0x043, 0x063, 0x087, 0x0A3, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x0B7, 0x0A2, 0x086, 0x062, 0x042, 0x03A, 0x032, 0x028, 0x000, 0x02B, 0x02F, 0x039, + 0x041, 0x065, 0x06F, 0x09F, 0x1D7, 0x168, 0x0AA, 0x096, 0x082, 0x048, 0x03C, 0x02C, + 0x000, 0x031, 0x035, 0x04D, 0x069, 0x093, 0x0A5, 0x103, 0x0CD, 0x086, 0x16A, 0x0B2, + 0x0A0, 0x06A, 0x04A, 0x036, 0x000, 0x03F, 0x04F, 0x085, 0x09B, 0x0B1, 0x16D, 0x037, + 0x053, 0x000, 0x084, 0x16E, 0x0BE, 0x098, 0x06C, 0x046, 0x000, 0x045, 0x081, 0x09D, + 0x0BB, 0x175, 0x035, 0x0EF, 0x003, 0x09C, 0x1D2, 0x0B4, 0x172, 0x0BC, 0x094, 0x066, + 0x000, 0x061, 0x091, 0x047, 0x171, 0x0B9, 0x1D5, 0x099, 0x00F, 0x050, 0x0BE, 0x1D0, + 0x094, 0x19C, 0x0AE, 0x08A, 0x000, 0x089, 0x0AD, 0x177, 0x083, 0x19F, 0x0BD, 0x055, + 0x01B, 0x038, 0x058, 0x09E, 0x00E, 0x092, 0x12E, 0x0A8, 0x000, 0x0A7, 0x12D, 0x091, + 0x00B, 0x09B, 0x057, 0x031 + }; + + private static readonly byte[] SpectrumB44Bits = + { + 02, 04, 06, 07, 07, 08, 10, 10, 00, 10, 10, 08, 07, 07, 06, 04, + 05, 05, 07, 08, 08, 10, 10, 10, 00, 10, 10, 10, 08, 08, 07, 05, + 06, 07, 08, 09, 09, 10, 10, 10, 00, 10, 10, 10, 10, 09, 08, 07, + 08, 08, 09, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 08, + 08, 08, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 08, + 09, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 09, 10, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 10, + 08, 08, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 10, 08, + 08, 08, 10, 10, 10, 10, 10, 10, 00, 10, 10, 10, 10, 10, 09, 08, + 06, 07, 08, 09, 10, 10, 10, 10, 00, 10, 10, 10, 09, 09, 08, 07, + 05, 05, 07, 08, 08, 10, 10, 10, 00, 10, 10, 10, 08, 08, 07, 05 + }; + + private static readonly short[] SpectrumB44Codes = + { + 0x002, 0x002, 0x030, 0x000, 0x002, 0x00C, 0x1D2, 0x1AE, 0x000, 0x1AF, 0x1D3, 0x00D, + 0x003, 0x001, 0x031, 0x003, 0x01E, 0x002, 0x070, 0x0C8, 0x07E, 0x1E8, 0x1C0, 0x176, + 0x000, 0x17F, 0x1C3, 0x1EB, 0x0CF, 0x0D3, 0x073, 0x009, 0x018, 0x06A, 0x0EC, 0x1DE, + 0x1A2, 0x1CA, 0x1AA, 0x164, 0x000, 0x16D, 0x1AD, 0x1D1, 0x1EF, 0x1DD, 0x0EB, 0x06D, + 0x0E8, 0x0CA, 0x1BE, 0x1CE, 0x1DA, 0x1B6, 0x170, 0x154, 0x000, 0x153, 0x173, 0x1B1, + 0x1D7, 0x1D5, 0x343, 0x0CD, 0x0DC, 0x078, 0x340, 0x1CC, 0x1BA, 0x1A8, 0x156, 0x148, + 0x000, 0x145, 0x15F, 0x1A1, 0x1BD, 0x1D9, 0x1ED, 0x07D, 0x1BC, 0x1DC, 0x1C4, 0x1B2, + 0x17C, 0x15A, 0x14A, 0x03A, 0x000, 0x039, 0x147, 0x16B, 0x17B, 0x1B5, 0x1C9, 0x1DF, + 0x1C6, 0x1B8, 0x1A2, 0x168, 0x160, 0x14C, 0x02E, 0x024, 0x000, 0x027, 0x03D, 0x151, + 0x15D, 0x16F, 0x1A7, 0x1BF, 0x1A4, 0x174, 0x162, 0x14E, 0x140, 0x02C, 0x02A, 0x022, + 0x000, 0x021, 0x029, 0x03F, 0x143, 0x159, 0x167, 0x179, 0x000, 0x000, 0x000, 0x000, + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x1A5, 0x178, 0x166, 0x158, 0x142, 0x03E, 0x028, 0x020, 0x000, 0x023, 0x02B, 0x02D, + 0x141, 0x14F, 0x163, 0x175, 0x1C7, 0x1BE, 0x1A6, 0x16E, 0x15C, 0x150, 0x03C, 0x026, + 0x000, 0x025, 0x02F, 0x14D, 0x161, 0x169, 0x1A3, 0x1B9, 0x1BD, 0x1DE, 0x1C8, 0x1B4, + 0x17A, 0x16A, 0x146, 0x038, 0x000, 0x03B, 0x14B, 0x15B, 0x17D, 0x1B3, 0x1C5, 0x1DD, + 0x0DD, 0x07C, 0x1EC, 0x1D8, 0x1BC, 0x1A0, 0x15E, 0x144, 0x000, 0x149, 0x157, 0x1A9, + 0x1BB, 0x1CD, 0x341, 0x079, 0x0E9, 0x0CC, 0x342, 0x1D4, 0x1D6, 0x1B0, 0x172, 0x152, + 0x000, 0x155, 0x171, 0x1B7, 0x1DB, 0x1CF, 0x1BF, 0x0CB, 0x019, 0x06C, 0x0EA, 0x1DC, + 0x1EE, 0x1D0, 0x1AC, 0x16C, 0x000, 0x165, 0x1AB, 0x1CB, 0x1A3, 0x1DF, 0x0ED, 0x06B, + 0x01F, 0x008, 0x072, 0x0D2, 0x0CE, 0x1EA, 0x1C2, 0x17E, 0x000, 0x177, 0x1C1, 0x1E9, + 0x07F, 0x0C9, 0x071, 0x003 + }; + + private static readonly byte[] SpectrumB52Bits = + { + 3, 4, 4, 4, 5, 5, 6, 6, 5, 5, 5, 6, 6, 6, 7, 7, + 0, 7, 7, 6, 6, 6, 5, 5, 5, 6, 6, 5, 5, 4, 4, 4 + }; + + private static readonly short[] SpectrumB52Codes = + { + 0x06, 0x0E, 0x06, 0x00, 0x0A, 0x04, 0x2C, 0x12, 0x14, 0x10, 0x06, 0x2E, 0x24, 0x10, 0x4E, 0x4C, + 0x00, 0x4D, 0x4F, 0x11, 0x25, 0x2F, 0x07, 0x11, 0x15, 0x13, 0x2D, 0x05, 0x0B, 0x01, 0x07, 0x0F + }; + + private static readonly byte[] SpectrumB53Bits = + { + 2, 3, 4, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 8, 8, + 0, 8, 8, 7, 7, 7, 6, 6, 6, 6, 6, 6, 5, 5, 4, 3 + }; + + private static readonly short[] SpectrumB53Codes = + { + 0x02, 0x00, 0x06, 0x1C, 0x18, 0x3E, 0x16, 0x10, 0x3C, 0x36, 0x14, 0x6A, 0x26, 0x24, 0xD2, 0xD0, + 0x00, 0xD1, 0xD3, 0x25, 0x27, 0x6B, 0x15, 0x37, 0x3D, 0x11, 0x17, 0x3F, 0x19, 0x1D, 0x07, 0x01 + }; + + private static readonly byte[] SpectrumB54Bits = + { + 2, 3, 4, 4, 5, 6, 6, 7, 6, 6, 7, 8, 8, 8, 9, 9, + 0, 9, 9, 8, 8, 8, 7, 6, 6, 7, 6, 6, 5, 4, 4, 3 + }; + + private static readonly short[] SpectrumB54Codes = + { + 0x003, 0x002, 0x008, 0x000, 0x014, 0x02E, 0x00E, 0x05A, 0x00A, 0x008, 0x01A, 0x0B2, + 0x032, 0x030, 0x162, 0x160, 0x000, 0x161, 0x163, 0x031, 0x033, 0x0B3, 0x01B, 0x009, + 0x00B, 0x05B, 0x00F, 0x02F, 0x015, 0x001, 0x009, 0x003 + }; + + private static readonly byte[] SpectrumB62Bits = + { + 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, + 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, + 0, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, + 6, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 4 + }; + + private static readonly short[] SpectrumB62Codes = + { + 0x0D, 0x06, 0x1C, 0x14, 0x0A, 0x04, 0x3E, 0x2E, 0x22, 0x0E, 0x06, 0x00, 0x5A, 0x4E, 0x40, 0x20, + 0x30, 0x32, 0x24, 0x12, 0x0C, 0x02, 0x78, 0x58, 0x42, 0x22, 0x0A, 0x08, 0xF6, 0xF4, 0x9A, 0x98, + 0x00, 0x99, 0x9B, 0xF5, 0xF7, 0x09, 0x0B, 0x23, 0x43, 0x59, 0x79, 0x03, 0x0D, 0x13, 0x25, 0x33, + 0x31, 0x21, 0x41, 0x4F, 0x5B, 0x01, 0x07, 0x0F, 0x23, 0x2F, 0x3F, 0x05, 0x0B, 0x15, 0x1D, 0x07 + }; + + private static readonly byte[] SpectrumB63Bits = + { + 3, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, + 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, + 0, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, 6, + 6, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 4, 4 + }; + + private static readonly short[] SpectrumB63Codes = + { + 0x006, 0x00E, 0x004, 0x014, 0x010, 0x006, 0x000, 0x026, 0x01C, 0x018, 0x004, 0x05C, + 0x04A, 0x03C, 0x016, 0x0BC, 0x006, 0x008, 0x058, 0x03E, 0x036, 0x014, 0x0B6, 0x0B4, + 0x090, 0x068, 0x17E, 0x17C, 0x126, 0x124, 0x0D6, 0x0D4, 0x000, 0x0D5, 0x0D7, 0x125, + 0x127, 0x17D, 0x17F, 0x069, 0x091, 0x0B5, 0x0B7, 0x015, 0x037, 0x03F, 0x059, 0x009, + 0x007, 0x0BD, 0x017, 0x03D, 0x04B, 0x05D, 0x005, 0x019, 0x01D, 0x027, 0x001, 0x007, + 0x011, 0x015, 0x005, 0x00F + }; + + private static readonly byte[] SpectrumB64Bits = + { + 03, 03, 04, 05, 05, 05, 06, 06, 06, 06, 07, 07, 07, 07, 07, 08, + 07, 07, 07, 08, 08, 08, 09, 09, 09, 09, 09, 09, 10, 10, 10, 10, + 00, 10, 10, 10, 10, 09, 09, 09, 09, 09, 09, 08, 08, 08, 07, 07, + 07, 08, 07, 07, 07, 07, 07, 06, 06, 06, 06, 05, 05, 05, 04, 03 + }; + + private static readonly short[] SpectrumB64Codes = + { + 0x007, 0x000, 0x008, 0x01A, 0x014, 0x00C, 0x032, 0x02E, 0x01E, 0x014, 0x062, 0x05A, + 0x03A, 0x026, 0x020, 0x0B2, 0x038, 0x02C, 0x022, 0x0C0, 0x05E, 0x04A, 0x186, 0x184, + 0x160, 0x0BA, 0x092, 0x090, 0x2C6, 0x2C4, 0x172, 0x170, 0x000, 0x171, 0x173, 0x2C5, + 0x2C7, 0x091, 0x093, 0x0BB, 0x161, 0x185, 0x187, 0x04B, 0x05F, 0x0C1, 0x023, 0x02D, + 0x039, 0x0B3, 0x021, 0x027, 0x03B, 0x05B, 0x063, 0x015, 0x01F, 0x02F, 0x033, 0x00D, + 0x015, 0x01B, 0x009, 0x001 + }; + + private static readonly byte[] SpectrumB72Bits = + { + 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5 + }; + + private static readonly short[] SpectrumB72Codes = + { + 0x01E, 0x016, 0x00C, 0x000, 0x038, 0x032, 0x028, 0x022, 0x01C, 0x012, 0x00E, 0x006, + 0x076, 0x06C, 0x060, 0x04E, 0x03E, 0x02A, 0x022, 0x01A, 0x012, 0x00A, 0x0FC, 0x0DC, + 0x0C6, 0x0A8, 0x094, 0x086, 0x058, 0x042, 0x040, 0x02A, 0x068, 0x07C, 0x06A, 0x056, + 0x048, 0x040, 0x02E, 0x028, 0x016, 0x010, 0x008, 0x0EA, 0x0DE, 0x0AA, 0x09A, 0x096, + 0x07A, 0x078, 0x05A, 0x032, 0x030, 0x028, 0x1FE, 0x1FC, 0x1D2, 0x1D0, 0x18A, 0x188, + 0x132, 0x130, 0x10A, 0x108, 0x000, 0x109, 0x10B, 0x131, 0x133, 0x189, 0x18B, 0x1D1, + 0x1D3, 0x1FD, 0x1FF, 0x029, 0x031, 0x033, 0x05B, 0x079, 0x07B, 0x097, 0x09B, 0x0AB, + 0x0DF, 0x0EB, 0x009, 0x011, 0x017, 0x029, 0x02F, 0x041, 0x049, 0x057, 0x06B, 0x07D, + 0x069, 0x02B, 0x041, 0x043, 0x059, 0x087, 0x095, 0x0A9, 0x0C7, 0x0DD, 0x0FD, 0x00B, + 0x013, 0x01B, 0x023, 0x02B, 0x03F, 0x04F, 0x061, 0x06D, 0x077, 0x007, 0x00F, 0x013, + 0x01D, 0x023, 0x029, 0x033, 0x039, 0x001, 0x00D, 0x017 + }; + + private static readonly byte[] SpectrumB73Bits = + { + 03, 04, 05, 05, 05, 06, 06, 06, 06, 06, 06, 07, 07, 07, 07, 07, + 07, 07, 07, 07, 08, 08, 08, 08, 08, 08, 08, 08, 08, 08, 09, 09, + 08, 07, 08, 08, 08, 08, 08, 08, 08, 08, 09, 09, 09, 09, 09, 09, + 09, 09, 09, 09, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 00, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 09, 09, 09, + 09, 09, 09, 09, 09, 09, 09, 08, 08, 08, 08, 08, 08, 08, 08, 07, + 08, 09, 09, 08, 08, 08, 08, 08, 08, 08, 08, 08, 08, 07, 07, 07, + 07, 07, 07, 07, 07, 07, 06, 06, 06, 06, 06, 06, 05, 05, 05, 04 + }; + + private static readonly short[] SpectrumB73Codes = + { + 0x000, 0x006, 0x018, 0x010, 0x004, 0x03A, 0x034, 0x02A, 0x026, 0x014, 0x010, 0x07E, + 0x072, 0x06E, 0x05C, 0x052, 0x04A, 0x02C, 0x024, 0x018, 0x0F4, 0x0E0, 0x0DA, 0x0B6, + 0x0B2, 0x0A0, 0x05E, 0x04E, 0x038, 0x034, 0x1E6, 0x1B2, 0x0FA, 0x01E, 0x0F8, 0x0F0, + 0x0BE, 0x0B4, 0x0A2, 0x090, 0x04C, 0x03A, 0x1EE, 0x1E4, 0x1C6, 0x1B0, 0x178, 0x162, + 0x126, 0x124, 0x0B8, 0x06C, 0x3DA, 0x3D8, 0x38A, 0x388, 0x2F6, 0x2F4, 0x2C2, 0x2C0, + 0x176, 0x174, 0x0DC, 0x0DE, 0x000, 0x0DF, 0x0DD, 0x175, 0x177, 0x2C1, 0x2C3, 0x2F5, + 0x2F7, 0x389, 0x38B, 0x3D9, 0x3DB, 0x06D, 0x0B9, 0x125, 0x127, 0x163, 0x179, 0x1B1, + 0x1C7, 0x1E5, 0x1EF, 0x03B, 0x04D, 0x091, 0x0A3, 0x0B5, 0x0BF, 0x0F1, 0x0F9, 0x01F, + 0x0FB, 0x1B3, 0x1E7, 0x035, 0x039, 0x04F, 0x05F, 0x0A1, 0x0B3, 0x0B7, 0x0DB, 0x0E1, + 0x0F5, 0x019, 0x025, 0x02D, 0x04B, 0x053, 0x05D, 0x06F, 0x073, 0x07F, 0x011, 0x015, + 0x027, 0x02B, 0x035, 0x03B, 0x005, 0x011, 0x019, 0x007 + }; + + private static readonly byte[] SpectrumB74Bits = + { + 03, 04, 05, 05, 05, 05, 06, 06, 06, 06, 06, 06, 07, 07, 07, 07, + 07, 07, 07, 07, 08, 08, 08, 08, 08, 08, 08, 08, 08, 09, 09, 09, + 08, 08, 08, 08, 08, 09, 09, 09, 09, 09, 09, 09, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 00, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 09, 09, 09, 09, 09, 09, 09, 08, 08, 08, 08, + 08, 09, 09, 09, 08, 08, 08, 08, 08, 08, 08, 08, 08, 07, 07, 07, + 07, 07, 07, 07, 07, 06, 06, 06, 06, 06, 06, 05, 05, 05, 05, 04 + }; + + private static readonly short[] SpectrumB74Codes = + { + 0x001, 0x008, 0x01E, 0x018, 0x00C, 0x002, 0x03A, 0x034, 0x02C, 0x01E, 0x016, 0x012, + 0x072, 0x06E, 0x05E, 0x056, 0x050, 0x038, 0x022, 0x004, 0x0E2, 0x0DA, 0x0BA, 0x0A8, + 0x076, 0x054, 0x050, 0x002, 0x000, 0x1C0, 0x1B0, 0x156, 0x0A4, 0x0A6, 0x074, 0x052, + 0x004, 0x1C2, 0x1B2, 0x170, 0x154, 0x0AE, 0x0AC, 0x086, 0x2E6, 0x2E4, 0x10A, 0x108, + 0x106, 0x104, 0x102, 0x100, 0x03E, 0x03A, 0x03C, 0x038, 0x036, 0x034, 0x032, 0x030, + 0x01E, 0x01A, 0x01C, 0x018, 0x000, 0x019, 0x01D, 0x01B, 0x01F, 0x031, 0x033, 0x035, + 0x037, 0x039, 0x03D, 0x03B, 0x03F, 0x101, 0x103, 0x105, 0x107, 0x109, 0x10B, 0x2E5, + 0x2E7, 0x087, 0x0AD, 0x0AF, 0x155, 0x171, 0x1B3, 0x1C3, 0x005, 0x053, 0x075, 0x0A7, + 0x0A5, 0x157, 0x1B1, 0x1C1, 0x001, 0x003, 0x051, 0x055, 0x077, 0x0A9, 0x0BB, 0x0DB, + 0x0E3, 0x005, 0x023, 0x039, 0x051, 0x057, 0x05F, 0x06F, 0x073, 0x013, 0x017, 0x01F, + 0x02D, 0x035, 0x03B, 0x003, 0x00D, 0x019, 0x01F, 0x009 + }; + + public static readonly byte[][] HuffmanScaleFactorsABits = + { + null, + ScaleFactorsA1Bits, ScaleFactorsA2Bits, ScaleFactorsA3Bits, + ScaleFactorsA4Bits, ScaleFactorsA5Bits, ScaleFactorsA6Bits + }; + + public static readonly short[][] HuffmanScaleFactorsACodes = + { + null, + ScaleFactorsA1Codes, ScaleFactorsA2Codes, ScaleFactorsA3Codes, + ScaleFactorsA4Codes, ScaleFactorsA5Codes, ScaleFactorsA6Codes + }; + + public static readonly byte[][] HuffmanScaleFactorsBBits = + { + null, null, + ScaleFactorsB2Bits, ScaleFactorsB3Bits, ScaleFactorsB4Bits, ScaleFactorsB5Bits + }; + + public static readonly short[][] HuffmanScaleFactorsBCodes = + { + null, null, + ScaleFactorsB2Codes, ScaleFactorsB3Codes, ScaleFactorsB4Codes, ScaleFactorsB5Codes + }; + + public static readonly byte[] HuffmanScaleFactorsGroupSizes = { 0, 0, 0, 0, 0, 0, 0 }; + + public static readonly byte[][][] HuffmanSpectrumABits = + { + null, + null, + new[] {SpectrumA21Bits, SpectrumA22Bits, SpectrumA23Bits, SpectrumA24Bits}, + new[] {SpectrumA31Bits, SpectrumA32Bits, SpectrumA33Bits, SpectrumA34Bits}, + new[] {SpectrumA41Bits, SpectrumA42Bits, SpectrumA43Bits, SpectrumA44Bits}, + new[] {SpectrumA51Bits, SpectrumA52Bits, SpectrumA53Bits, SpectrumA54Bits}, + new[] {SpectrumA61Bits, SpectrumA62Bits, SpectrumA63Bits, SpectrumA64Bits}, + new[] {SpectrumA71Bits, SpectrumA72Bits, SpectrumA73Bits, SpectrumA74Bits} + }; + + public static readonly short[][][] HuffmanSpectrumACodes = + { + null, + null, + new[] {SpectrumA21Codes, SpectrumA22Codes, SpectrumA23Codes, SpectrumA24Codes}, + new[] {SpectrumA31Codes, SpectrumA32Codes, SpectrumA33Codes, SpectrumA34Codes}, + new[] {SpectrumA41Codes, SpectrumA42Codes, SpectrumA43Codes, SpectrumA44Codes}, + new[] {SpectrumA51Codes, SpectrumA52Codes, SpectrumA53Codes, SpectrumA54Codes}, + new[] {SpectrumA61Codes, SpectrumA62Codes, SpectrumA63Codes, SpectrumA64Codes}, + new[] {SpectrumA71Codes, SpectrumA72Codes, SpectrumA73Codes, SpectrumA74Codes} + }; + + public static readonly byte[][] HuffmanSpectrumAGroupSizes = + { + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {1, 2, 2, 2}, + new byte[] {1, 1, 1, 1}, + new byte[] {1, 1, 1, 1}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0} + }; + + public static readonly byte[][][] HuffmanSpectrumBBits = + { + null, + null, + new[] {null, SpectrumB22Bits, SpectrumB23Bits, SpectrumB24Bits}, + new[] {null, SpectrumB32Bits, SpectrumB33Bits, SpectrumB34Bits}, + new[] {null, SpectrumB42Bits, SpectrumB43Bits, SpectrumB44Bits}, + new[] {null, SpectrumB52Bits, SpectrumB53Bits, SpectrumB54Bits}, + new[] {null, SpectrumB62Bits, SpectrumB63Bits, SpectrumB64Bits}, + new[] {null, SpectrumB72Bits, SpectrumB73Bits, SpectrumB74Bits} + }; + + public static readonly short[][][] HuffmanSpectrumBCodes = + { + null, + null, + new[] {null, SpectrumB22Codes, SpectrumB23Codes, SpectrumB24Codes}, + new[] {null, SpectrumB32Codes, SpectrumB33Codes, SpectrumB34Codes}, + new[] {null, SpectrumB42Codes, SpectrumB43Codes, SpectrumB44Codes}, + new[] {null, SpectrumB52Codes, SpectrumB53Codes, SpectrumB54Codes}, + new[] {null, SpectrumB62Codes, SpectrumB63Codes, SpectrumB64Codes}, + new[] {null, SpectrumB72Codes, SpectrumB73Codes, SpectrumB74Codes} + }; + + public static readonly byte[][] HuffmanSpectrumBGroupSizes = + { + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 2, 2, 2}, + new byte[] {0, 1, 1, 1}, + new byte[] {0, 1, 1, 1}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0}, + new byte[] {0, 0, 0, 0} + }; + } +} \ No newline at end of file diff --git a/src/SharpEmu.GUI/Atrac9/LICENSE.txt b/src/SharpEmu.GUI/Atrac9/LICENSE.txt new file mode 100644 index 0000000..2b5cc1f --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Alex Barney + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/SharpEmu.GUI/Atrac9/Quantization.cs b/src/SharpEmu.GUI/Atrac9/Quantization.cs new file mode 100644 index 0000000..4e10dbf --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Quantization.cs @@ -0,0 +1,58 @@ +#nullable disable +using System; + +namespace LibAtrac9 +{ + internal static class Quantization + { + public static void DequantizeSpectra(Block block) + { + foreach (Channel channel in block.Channels) + { + Array.Clear(channel.Spectra, 0, channel.Spectra.Length); + + for (int i = 0; i < channel.CodedQuantUnits; i++) + { + DequantizeQuantUnit(channel, i); + } + } + } + + private static void DequantizeQuantUnit(Channel channel, int band) + { + int subBandIndex = Tables.QuantUnitToCoeffIndex[band]; + int subBandCount = Tables.QuantUnitToCoeffCount[band]; + double stepSize = Tables.QuantizerStepSize[channel.Precisions[band]]; + double stepSizeFine = Tables.QuantizerFineStepSize[channel.PrecisionsFine[band]]; + + for (int sb = 0; sb < subBandCount; sb++) + { + double coarse = channel.QuantizedSpectra[subBandIndex + sb] * stepSize; + double fine = channel.QuantizedSpectraFine[subBandIndex + sb] * stepSizeFine; + channel.Spectra[subBandIndex + sb] = coarse + fine; + } + } + + public static void ScaleSpectrum(Block block) + { + foreach (Channel channel in block.Channels) + { + ScaleSpectrum(channel); + } + } + + private static void ScaleSpectrum(Channel channel) + { + int quantUnitCount = channel.Block.QuantizationUnitCount; + double[] spectra = channel.Spectra; + + for (int i = 0; i < quantUnitCount; i++) + { + for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++) + { + spectra[sb] *= Tables.SpectrumScale[channel.ScaleFactors[i]]; + } + } + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/ScaleFactors.cs b/src/SharpEmu.GUI/Atrac9/ScaleFactors.cs new file mode 100644 index 0000000..ace9245 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/ScaleFactors.cs @@ -0,0 +1,172 @@ +#nullable disable +using System; +using System.IO; +using LibAtrac9.Utilities; + +namespace LibAtrac9 +{ + internal static class ScaleFactors + { + public static void Read(BitReader reader, Channel channel) + { + Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length); + + channel.ScaleFactorCodingMode = reader.ReadInt(2); + if (channel.ChannelIndex == 0) + { + switch (channel.ScaleFactorCodingMode) + { + case 0: + ReadVlcDeltaOffset(reader, channel); + break; + case 1: + ReadClcOffset(reader, channel); + break; + case 2: + if (channel.Block.FirstInSuperframe) throw new InvalidDataException(); + ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev); + break; + case 3: + if (channel.Block.FirstInSuperframe) throw new InvalidDataException(); + ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev); + break; + } + } + else + { + switch (channel.ScaleFactorCodingMode) + { + case 0: + ReadVlcDeltaOffset(reader, channel); + break; + case 1: + ReadVlcDistanceToBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit); + break; + case 2: + ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit); + break; + case 3: + if (channel.Block.FirstInSuperframe) throw new InvalidDataException(); + ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev); + break; + } + } + + for (int i = 0; i < channel.Block.ExtensionUnit; i++) + { + if (channel.ScaleFactors[i] < 0 || channel.ScaleFactors[i] > 31) + { + throw new InvalidDataException("Scale factor values are out of range."); + } + } + + Array.Copy(channel.ScaleFactors, channel.ScaleFactorsPrev, channel.ScaleFactors.Length); + } + + private static void ReadClcOffset(BitReader reader, Channel channel) + { + const int maxBits = 5; + int[] sf = channel.ScaleFactors; + int bitLength = reader.ReadInt(2) + 2; + int baseValue = bitLength < maxBits ? reader.ReadInt(maxBits) : 0; + + for (int i = 0; i < channel.Block.ExtensionUnit; i++) + { + sf[i] = reader.ReadInt(bitLength) + baseValue; + } + } + + private static void ReadVlcDeltaOffset(BitReader reader, Channel channel) + { + int weightIndex = reader.ReadInt(3); + byte[] weights = ScaleFactorWeights[weightIndex]; + + int[] sf = channel.ScaleFactors; + int baseValue = reader.ReadInt(5); + int bitLength = reader.ReadInt(2) + 3; + HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength]; + + sf[0] = reader.ReadInt(bitLength); + + for (int i = 1; i < channel.Block.ExtensionUnit; i++) + { + int delta = Unpack.ReadHuffmanValue(codebook, reader); + sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1); + } + + for (int i = 0; i < channel.Block.ExtensionUnit; i++) + { + sf[i] += baseValue - weights[i]; + } + } + + private static void ReadVlcDistanceToBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength) + { + int[] sf = channel.ScaleFactors; + int bitLength = reader.ReadInt(2) + 2; + HuffmanCodebook codebook = Tables.HuffmanScaleFactorsSigned[bitLength]; + int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength); + + for (int i = 0; i < unitCount; i++) + { + int distance = Unpack.ReadHuffmanValue(codebook, reader, true); + sf[i] = (baseline[i] + distance) & 31; + } + + for (int i = unitCount; i < channel.Block.ExtensionUnit; i++) + { + sf[i] = reader.ReadInt(5); + } + } + + private static void ReadVlcDeltaOffsetWithBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength) + { + int[] sf = channel.ScaleFactors; + int baseValue = reader.ReadOffsetBinary(5, BitReader.OffsetBias.Negative); + int bitLength = reader.ReadInt(2) + 1; + HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength]; + int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength); + + sf[0] = reader.ReadInt(bitLength); + + for (int i = 1; i < unitCount; i++) + { + int delta = Unpack.ReadHuffmanValue(codebook, reader); + sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1); + } + + for (int i = 0; i < unitCount; i++) + { + sf[i] += baseValue + baseline[i]; + } + + for (int i = unitCount; i < channel.Block.ExtensionUnit; i++) + { + sf[i] = reader.ReadInt(5); + } + } + + public static readonly byte[][] ScaleFactorWeights = + { + new byte[] { + 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10, 12, 12, 12 + }, new byte[] { + 3, 2, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 7, 10, 10, 10 + }, new byte[] { + 0, 2, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 9, 12, 12, 12 + }, new byte[] { + 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 8, 8, 10, 11, 11, 12, 13, 13, 13, 13 + }, new byte[] { + 0, 2, 2, 3, 3, 4, 4, 5, 4, 5, 5, 5, 5, 6, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14 + }, new byte[] { + 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 9, 11, 11, 11 + }, new byte[] { + 0, 5, 8, 10, 11, 11, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12, + 12, 13, 15, 15, 15 + }, new byte[] { + 0, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, + 15, 15, 15 + } + }; + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Stereo.cs b/src/SharpEmu.GUI/Atrac9/Stereo.cs new file mode 100644 index 0000000..ab158a4 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Stereo.cs @@ -0,0 +1,34 @@ +#nullable disable +namespace LibAtrac9 +{ + internal static class Stereo + { + public static void ApplyIntensityStereo(Block block) + { + if (block.BlockType != BlockType.Stereo) return; + + int totalUnits = block.QuantizationUnitCount; + int stereoUnits = block.StereoQuantizationUnit; + if (stereoUnits >= totalUnits) return; + + Channel source = block.PrimaryChannel; + Channel dest = block.SecondaryChannel; + + for (int i = stereoUnits; i < totalUnits; i++) + { + int sign = block.JointStereoSigns[i]; + for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++) + { + if (sign > 0) + { + dest.Spectra[sb] = -source.Spectra[sb]; + } + else + { + dest.Spectra[sb] = source.Spectra[sb]; + } + } + } + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Tables.cs b/src/SharpEmu.GUI/Atrac9/Tables.cs new file mode 100644 index 0000000..72b4e74 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Tables.cs @@ -0,0 +1,116 @@ +#nullable disable +using System; +using static LibAtrac9.HuffmanCodebooks; + +namespace LibAtrac9 +{ + internal static class Tables + { + public static int MaxHuffPrecision(bool highSampleRate) => highSampleRate ? 1 : 7; + public static int MinBandCount(bool highSampleRate) => highSampleRate ? 1 : 3; + public static int MaxExtensionBand(bool highSampleRate) => highSampleRate ? 16 : 18; + + public static readonly int[] SampleRates = + { + 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, + 44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000 + }; + + public static readonly byte[] SamplingRateIndexToFrameSamplesPower = { 6, 6, 7, 7, 7, 8, 8, 8, 6, 6, 7, 7, 7, 8, 8, 8 }; + + // From sampling rate index + public static readonly byte[] MaxBandCount = { 8, 8, 12, 12, 12, 18, 18, 18, 8, 8, 12, 12, 12, 16, 16, 16 }; + public static readonly byte[] BandToQuantUnitCount = { 0, 4, 8, 10, 12, 13, 14, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 30 }; + + public static readonly byte[] QuantUnitToCoeffCount = + { + 02, 02, 02, 02, 02, 02, 02, 02, 04, 04, 04, 04, 08, 08, 08, + 08, 08, 08, 08, 08, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 + }; + + public static readonly short[] QuantUnitToCoeffIndex = + { + 0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, + 64, 72, 80, 88, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256 + }; + + public static readonly byte[] QuantUnitToCodebookIndex = + { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, + 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 + }; + + public static readonly ChannelConfig[] ChannelConfig = + { + new ChannelConfig(BlockType.Mono), + new ChannelConfig(BlockType.Mono, BlockType.Mono), + new ChannelConfig(BlockType.Stereo), + new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo), + new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo, BlockType.Stereo), + new ChannelConfig(BlockType.Stereo, BlockType.Stereo) + }; + + public static readonly HuffmanCodebook[] HuffmanScaleFactorsUnsigned = + GenerateHuffmanCodebooks(HuffmanScaleFactorsACodes, HuffmanScaleFactorsABits, HuffmanScaleFactorsGroupSizes); + + public static readonly HuffmanCodebook[] HuffmanScaleFactorsSigned = + GenerateHuffmanCodebooks(HuffmanScaleFactorsBCodes, HuffmanScaleFactorsBBits, HuffmanScaleFactorsGroupSizes); + + public static readonly HuffmanCodebook[][][] HuffmanSpectrum = + { + GenerateHuffmanCodebooks(HuffmanSpectrumACodes, HuffmanSpectrumABits, HuffmanSpectrumAGroupSizes), + GenerateHuffmanCodebooks(HuffmanSpectrumBCodes, HuffmanSpectrumBBits, HuffmanSpectrumBGroupSizes) + }; + + public static readonly double[][] ImdctWindow = { GenerateImdctWindow(6), GenerateImdctWindow(7), GenerateImdctWindow(8) }; + + public static readonly double[] SpectrumScale = Generate(32, SpectrumScaleFunction); + public static readonly double[] QuantizerStepSize = Generate(16, QuantizerStepSizeFunction); + public static readonly double[] QuantizerFineStepSize = Generate(16, QuantizerFineStepSizeFunction); + + public static readonly byte[][] GradientCurves = BitAllocation.GenerateGradientCurves(); + + private static double QuantizerStepSizeFunction(int x) => 2.0 / ((1 << (x + 1)) - 1); + private static double QuantizerFineStepSizeFunction(int x) => QuantizerStepSizeFunction(x) / ushort.MaxValue; + private static double SpectrumScaleFunction(int x) => Math.Pow(2, x - 15); + + private static double[] GenerateImdctWindow(int frameSizePower) + { + int frameSize = 1 << frameSizePower; + var output = new double[frameSize]; + + double[] a1 = GenerateMdctWindow(frameSizePower); + + for (int i = 0; i < frameSize; i++) + { + output[i] = a1[i] / (a1[frameSize - 1 - i] * a1[frameSize - 1 - i] + a1[i] * a1[i]); + } + return output; + } + + private static double[] GenerateMdctWindow(int frameSizePower) + { + int frameSize = 1 << frameSizePower; + var output = new double[frameSize]; + + for (int i = 0; i < frameSize; i++) + { + output[i] = (Math.Sin(((i + 0.5) / frameSize - 0.5) * Math.PI) + 1.0) * 0.5; + } + + return output; + } + + private static T[] Generate(int count, Func elementGenerator) + { + var table = new T[count]; + for (int i = 0; i < count; i++) + { + table[i] = elementGenerator(i); + } + return table; + } + + + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Unpack.cs b/src/SharpEmu.GUI/Atrac9/Unpack.cs new file mode 100644 index 0000000..2ddbc45 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Unpack.cs @@ -0,0 +1,426 @@ +#nullable disable +using System; +using System.IO; +using LibAtrac9.Utilities; + +namespace LibAtrac9 +{ + internal static class Unpack + { + public static void UnpackFrame(BitReader reader, Frame frame) + { + foreach (Block block in frame.Blocks) + { + UnpackBlock(reader, block); + } + } + + private static void UnpackBlock(BitReader reader, Block block) + { + ReadBlockHeader(reader, block); + + if (block.BlockType == BlockType.LFE) + { + UnpackLfeBlock(reader, block); + } + else + { + UnpackStandardBlock(reader, block); + } + + reader.AlignPosition(8); + } + + private static void ReadBlockHeader(BitReader reader, Block block) + { + bool firstInSuperframe = block.Frame.FrameIndex == 0; + block.FirstInSuperframe = !reader.ReadBool(); + block.ReuseBandParams = reader.ReadBool(); + + if (block.FirstInSuperframe != firstInSuperframe) + { + throw new InvalidDataException(); + } + + if (firstInSuperframe && block.ReuseBandParams && block.BlockType != BlockType.LFE) + { + throw new InvalidDataException(); + } + } + + private static void UnpackStandardBlock(BitReader reader, Block block) + { + Channel[] channels = block.Channels; + + if (!block.ReuseBandParams) + { + ReadBandParams(reader, block); + } + + ReadGradientParams(reader, block); + BitAllocation.CreateGradient(block); + ReadStereoParams(reader, block); + ReadExtensionParams(reader, block); + + foreach (Channel channel in channels) + { + channel.UpdateCodedUnits(); + + ScaleFactors.Read(reader, channel); + BitAllocation.CalculateMask(channel); + BitAllocation.CalculatePrecisions(channel); + CalculateSpectrumCodebookIndex(channel); + + ReadSpectra(reader, channel); + ReadSpectraFine(reader, channel); + } + + block.QuantizationUnitsPrev = block.BandExtensionEnabled ? block.ExtensionUnit : block.QuantizationUnitCount; + } + + private static void ReadBandParams(BitReader reader, Block block) + { + int minBandCount = Tables.MinBandCount(block.Config.HighSampleRate); + int maxExtensionBand = Tables.MaxExtensionBand(block.Config.HighSampleRate); + block.BandCount = reader.ReadInt(4); + block.BandCount += minBandCount; + block.QuantizationUnitCount = Tables.BandToQuantUnitCount[block.BandCount]; + if (block.BandCount < minBandCount || block.BandCount > + Tables.MaxBandCount[block.Config.SampleRateIndex]) + { + return; + } + + if (block.BlockType == BlockType.Stereo) + { + block.StereoBand = reader.ReadInt(4); + block.StereoBand += minBandCount; + block.StereoQuantizationUnit = Tables.BandToQuantUnitCount[block.StereoBand]; + } + else + { + block.StereoBand = block.BandCount; + } + + block.BandExtensionEnabled = reader.ReadBool(); + if (block.BandExtensionEnabled) + { + block.ExtensionBand = reader.ReadInt(4); + block.ExtensionBand += minBandCount; + + if (block.ExtensionBand < block.BandCount || block.ExtensionBand > maxExtensionBand) + { + throw new InvalidDataException(); + } + + block.ExtensionUnit = Tables.BandToQuantUnitCount[block.ExtensionBand]; + } + else + { + block.ExtensionBand = block.BandCount; + block.ExtensionUnit = block.QuantizationUnitCount; + } + } + + private static void ReadGradientParams(BitReader reader, Block block) + { + block.GradientMode = reader.ReadInt(2); + if (block.GradientMode > 0) + { + block.GradientEndUnit = 31; + block.GradientEndValue = 31; + block.GradientStartUnit = reader.ReadInt(5); + block.GradientStartValue = reader.ReadInt(5); + } + else + { + block.GradientStartUnit = reader.ReadInt(6); + block.GradientEndUnit = reader.ReadInt(6) + 1; + block.GradientStartValue = reader.ReadInt(5); + block.GradientEndValue = reader.ReadInt(5); + } + block.GradientBoundary = reader.ReadInt(4); + + if (block.GradientBoundary > block.QuantizationUnitCount) + { + throw new InvalidDataException(); + } + if (block.GradientStartUnit < 1 || block.GradientStartUnit >= 48) + { + throw new InvalidDataException(); + } + if (block.GradientEndUnit < 1 || block.GradientEndUnit >= 48) + { + throw new InvalidDataException(); + } + if (block.GradientStartUnit > block.GradientEndUnit) + { + throw new InvalidDataException(); + } + if (block.GradientStartValue < 0 || block.GradientStartValue >= 32) + { + throw new InvalidDataException(); + } + if (block.GradientEndValue < 0 || block.GradientEndValue >= 32) + { + throw new InvalidDataException(); + } + } + + private static void ReadStereoParams(BitReader reader, Block block) + { + if (block.BlockType != BlockType.Stereo) return; + + block.PrimaryChannelIndex = reader.ReadInt(1); + block.HasJointStereoSigns = reader.ReadBool(); + if (block.HasJointStereoSigns) + { + for (int i = block.StereoQuantizationUnit; i < block.QuantizationUnitCount; i++) + { + block.JointStereoSigns[i] = reader.ReadInt(1); + } + } + else + { + Array.Clear(block.JointStereoSigns, 0, block.JointStereoSigns.Length); + } + } + + private static void ReadExtensionParams(BitReader reader, Block block) + { + // ReSharper disable once RedundantAssignment + int bexBand = 0; + if (block.BandExtensionEnabled) + { + BandExtension.GetBexBandInfo(out bexBand, out _, out _, block.QuantizationUnitCount); + if (block.BlockType == BlockType.Stereo) + { + ReadHeader(block.Channels[1]); + } + else + { + reader.Position += 1; + } + } + block.HasExtensionData = reader.ReadBool(); + + if (!block.HasExtensionData) return; + if (!block.BandExtensionEnabled) + { + block.BexMode = reader.ReadInt(2); + block.BexDataLength = reader.ReadInt(5); + reader.Position += block.BexDataLength; + return; + } + + ReadHeader(block.Channels[0]); + + block.BexDataLength = reader.ReadInt(5); + if (block.BexDataLength <= 0) return; + int bexDataEnd = reader.Position + block.BexDataLength; + + ReadData(block.Channels[0]); + + if (block.BlockType == BlockType.Stereo) + { + ReadData(block.Channels[1]); + } + + // Make sure we didn't read too many bits + if (reader.Position > bexDataEnd) + { + throw new InvalidDataException(); + } + + void ReadHeader(Channel channel) + { + int bexMode = reader.ReadInt(2); + channel.BexMode = bexBand > 2 ? bexMode : 4; + channel.BexValueCount = BandExtension.BexEncodedValueCounts[channel.BexMode][bexBand]; + } + + void ReadData(Channel channel) + { + for (int i = 0; i < channel.BexValueCount; i++) + { + int dataLength = BandExtension.BexDataLengths[channel.BexMode][bexBand][i]; + channel.BexValues[i] = reader.ReadInt(dataLength); + } + } + } + + private static void CalculateSpectrumCodebookIndex(Channel channel) + { + Array.Clear(channel.CodebookSet, 0, channel.CodebookSet.Length); + int quantUnits = channel.CodedQuantUnits; + int[] sf = channel.ScaleFactors; + + if (quantUnits <= 1) return; + if (channel.Config.HighSampleRate) return; + + // Temporarily setting this value allows for simpler code by + // making the last value a non-special case. + int originalScaleTmp = sf[quantUnits]; + sf[quantUnits] = sf[quantUnits - 1]; + + int avg = 0; + if (quantUnits > 12) + { + for (int i = 0; i < 12; i++) + { + avg += sf[i]; + } + avg = (avg + 6) / 12; + } + + for (int i = 8; i < quantUnits; i++) + { + int prevSf = sf[i - 1]; + int nextSf = sf[i + 1]; + int minSf = Math.Min(prevSf, nextSf); + if (sf[i] - minSf >= 3 || sf[i] - prevSf + sf[i] - nextSf >= 3) + { + channel.CodebookSet[i] = 1; + } + } + + for (int i = 12; i < quantUnits; i++) + { + if (channel.CodebookSet[i] == 0) + { + int minSf = Math.Min(sf[i - 1], sf[i + 1]); + if (sf[i] - minSf >= 2 && sf[i] >= avg - (Tables.QuantUnitToCoeffCount[i] == 16 ? 1 : 0)) + { + channel.CodebookSet[i] = 1; + } + } + } + + sf[quantUnits] = originalScaleTmp; + } + + private static void ReadSpectra(BitReader reader, Channel channel) + { + int[] values = channel.SpectraValuesBuffer; + Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length); + int maxHuffPrecision = Tables.MaxHuffPrecision(channel.Config.HighSampleRate); + + for (int i = 0; i < channel.CodedQuantUnits; i++) + { + int subbandCount = Tables.QuantUnitToCoeffCount[i]; + int precision = channel.Precisions[i] + 1; + if (precision <= maxHuffPrecision) + { + HuffmanCodebook huff = Tables.HuffmanSpectrum[channel.CodebookSet[i]][precision][Tables.QuantUnitToCodebookIndex[i]]; + int groupCount = subbandCount >> huff.ValueCountPower; + for (int j = 0; j < groupCount; j++) + { + values[j] = ReadHuffmanValue(huff, reader); + } + + DecodeHuffmanValues(channel.QuantizedSpectra, Tables.QuantUnitToCoeffIndex[i], subbandCount, huff, values); + } + else + { + int subbandIndex = Tables.QuantUnitToCoeffIndex[i]; + for (int j = subbandIndex; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++) + { + channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision); + } + } + } + } + + private static void ReadSpectraFine(BitReader reader, Channel channel) + { + Array.Clear(channel.QuantizedSpectraFine, 0, channel.QuantizedSpectraFine.Length); + + for (int i = 0; i < channel.CodedQuantUnits; i++) + { + if (channel.PrecisionsFine[i] > 0) + { + int overflowBits = channel.PrecisionsFine[i] + 1; + int startSubband = Tables.QuantUnitToCoeffIndex[i]; + int endSubband = Tables.QuantUnitToCoeffIndex[i + 1]; + + for (int j = startSubband; j < endSubband; j++) + { + channel.QuantizedSpectraFine[j] = reader.ReadSignedInt(overflowBits); + } + } + } + } + + private static void DecodeHuffmanValues(int[] spectrum, int index, int bandCount, HuffmanCodebook huff, int[] values) + { + int valueCount = bandCount >> huff.ValueCountPower; + int mask = (1 << huff.ValueBits) - 1; + + for (int i = 0; i < valueCount; i++) + { + int value = values[i]; + for (int j = 0; j < huff.ValueCount; j++) + { + spectrum[index++] = Bit.SignExtend32(value & mask, huff.ValueBits); + value >>= huff.ValueBits; + } + } + } + + public static int ReadHuffmanValue(HuffmanCodebook huff, BitReader reader, bool signed = false) + { + int code = reader.PeekInt(huff.MaxBitSize); + byte value = huff.Lookup[code]; + int bits = huff.Bits[value]; + reader.Position += bits; + return signed ? Bit.SignExtend32(value, huff.ValueBits) : value; + } + + private static void UnpackLfeBlock(BitReader reader, Block block) + { + Channel channel = block.Channels[0]; + block.QuantizationUnitCount = 2; + + DecodeLfeScaleFactors(reader, channel); + CalculateLfePrecision(channel); + channel.CodedQuantUnits = block.QuantizationUnitCount; + ReadLfeSpectra(reader, channel); + } + + private static void DecodeLfeScaleFactors(BitReader reader, Channel channel) + { + Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length); + for (int i = 0; i < channel.Block.QuantizationUnitCount; i++) + { + channel.ScaleFactors[i] = reader.ReadInt(5); + } + } + + private static void CalculateLfePrecision(Channel channel) + { + Block block = channel.Block; + int precision = block.ReuseBandParams ? 8 : 4; + for (int i = 0; i < block.QuantizationUnitCount; i++) + { + channel.Precisions[i] = precision; + channel.PrecisionsFine[i] = 0; + } + } + + private static void ReadLfeSpectra(BitReader reader, Channel channel) + { + Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length); + + for (int i = 0; i < channel.CodedQuantUnits; i++) + { + if (channel.Precisions[i] <= 0) continue; + + int precision = channel.Precisions[i] + 1; + for (int j = Tables.QuantUnitToCoeffIndex[i]; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++) + { + channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision); + } + } + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Utilities/Bit.cs b/src/SharpEmu.GUI/Atrac9/Utilities/Bit.cs new file mode 100644 index 0000000..a3349be --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Utilities/Bit.cs @@ -0,0 +1,23 @@ +#nullable disable +namespace LibAtrac9.Utilities +{ + internal static class Bit + { + private static uint BitReverse32(uint value) + { + value = ((value & 0xaaaaaaaa) >> 1) | ((value & 0x55555555) << 1); + value = ((value & 0xcccccccc) >> 2) | ((value & 0x33333333) << 2); + value = ((value & 0xf0f0f0f0) >> 4) | ((value & 0x0f0f0f0f) << 4); + value = ((value & 0xff00ff00) >> 8) | ((value & 0x00ff00ff) << 8); + return (value >> 16) | (value << 16); + } + private static uint BitReverse32(uint value, int bitCount) => BitReverse32(value) >> (32 - bitCount); + public static int BitReverse32(int value, int bitCount) => (int) BitReverse32((uint) value, bitCount); + + public static int SignExtend32(int value, int bits) + { + int shift = 8 * sizeof(int) - bits; + return (value << shift) >> shift; + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Utilities/BitReader.cs b/src/SharpEmu.GUI/Atrac9/Utilities/BitReader.cs new file mode 100644 index 0000000..4622560 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Utilities/BitReader.cs @@ -0,0 +1,133 @@ +#nullable disable +using System; +using System.Diagnostics; + +namespace LibAtrac9.Utilities +{ + internal class BitReader + { + private byte[] Buffer { get; set; } + private int LengthBits { get; set; } + public int Position { get; set; } + private int Remaining => LengthBits - Position; + + public BitReader(byte[] buffer) => SetBuffer(buffer); + + public void SetBuffer(byte[] buffer) + { + Buffer = buffer; + LengthBits = Buffer?.Length * 8 ?? 0; + Position = 0; + } + + public int ReadInt(int bitCount) + { + int value = PeekInt(bitCount); + Position += bitCount; + return value; + } + + public int ReadSignedInt(int bitCount) + { + int value = PeekInt(bitCount); + Position += bitCount; + return Bit.SignExtend32(value, bitCount); + } + + public bool ReadBool() => ReadInt(1) == 1; + + public int ReadOffsetBinary(int bitCount, OffsetBias bias) + { + int offset = (1 << (bitCount - 1)) - (int)bias; + int value = PeekInt(bitCount) - offset; + Position += bitCount; + return value; + } + + public void AlignPosition(int multiple) + { + Position = Helpers.GetNextMultiple(Position, multiple); + } + + public int PeekInt(int bitCount) + { + Debug.Assert(bitCount >= 0 && bitCount <= 32); + + if (bitCount > Remaining) + { + if (Position >= LengthBits) return 0; + + int extraBits = bitCount - Remaining; + return PeekIntFallback(Remaining) << extraBits; + } + + int byteIndex = Position / 8; + int bitIndex = Position % 8; + + if (bitCount <= 9 && Remaining >= 16) + { + int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1]; + value &= 0xFFFF >> bitIndex; + value >>= 16 - bitCount - bitIndex; + return value; + } + + if (bitCount <= 17 && Remaining >= 24) + { + int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2]; + value &= 0xFFFFFF >> bitIndex; + value >>= 24 - bitCount - bitIndex; + return value; + } + + if (bitCount <= 25 && Remaining >= 32) + { + int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3]; + value &= (int)(0xFFFFFFFF >> bitIndex); + value >>= 32 - bitCount - bitIndex; + return value; + } + return PeekIntFallback(bitCount); + } + + private int PeekIntFallback(int bitCount) + { + int value = 0; + int byteIndex = Position / 8; + int bitIndex = Position % 8; + + while (bitCount > 0) + { + if (bitIndex >= 8) + { + bitIndex = 0; + byteIndex++; + } + + int bitsToRead = Math.Min(bitCount, 8 - bitIndex); + int mask = 0xFF >> bitIndex; + int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead); + + value = (value << bitsToRead) | currentByte; + bitIndex += bitsToRead; + bitCount -= bitsToRead; + } + return value; + } + + /// + /// Specifies the bias of an offset binary value. A positive bias can represent one more + /// positive value than negative value, and a negative bias can represent one more + /// negative value than positive value. + /// + /// Example: + /// A 4-bit offset binary value with a positive bias can store + /// the values 8 through -7 inclusive. + /// A 4-bit offset binary value with a positive bias can store + /// the values 7 through -8 inclusive. + public enum OffsetBias + { + Negative = 0 + } + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Utilities/Helpers.cs b/src/SharpEmu.GUI/Atrac9/Utilities/Helpers.cs new file mode 100644 index 0000000..bd5d940 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Utilities/Helpers.cs @@ -0,0 +1,51 @@ +#nullable disable +using System.Runtime.CompilerServices; + +namespace LibAtrac9.Utilities +{ + internal static class Helpers + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Clamp16(int value) + { + if (value > short.MaxValue) + return short.MaxValue; + if (value < short.MinValue) + return short.MinValue; + return (short)value; + } + + public static int GetNextMultiple(int value, int multiple) + { + if (multiple <= 0) + return value; + + if (value % multiple == 0) + return value; + + return value + multiple - value % multiple; + } + + /// + /// Returns the floor of the base 2 logarithm of a specified number. + /// + /// The number whose logarithm is to be found. + /// The floor of the base 2 logarithm of . + public static int Log2(int value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + + return MultiplyDeBruijnBitPosition[(uint)(value * 0x07C4ACDDU) >> 27]; + } + + private static readonly int[] MultiplyDeBruijnBitPosition = + { + 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, + 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31 + }; + } +} diff --git a/src/SharpEmu.GUI/Atrac9/Utilities/Mdct.cs b/src/SharpEmu.GUI/Atrac9/Utilities/Mdct.cs new file mode 100644 index 0000000..0413b61 --- /dev/null +++ b/src/SharpEmu.GUI/Atrac9/Utilities/Mdct.cs @@ -0,0 +1,178 @@ +#nullable disable +using System; +using System.Collections.Generic; + +namespace LibAtrac9.Utilities +{ + internal class Mdct + { + private int MdctBits { get; } + private int MdctSize { get; } + private double Scale { get; } + + private static readonly object TableLock = new object(); + private static int _tableBits = -1; + private static readonly List SinTables = new List(); + private static readonly List CosTables = new List(); + private static readonly List ShuffleTables = new List(); + + private readonly double[] _imdctPrevious; + private readonly double[] _imdctWindow; + + private readonly double[] _scratchMdct; + private readonly double[] _scratchDct; + + public Mdct(int mdctBits, double[] window, double scale = 1) + { + SetTables(mdctBits); + + MdctBits = mdctBits; + MdctSize = 1 << mdctBits; + Scale = scale; + + if (window.Length < MdctSize) + { + throw new ArgumentException("Window must be as long as the MDCT size.", nameof(window)); + } + + _imdctPrevious = new double[MdctSize]; + _scratchMdct = new double[MdctSize]; + _scratchDct = new double[MdctSize]; + _imdctWindow = window; + } + + private static void SetTables(int maxBits) + { + lock (TableLock) + { + if (maxBits > _tableBits) + { + for (int i = _tableBits + 1; i <= maxBits; i++) + { + GenerateTrigTables(i, out double[] sin, out double[] cos); + SinTables.Add(sin); + CosTables.Add(cos); + ShuffleTables.Add(GenerateShuffleTable(i)); + } + _tableBits = maxBits; + } + } + } + + public void RunImdct(double[] input, double[] output) + { + if (input.Length < MdctSize) + { + throw new ArgumentException("Input must be as long as the MDCT size.", nameof(input)); + } + + if (output.Length < MdctSize) + { + throw new ArgumentException("Output must be as long as the MDCT size.", nameof(output)); + } + + int size = MdctSize; + int half = size / 2; + double[] dctOut = _scratchMdct; + + Dct4(input, dctOut); + + for (int i = 0; i < half; i++) + { + output[i] = _imdctWindow[i] * dctOut[i + half] + _imdctPrevious[i]; + output[i + half] = _imdctWindow[i + half] * -dctOut[size - 1 - i] - _imdctPrevious[i + half]; + _imdctPrevious[i] = _imdctWindow[size - 1 - i] * -dctOut[half - i - 1]; + _imdctPrevious[i + half] = _imdctWindow[half - i - 1] * dctOut[i]; + } + } + + /// + /// Does a Type-4 DCT. + /// + /// The input array containing the time or frequency-domain samples + /// The output array that will contain the transformed time or frequency-domain samples + private void Dct4(double[] input, double[] output) + { + int[] shuffleTable = ShuffleTables[MdctBits]; + double[] sinTable = SinTables[MdctBits]; + double[] cosTable = CosTables[MdctBits]; + double[] dctTemp = _scratchDct; + + int size = MdctSize; + int lastIndex = size - 1; + int halfSize = size / 2; + + for (int i = 0; i < halfSize; i++) + { + int i2 = i * 2; + double a = input[i2]; + double b = input[lastIndex - i2]; + double sin = sinTable[i]; + double cos = cosTable[i]; + dctTemp[i2] = a * cos + b * sin; + dctTemp[i2 + 1] = a * sin - b * cos; + } + int stageCount = MdctBits - 1; + + for (int stage = 0; stage < stageCount; stage++) + { + int blockCount = 1 << stage; + int blockSizeBits = stageCount - stage; + int blockHalfSizeBits = blockSizeBits - 1; + int blockSize = 1 << blockSizeBits; + int blockHalfSize = 1 << blockHalfSizeBits; + sinTable = SinTables[blockHalfSizeBits]; + cosTable = CosTables[blockHalfSizeBits]; + + for (int block = 0; block < blockCount; block++) + { + for (int i = 0; i < blockHalfSize; i++) + { + int frontPos = (block * blockSize + i) * 2; + int backPos = frontPos + blockSize; + double a = dctTemp[frontPos] - dctTemp[backPos]; + double b = dctTemp[frontPos + 1] - dctTemp[backPos + 1]; + double sin = sinTable[i]; + double cos = cosTable[i]; + dctTemp[frontPos] += dctTemp[backPos]; + dctTemp[frontPos + 1] += dctTemp[backPos + 1]; + dctTemp[backPos] = a * cos + b * sin; + dctTemp[backPos + 1] = a * sin - b * cos; + } + } + } + + for (int i = 0; i < MdctSize; i++) + { + output[i] = dctTemp[shuffleTable[i]] * Scale; + } + } + + internal static void GenerateTrigTables(int sizeBits, out double[] sin, out double[] cos) + { + int size = 1 << sizeBits; + sin = new double[size]; + cos = new double[size]; + + for (int i = 0; i < size; i++) + { + double value = Math.PI * (4 * i + 1) / (4 * size); + sin[i] = Math.Sin(value); + cos[i] = Math.Cos(value); + } + } + + internal static int[] GenerateShuffleTable(int sizeBits) + { + int size = 1 << sizeBits; + var table = new int[size]; + + for (int i = 0; i < size; i++) + { + table[i] = Bit.BitReverse32(i ^ (i / 2), sizeBits); + } + + return table; + } + } +} diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs index 5ffaf3b..85c6c9b 100644 --- a/src/SharpEmu.GUI/GuiSettings.cs +++ b/src/SharpEmu.GUI/GuiSettings.cs @@ -23,6 +23,12 @@ public sealed class GuiSettings public bool StrictDynlibResolution { get; set; } + /// Mirror emulator output to user/logs/<titleId>-<timestamp>.log. + public bool LogToFile { get; set; } + + /// Loop the selected game's sce_sys/snd0.at9 preview music. + public bool PlayTitleMusic { get; set; } = true; + public string? EmulatorPath { get; set; } // The emulator is portable and keeps its data next to the executable; diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index aacdf79..80bc6b7 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -244,10 +244,18 @@ SPDX-License-Identifier: GPL-2.0-or-later - + + + + + + + + + diff --git a/src/SharpEmu.GUI/MainWindow.axaml.cs b/src/SharpEmu.GUI/MainWindow.axaml.cs index f0010f6..fd3e612 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml.cs +++ b/src/SharpEmu.GUI/MainWindow.axaml.cs @@ -16,6 +16,7 @@ using Avalonia.Platform.Storage; using Avalonia.Threading; using Avalonia.VisualTree; using SharpEmu.Libs.Pad; +using SharpEmu.Logging; namespace SharpEmu.GUI; @@ -38,6 +39,8 @@ public partial class MainWindow : Window private GuiSettings _settings = new(); private EmulatorProcess? _emulator; + private StreamWriter? _fileLog; + private readonly SndPreviewPlayer _sndPreview = new(); private string? _emulatorExePath; private bool _isRunning; private int _autoScrollTicks; @@ -84,6 +87,7 @@ public partial class MainWindow : Window CopyLogButton.Click += async (_, _) => await CopyConsoleAsync(); OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true; ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true; + TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled(); GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel); CtxLaunch.Click += (_, _) => LaunchSelected(); @@ -98,6 +102,7 @@ public partial class MainWindow : Window Closing += (_, _) => OnWindowClosing(); DualSenseReader.EnsureStarted(); + XInputReader.EnsureStarted(); _gamepadTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50), @@ -110,7 +115,8 @@ public partial class MainWindow : Window private void PollGamepad() { - if (!DualSenseReader.TryGetState(out var pad)) + // DualSense wins when both are connected; XInput covers Xbox pads. + if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad)) { _previousPadButtons = 0; return; @@ -216,10 +222,15 @@ public partial class MainWindow : Window private async Task OnOpenedAsync() { var version = Assembly.GetExecutingAssembly().GetName().Version; - if (version is not null) - { - VersionText.Text = $"v{version.ToString(3)}"; - } + var display = version is not null ? $"v{version.ToString(3)}" : "v0.0.1"; + display += BuildInfo.CommitSha is null + ? " · dev" + : BuildInfo.IsOfficialRelease + ? $" · {BuildInfo.CommitSha}" + : $" · UNOFFICIAL {BuildInfo.CommitSha}"; + VersionText.Text = display; + Title = $"SharpEmu {display}"; + ToolTip.SetTip(VersionText, BuildInfo.Banner); _settings = GuiSettings.Load(); ApplySettingsToControls(); @@ -233,7 +244,9 @@ public partial class MainWindow : Window _settings.Save(); _consoleFlushTimer.Stop(); _gamepadTimer.Stop(); + _sndPreview.Stop(); _emulator?.Dispose(); + DropFileLog(); } private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) @@ -260,6 +273,8 @@ public partial class MainWindow : Window }; TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096); StrictToggle.IsChecked = _settings.StrictDynlibResolution; + LogToFileToggle.IsChecked = _settings.LogToFile; + TitleMusicToggle.IsChecked = _settings.PlayTitleMusic; } private void ReadControlsIntoSettings() @@ -267,6 +282,8 @@ public partial class MainWindow : Window _settings.LogLevel = SelectedLogLevel(); _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0); _settings.StrictDynlibResolution = StrictToggle.IsChecked == true; + _settings.LogToFile = LogToFileToggle.IsChecked == true; + _settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true; } private string SelectedLogLevel() @@ -778,6 +795,7 @@ public partial class MainWindow : Window SelectedGamePath.Text = game.Path; SelectedCoverPanel.DataContext = game; _ = UpdateBackdropAsync(game); + PlaySelectedGamePreview(game); } else { @@ -785,11 +803,66 @@ public partial class MainWindow : Window SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly."; SelectedCoverPanel.DataContext = null; _ = UpdateBackdropAsync(null); + _sndPreview.Stop(); } UpdateRunButtons(); } + /// + /// Loops the selected game's sce_sys/snd0.at9 preview music, console + /// home screen style. Silent while a game is running or when disabled + /// in the options. + /// + private void PlaySelectedGamePreview(GameEntry game) + { + if (_isRunning || !_settings.PlayTitleMusic) + { + return; + } + + var directory = Path.GetDirectoryName(game.Path); + var sndPath = directory is null ? null : Path.Combine(directory, "sce_sys", "snd0.at9"); + if (sndPath is not null && File.Exists(sndPath)) + { + _sndPreview.Play(sndPath); + } + else + { + _sndPreview.Stop(); + } + } + + private void OnTitleMusicToggled() + { + _settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true; + if (!_settings.PlayTitleMusic) + { + _sndPreview.Stop(); + } + else if (GameList.SelectedItem is GameEntry game) + { + PlaySelectedGamePreview(game); + } + } + + /// Pauses the preview music while the window is minimized. + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == WindowStateProperty) + { + if (WindowState == WindowState.Minimized) + { + _sndPreview.Pause(); + } + else + { + _sndPreview.Resume(); + } + } + } + /// /// Fades the window backdrop to the selected game's key art. The image /// decodes off the UI thread and is cached on the entry; a newer @@ -855,11 +928,11 @@ public partial class MainWindow : Window { if (GameList.SelectedItem is GameEntry game) { - Launch(game.Path, game.Name); + Launch(game.Path, game.Name, game.TitleId); } } - private void Launch(string ebootPath, string displayName) + private void Launch(string ebootPath, string displayName, string? titleId = null) { if (_isRunning) { @@ -876,6 +949,7 @@ public partial class MainWindow : Window } } + _sndPreview.Stop(); ReadControlsIntoSettings(); _settings.Save(); @@ -898,6 +972,23 @@ public partial class MainWindow : Window _consoleLines.Clear(); ConsoleToggle.IsChecked = true; + + // Mirror everything the console pane shows into a log file for the + // duration of the run, regardless of the emulator's log level. + DropFileLog(); + if (_settings.LogToFile && BuildLogFilePath(titleId) is { } logFilePath) + { + try + { + _fileLog = new StreamWriter(logFilePath, append: false); + AppendConsoleLine($"Log file: {logFilePath}", DimLineBrush); + } + catch (Exception ex) + { + AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush); + } + } + AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush); var emulator = new EmulatorProcess(); @@ -912,6 +1003,7 @@ public partial class MainWindow : Window { emulator.Dispose(); AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush); + DropFileLog(); return; } @@ -923,6 +1015,37 @@ public partial class MainWindow : Window UpdateRunButtons(); } + /// + /// Builds "user/logs/<titleId>-<timestamp>.log" next to the emulator + /// executable, following the same portable-data convention as savedata. + /// + private string? BuildLogFilePath(string? titleId) + { + try + { + var exeDirectory = Path.GetDirectoryName(_emulatorExePath); + if (string.IsNullOrEmpty(exeDirectory)) + { + return null; + } + + var logsDirectory = Path.Combine(exeDirectory, "user", "logs"); + Directory.CreateDirectory(logsDirectory); + + var id = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId; + foreach (var invalid in Path.GetInvalidFileNameChars()) + { + id = id.Replace(invalid, '_'); + } + + return Path.Combine(logsDirectory, $"{id}-{DateTime.Now:yyyyMMdd-HHmmss}.log"); + } + catch (Exception) + { + return null; // unwritable location: launch continues without a log file + } + } + private void OnEmulatorExited(int exitCode) { FlushPendingConsoleLines(); @@ -941,6 +1064,7 @@ public partial class MainWindow : Window }; var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush; AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush); + CloseFileLogSoon(); StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush; StatusText.Text = $"Exited with code {exitCode} ({meaning})"; @@ -967,9 +1091,12 @@ public partial class MainWindow : Window var incoming = new List(); while (_pendingLines.TryDequeue(out var pending)) { + WriteFileLog(pending.Line); incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line))); } + FlushFileLog(); + if (incoming.Count >= MaxConsoleLines) { // A burst larger than the cap: keep only the newest lines. @@ -998,11 +1125,81 @@ public partial class MainWindow : Window private void AppendConsoleLine(string text, IBrush brush) { + WriteFileLog(text); + FlushFileLog(); _consoleLines.Add(new LogLine(text, brush)); _autoScrollTicks = 3; MaybeAutoScroll(); } + // ---- Console-to-file mirroring ---- + + private void WriteFileLog(string text) + { + if (_fileLog is not { } writer) + { + return; + } + + try + { + writer.Write('['); + writer.Write(DateTime.Now.ToString("HH:mm:ss.fff")); + writer.Write("] "); + writer.WriteLine(text); + } + catch (Exception) + { + DropFileLog(); // unwritable (disk full, etc.): stop mirroring + } + } + + private void FlushFileLog() + { + try + { + _fileLog?.Flush(); + } + catch (Exception) + { + DropFileLog(); + } + } + + private void DropFileLog() + { + var writer = _fileLog; + _fileLog = null; + try + { + writer?.Dispose(); + } + catch (Exception) + { + } + } + + /// + /// The pipe reader threads can deliver a final burst after the exit + /// event, so the file stays open for one more flush cycle. + /// + private void CloseFileLogSoon() + { + if (_fileLog is not { } writer) + { + return; + } + + DispatcherTimer.RunOnce(() => + { + if (ReferenceEquals(_fileLog, writer)) + { + FlushPendingConsoleLines(); + DropFileLog(); + } + }, TimeSpan.FromMilliseconds(400)); + } + private void MaybeAutoScroll() { // ScrollToEnd is applied over a few flush-timer ticks because the diff --git a/src/SharpEmu.GUI/SharpEmu.GUI.csproj b/src/SharpEmu.GUI/SharpEmu.GUI.csproj index 132338a..ffeaa28 100644 --- a/src/SharpEmu.GUI/SharpEmu.GUI.csproj +++ b/src/SharpEmu.GUI/SharpEmu.GUI.csproj @@ -12,6 +12,12 @@ SPDX-License-Identifier: GPL-2.0-or-later 0.0.1 + + + + + @@ -24,12 +30,15 @@ SPDX-License-Identifier: GPL-2.0-or-later - + + + diff --git a/src/SharpEmu.GUI/SndPreviewPlayer.cs b/src/SharpEmu.GUI/SndPreviewPlayer.cs new file mode 100644 index 0000000..09c413b --- /dev/null +++ b/src/SharpEmu.GUI/SndPreviewPlayer.cs @@ -0,0 +1,301 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; +using LibAtrac9; + +namespace SharpEmu.GUI; + +/// +/// Loops a game's sce_sys/snd0.at9 preview music while the game is selected +/// in the library, like the console home screen. The ATRAC9 stream is decoded +/// to WAV on a background task (vendored LibAtrac9); playback uses winmm's +/// PlaySound, so this is Windows-only and a no-op elsewhere. +/// +internal sealed class SndPreviewPlayer +{ + private const uint SND_ASYNC = 0x0001; + private const uint SND_NODEFAULT = 0x0002; + private const uint SND_MEMORY = 0x0004; + private const uint SND_LOOP = 0x0008; + + private readonly object _sync = new(); + private int _generation; + private GCHandle _pinnedWav; + private bool _playing; + private bool _paused; + private string? _cachedPath; + private byte[]? _cachedWav; + + /// Starts looping the given snd0.at9 after a short debounce. + public void Play(string at9Path) + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + int generation; + lock (_sync) + { + generation = ++_generation; + } + + _ = Task.Run(async () => + { + // Debounce so skimming through the library does not decode (or + // start) a preview per tile. + await Task.Delay(300).ConfigureAwait(false); + + byte[]? wav; + lock (_sync) + { + if (generation != _generation) + { + return; + } + + wav = string.Equals(_cachedPath, at9Path, StringComparison.OrdinalIgnoreCase) + ? _cachedWav + : null; + } + + if (wav is null) + { + try + { + wav = DecodeAt9ToWav(File.ReadAllBytes(at9Path)); + } + catch (Exception) + { + return; // corrupt or unsupported preview: stay silent + } + } + + lock (_sync) + { + if (generation != _generation) + { + return; + } + + _cachedPath = at9Path; + _cachedWav = wav; + StopLocked(); + + // The WAV image must stay pinned while winmm plays from it. + _pinnedWav = GCHandle.Alloc(wav, GCHandleType.Pinned); + _playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT); + if (!_playing) + { + _pinnedWav.Free(); + } + } + }); + } + + public void Stop() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + lock (_sync) + { + _generation++; + StopLocked(); + } + } + + /// + /// Silences playback but keeps the decoded track ready, so + /// can restart it (winmm cannot truly pause). + /// + public void Pause() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + lock (_sync) + { + if (!_playing) + { + return; + } + + _ = PlaySound(0, 0, 0); + _playing = false; + _paused = true; + } + } + + /// Restarts the track silenced by . + public void Resume() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + lock (_sync) + { + if (!_paused || !_pinnedWav.IsAllocated) + { + return; + } + + _paused = false; + _playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT); + } + } + + private void StopLocked() + { + _ = PlaySound(0, 0, 0); + _playing = false; + _paused = false; + if (_pinnedWav.IsAllocated) + { + _pinnedWav.Free(); + } + } + + private static readonly Guid Atrac9SubFormat = new("47E142D2-36BA-4D8D-88FC-61654F8C836C"); + + /// + /// Decodes a Sony AT9 (RIFF-wrapped ATRAC9) file to a PCM16 WAV image. + /// Layout per Sony's container: an extensible fmt chunk whose extension + /// carries the 4-byte codec config, a fact chunk with the sample count + /// and encoder delay, and superframes in the data chunk. + /// + private static byte[] DecodeAt9ToWav(byte[] file) + { + using var reader = new BinaryReader(new MemoryStream(file), Encoding.ASCII); + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF") + { + throw new InvalidDataException("Not a RIFF file."); + } + + reader.BaseStream.Position += 4; // riff size + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE") + { + throw new InvalidDataException("Not a WAVE file."); + } + + byte[]? configData = null; + var sampleCount = 0; + var encoderDelay = 0; + var dataOffset = -1; + var dataSize = 0; + + while (reader.BaseStream.Position + 8 <= reader.BaseStream.Length) + { + var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4)); + var chunkSize = reader.ReadInt32(); + var chunkStart = reader.BaseStream.Position; + switch (chunkId) + { + case "fmt ": + var formatTag = reader.ReadUInt16(); + reader.BaseStream.Position = chunkStart + 24; // to SubFormat GUID + var subFormat = new Guid(reader.ReadBytes(16)); + if (formatTag != 0xFFFE || subFormat != Atrac9SubFormat) + { + throw new InvalidDataException("Not an ATRAC9 stream."); + } + + reader.BaseStream.Position += 4; // version info + configData = reader.ReadBytes(4); + break; + case "fact": + sampleCount = reader.ReadInt32(); + reader.BaseStream.Position += 4; // input overlap delay + encoderDelay = reader.ReadInt32(); + break; + case "data": + dataOffset = (int)chunkStart; + dataSize = chunkSize; + break; + } + + reader.BaseStream.Position = chunkStart + chunkSize + (chunkSize & 1); + } + + if (configData is null || sampleCount <= 0 || dataOffset < 0) + { + throw new InvalidDataException("Missing fmt, fact, or data chunk."); + } + + var decoder = new Atrac9Decoder(); + decoder.Initialize(configData); + var config = decoder.Config; + + var superframeCount = (sampleCount + encoderDelay + config.SuperframeSamples - 1) / config.SuperframeSamples; + superframeCount = Math.Min(superframeCount, dataSize / config.SuperframeBytes); + + var channels = config.ChannelCount; + var pcmBuffer = new short[channels][]; + for (var i = 0; i < channels; i++) + { + pcmBuffer[i] = new short[config.SuperframeSamples]; + } + + var wav = new byte[44 + (sampleCount * channels * 2)]; + WriteWavHeader(wav, channels, config.SampleRate, sampleCount); + + var superframe = new byte[config.SuperframeBytes]; + var decodedIndex = 0L; // per-channel, includes the encoder delay + var written = 0; + for (var f = 0; f < superframeCount && written < sampleCount; f++) + { + Buffer.BlockCopy(file, dataOffset + (f * config.SuperframeBytes), superframe, 0, config.SuperframeBytes); + decoder.Decode(superframe, pcmBuffer); + for (var s = 0; s < config.SuperframeSamples && written < sampleCount; s++) + { + if (decodedIndex++ < encoderDelay) + { + continue; + } + + var sampleOffset = 44 + ((long)written * channels * 2); + for (var ch = 0; ch < channels; ch++) + { + BinaryPrimitives.WriteInt16LittleEndian( + wav.AsSpan((int)(sampleOffset + (ch * 2))), + pcmBuffer[ch][s]); + } + + written++; + } + } + + return wav; + } + + private static void WriteWavHeader(byte[] wav, int channels, int sampleRate, int sampleCount) + { + var span = wav.AsSpan(); + Encoding.ASCII.GetBytes("RIFF").CopyTo(span); + BinaryPrimitives.WriteInt32LittleEndian(span[4..], wav.Length - 8); + Encoding.ASCII.GetBytes("WAVE").CopyTo(span[8..]); + Encoding.ASCII.GetBytes("fmt ").CopyTo(span[12..]); + BinaryPrimitives.WriteInt32LittleEndian(span[16..], 16); + BinaryPrimitives.WriteInt16LittleEndian(span[20..], 1); // PCM + BinaryPrimitives.WriteInt16LittleEndian(span[22..], (short)channels); + BinaryPrimitives.WriteInt32LittleEndian(span[24..], sampleRate); + BinaryPrimitives.WriteInt32LittleEndian(span[28..], sampleRate * channels * 2); + BinaryPrimitives.WriteInt16LittleEndian(span[32..], (short)(channels * 2)); + BinaryPrimitives.WriteInt16LittleEndian(span[34..], 16); // bits per sample + Encoding.ASCII.GetBytes("data").CopyTo(span[36..]); + BinaryPrimitives.WriteInt32LittleEndian(span[40..], sampleCount * channels * 2); + } + + [DllImport("winmm.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool PlaySound(nint sound, nint module, uint flags); +} diff --git a/src/SharpEmu.GUI/packages.lock.json b/src/SharpEmu.GUI/packages.lock.json index 51e7f67..87726bf 100644 --- a/src/SharpEmu.GUI/packages.lock.json +++ b/src/SharpEmu.GUI/packages.lock.json @@ -180,6 +180,9 @@ "type": "Transitive", "resolved": "2.88.9", "contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w==" + }, + "sharpemu.logging": { + "type": "Project" } } } diff --git a/src/SharpEmu.Libs/Pad/DualSenseReader.cs b/src/SharpEmu.Libs/Pad/DualSenseReader.cs index 92ad3ee..7094cdc 100644 --- a/src/SharpEmu.Libs/Pad/DualSenseReader.cs +++ b/src/SharpEmu.Libs/Pad/DualSenseReader.cs @@ -5,21 +5,6 @@ using Microsoft.Win32.SafeHandles; namespace SharpEmu.Libs.Pad; -/// -/// Snapshot of the DualSense state, already translated to ORBIS pad -/// conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered; -/// triggers 0..255). -/// -internal readonly record struct DualSenseState( - bool Connected, - uint Buttons, - byte LeftX, - byte LeftY, - byte RightX, - byte RightY, - byte L2, - byte R2); - /// /// Reads a DualSense controller over raw HID on a background thread. /// Supports USB (input report 0x01) and Bluetooth (extended report 0x31, @@ -31,26 +16,8 @@ internal static class DualSenseReader private const ushort DualSenseProductId = 0x0CE6; private const ushort DualSenseEdgeProductId = 0x0DF2; - // SCE_PAD_BUTTON bit values. - private const uint ButtonL3 = 0x0002; - private const uint ButtonR3 = 0x0004; - private const uint ButtonOptions = 0x0008; - private const uint ButtonUp = 0x0010; - private const uint ButtonRight = 0x0020; - private const uint ButtonDown = 0x0040; - private const uint ButtonLeft = 0x0080; - private const uint ButtonL2 = 0x0100; - private const uint ButtonR2 = 0x0200; - private const uint ButtonL1 = 0x0400; - private const uint ButtonR1 = 0x0800; - private const uint ButtonTriangle = 0x1000; - private const uint ButtonCircle = 0x2000; - private const uint ButtonCross = 0x4000; - private const uint ButtonSquare = 0x8000; - private const uint ButtonTouchPad = 0x100000; - private static readonly object Gate = new(); - private static DualSenseState _state; + private static PadState _state; private static bool _started; // Output (rumble/lightbar) state, all guarded by Gate. @@ -92,7 +59,7 @@ internal static class DualSenseReader } } - internal static bool TryGetState(out DualSenseState state) + internal static bool TryGetState(out PadState state) { lock (Gate) { @@ -102,7 +69,7 @@ internal static class DualSenseReader return state.Connected; } - private static void SetState(in DualSenseState state) + private static void SetState(in PadState state) { lock (Gate) { @@ -399,7 +366,7 @@ internal static class DualSenseReader return null; } - private static bool TryParseReport(ReadOnlySpan report, out DualSenseState state) + private static bool TryParseReport(ReadOnlySpan report, out PadState state) { // USB: report id 0x01, payload starts at [1]. // Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2]. @@ -429,21 +396,21 @@ internal static class DualSenseReader var buttons2 = report[offset + 9]; uint buttons = 0; - buttons |= (buttons0 & 0x10) != 0 ? ButtonSquare : 0; - buttons |= (buttons0 & 0x20) != 0 ? ButtonCross : 0; - buttons |= (buttons0 & 0x40) != 0 ? ButtonCircle : 0; - buttons |= (buttons0 & 0x80) != 0 ? ButtonTriangle : 0; + buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0; + buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0; + buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0; + buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0; buttons |= HatToButtons(buttons0 & 0x0F); - buttons |= (buttons1 & 0x01) != 0 ? ButtonL1 : 0; - buttons |= (buttons1 & 0x02) != 0 ? ButtonR1 : 0; - buttons |= (buttons1 & 0x04) != 0 ? ButtonL2 : 0; - buttons |= (buttons1 & 0x08) != 0 ? ButtonR2 : 0; - buttons |= (buttons1 & 0x20) != 0 ? ButtonOptions : 0; - buttons |= (buttons1 & 0x40) != 0 ? ButtonL3 : 0; - buttons |= (buttons1 & 0x80) != 0 ? ButtonR3 : 0; - buttons |= (buttons2 & 0x02) != 0 ? ButtonTouchPad : 0; + buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0; + buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0; + buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0; + buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0; + buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0; + buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0; + buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0; + buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0; - state = new DualSenseState( + state = new PadState( Connected: true, Buttons: buttons, LeftX: leftX, @@ -457,14 +424,14 @@ internal static class DualSenseReader private static uint HatToButtons(int hat) => hat switch { - 0 => ButtonUp, - 1 => ButtonUp | ButtonRight, - 2 => ButtonRight, - 3 => ButtonRight | ButtonDown, - 4 => ButtonDown, - 5 => ButtonDown | ButtonLeft, - 6 => ButtonLeft, - 7 => ButtonLeft | ButtonUp, + 0 => OrbisPadButton.Up, + 1 => OrbisPadButton.Up | OrbisPadButton.Right, + 2 => OrbisPadButton.Right, + 3 => OrbisPadButton.Right | OrbisPadButton.Down, + 4 => OrbisPadButton.Down, + 5 => OrbisPadButton.Down | OrbisPadButton.Left, + 6 => OrbisPadButton.Left, + 7 => OrbisPadButton.Left | OrbisPadButton.Up, _ => 0, }; } diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index dc9f3f8..459de27 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -31,6 +31,7 @@ public static class PadExports { _initialized = true; DualSenseReader.EnsureStarted(); + XInputReader.EnsureStarted(); return ctx.SetReturn(0); } @@ -61,9 +62,12 @@ public static class PadExports } DualSenseReader.EnsureStarted(); + XInputReader.EnsureStarted(); Console.Error.WriteLine(DualSenseReader.TryGetState(out _) ? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)." - : "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense will be used automatically when plugged in."); + : XInputReader.TryGetState(out _) + ? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)." + : "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in."); return ctx.SetReturn(PrimaryPadHandle); } @@ -201,6 +205,7 @@ public static class PadExports } DualSenseReader.SetRumble(parameter[0], parameter[1]); + XInputReader.SetRumble(parameter[0], parameter[1]); return ctx.SetReturn(0); } @@ -277,6 +282,17 @@ public static class PadExports r2 = Math.Max(r2, pad.R2); } + if (XInputReader.TryGetState(out var xpad)) + { + buttons |= xpad.Buttons; + leftX = MergeAxis(xpad.LeftX, leftX); + leftY = MergeAxis(xpad.LeftY, leftY); + rightX = MergeAxis(xpad.RightX, rightX); + rightY = MergeAxis(xpad.RightY, rightY); + l2 = Math.Max(l2, xpad.L2); + r2 = Math.Max(r2, xpad.R2); + } + BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons); data[0x04] = leftX; data[0x05] = leftY; diff --git a/src/SharpEmu.Libs/Pad/PadState.cs b/src/SharpEmu.Libs/Pad/PadState.cs new file mode 100644 index 0000000..c934255 --- /dev/null +++ b/src/SharpEmu.Libs/Pad/PadState.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.Pad; + +/// +/// Snapshot of a physical controller's state, already translated to ORBIS +/// pad conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered +/// and Y growing downward; triggers 0..255). +/// +internal readonly record struct PadState( + bool Connected, + uint Buttons, + byte LeftX, + byte LeftY, + byte RightX, + byte RightY, + byte L2, + byte R2); + +/// SCE_PAD_BUTTON bit values. +internal static class OrbisPadButton +{ + internal const uint L3 = 0x0002; + internal const uint R3 = 0x0004; + internal const uint Options = 0x0008; + internal const uint Up = 0x0010; + internal const uint Right = 0x0020; + internal const uint Down = 0x0040; + internal const uint Left = 0x0080; + internal const uint L2 = 0x0100; + internal const uint R2 = 0x0200; + internal const uint L1 = 0x0400; + internal const uint R1 = 0x0800; + internal const uint Triangle = 0x1000; + internal const uint Circle = 0x2000; + internal const uint Cross = 0x4000; + internal const uint Square = 0x8000; + internal const uint TouchPad = 0x100000; +} diff --git a/src/SharpEmu.Libs/Pad/XInputReader.cs b/src/SharpEmu.Libs/Pad/XInputReader.cs new file mode 100644 index 0000000..9336120 --- /dev/null +++ b/src/SharpEmu.Libs/Pad/XInputReader.cs @@ -0,0 +1,246 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Runtime.InteropServices; + +namespace SharpEmu.Libs.Pad; + +/// +/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via +/// the Windows XInput API on a background thread, translated to the same +/// ORBIS pad conventions as . Supports rumble +/// and hot-plug retry; the first connected slot (of four) is used. +/// +internal static class XInputReader +{ + private const uint ErrorSuccess = 0; + private const int SlotCount = 4; + private const byte TriggerThreshold = 30; // XINPUT_GAMEPAD_TRIGGER_THRESHOLD + + // XINPUT_GAMEPAD wButtons bit values. + private const ushort XinputDpadUp = 0x0001; + private const ushort XinputDpadDown = 0x0002; + private const ushort XinputDpadLeft = 0x0004; + private const ushort XinputDpadRight = 0x0008; + private const ushort XinputStart = 0x0010; + private const ushort XinputBack = 0x0020; + private const ushort XinputLeftThumb = 0x0040; + private const ushort XinputRightThumb = 0x0080; + private const ushort XinputLeftShoulder = 0x0100; + private const ushort XinputRightShoulder = 0x0200; + private const ushort XinputA = 0x1000; + private const ushort XinputB = 0x2000; + private const ushort XinputX = 0x4000; + private const ushort XinputY = 0x8000; + + private static readonly object Gate = new(); + private static PadState _state; + private static bool _started; + private static int _slot = -1; // connected XInput user index, -1 when none + private static byte _motorLeft; + private static byte _motorRight; + + /// Starts the background reader once; safe to call repeatedly. + internal static void EnsureStarted() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + lock (Gate) + { + if (_started) + { + return; + } + + _started = true; + var thread = new Thread(ReadLoop) + { + IsBackground = true, + Name = "XInputReader", + }; + thread.Start(); + } + } + + internal static bool TryGetState(out PadState state) + { + lock (Gate) + { + state = _state; + } + + return state.Connected; + } + + private static void SetState(in PadState state) + { + lock (Gate) + { + _state = state; + } + } + + /// Sets rumble; large = left/strong motor, small = right/weak. + internal static void SetRumble(byte largeMotor, byte smallMotor) + { + lock (Gate) + { + if (_motorLeft == largeMotor && _motorRight == smallMotor) + { + return; + } + + _motorLeft = largeMotor; + _motorRight = smallMotor; + SendRumbleLocked(); + } + } + + private static void SendRumbleLocked() + { + if (_slot < 0) + { + return; // resent on connect + } + + var vibration = new XInputVibration + { + LeftMotorSpeed = (ushort)(_motorLeft * 257), // 0..255 -> 0..65535 + RightMotorSpeed = (ushort)(_motorRight * 257), + }; + _ = XInputSetState((uint)_slot, ref vibration); + } + + private static void ReadLoop() + { + try + { + while (true) + { + var slot = FindConnectedSlot(); + if (slot < 0) + { + SetState(default); + Thread.Sleep(1000); + continue; + } + + lock (Gate) + { + _slot = slot; + SendRumbleLocked(); + } + + Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller connected."); + while (XInputGetState((uint)slot, out var state) == ErrorSuccess) + { + SetState(Translate(state.Gamepad)); + Thread.Sleep(8); + } + + Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller disconnected."); + lock (Gate) + { + _slot = -1; + _motorLeft = 0; + _motorRight = 0; + _state = default; + } + + Thread.Sleep(1000); + } + } + catch (DllNotFoundException) + { + // XInput unavailable on this system; leave the reader disconnected. + } + catch (EntryPointNotFoundException) + { + } + } + + private static int FindConnectedSlot() + { + for (var index = 0; index < SlotCount; index++) + { + if (XInputGetState((uint)index, out _) == ErrorSuccess) + { + return index; + } + } + + return -1; + } + + private static PadState Translate(in XInputGamepad pad) + { + uint buttons = 0; + buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0; + buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0; + buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0; + buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0; + buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0; + buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0; + buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0; + buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0; + buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0; + buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0; + buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0; + buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0; + buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0; + buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0; + buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0; + buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0; + + return new PadState( + Connected: true, + Buttons: buttons, + LeftX: AxisToByte(pad.ThumbLX), + LeftY: AxisToByteInverted(pad.ThumbLY), + RightX: AxisToByte(pad.ThumbRX), + RightY: AxisToByteInverted(pad.ThumbRY), + L2: pad.LeftTrigger, + R2: pad.RightTrigger); + } + + private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8); + + // XInput Y grows upward, ORBIS pads report Y growing downward. + private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8)); + + [StructLayout(LayoutKind.Sequential)] + private struct XInputGamepad + { + public ushort Buttons; + public byte LeftTrigger; + public byte RightTrigger; + public short ThumbLX; + public short ThumbLY; + public short ThumbRX; + public short ThumbRY; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XInputState + { + public uint PacketNumber; + public XInputGamepad Gamepad; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XInputVibration + { + public ushort LeftMotorSpeed; + public ushort RightMotorSpeed; + } + + // xinput1_4.dll ships with Windows 8 and later. + [DllImport("xinput1_4.dll")] + private static extern uint XInputGetState(uint userIndex, out XInputState state); + + [DllImport("xinput1_4.dll")] + private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration); +} diff --git a/src/SharpEmu.Logging/SharpEmuLog.cs b/src/SharpEmu.Logging/SharpEmuLog.cs index e3dcac9..f778bb6 100644 --- a/src/SharpEmu.Logging/SharpEmuLog.cs +++ b/src/SharpEmu.Logging/SharpEmuLog.cs @@ -12,8 +12,14 @@ public static class SharpEmuLog new(StringComparer.Ordinal); private static readonly object ConfigurationSync = new(); private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment(); + private static bool _fileCapturesAllLevels; private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment(); + /// + /// Entries below this level are dropped. When a SHARPEMU_LOG_FILE sink is + /// active it only limits the console — the file receives every level. + /// disables logging entirely, file included. + /// public static LogLevel MinimumLevel { get => _minimumLevel; @@ -40,6 +46,11 @@ public static class SharpEmuLog return; } + // A replacement sink is not the environment-configured + // console+file pair, so the minimum level applies globally + // again. + _fileCapturesAllLevels = false; + if (_sink is IDisposable disposable) { try @@ -122,7 +133,14 @@ public static class SharpEmuLog internal static bool IsEnabled(LogLevel level) { var minimum = _minimumLevel; - return minimum != LogLevel.None && level >= minimum; + if (minimum == LogLevel.None) + { + return false; + } + + // With a file sink capturing all levels, the console filter is + // applied per-sink instead of here. + return _fileCapturesAllLevels || level >= minimum; } internal static void Write( @@ -187,7 +205,10 @@ public static class SharpEmuLog try { var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true); - return new CompositeLogSink(consoleSink, fileSink); + // The file gets every level; the configured minimum only + // limits what reaches the console. + _fileCapturesAllLevels = true; + return new CompositeLogSink(new MinimumLevelFilterSink(consoleSink), fileSink); } catch (Exception ex) { @@ -199,6 +220,25 @@ public static class SharpEmuLog return consoleSink; } + /// + /// Forwards only entries at or above . Wraps + /// the console sink when a file sink captures all levels. + /// + private sealed class MinimumLevelFilterSink : ISharpEmuLogSink + { + private readonly ISharpEmuLogSink _inner; + + internal MinimumLevelFilterSink(ISharpEmuLogSink inner) => _inner = inner; + + public void Write(in LogEntry entry) + { + if (entry.Level >= _minimumLevel) + { + _inner.Write(in entry); + } + } + } + private static bool IsTrueLike(string? text) { if (string.IsNullOrWhiteSpace(text))