diff --git a/build/Build.csproj b/build/Build.csproj index 4108b33e..ff82c8e9 100644 --- a/build/Build.csproj +++ b/build/Build.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/build/NuGet.Config b/build/NuGet.Config deleted file mode 100644 index 09f2371f..00000000 --- a/build/NuGet.Config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/VGAudio.Cli/CliArguments.cs b/src/VGAudio.Cli/CliArguments.cs index da78467b..25637611 100644 --- a/src/VGAudio.Cli/CliArguments.cs +++ b/src/VGAudio.Cli/CliArguments.cs @@ -353,6 +353,9 @@ public static Options Parse(string[] args) case "NAMCO": nxHeaderType = NxOpusHeaderType.Namco; break; + case "KTSS": + nxHeaderType = NxOpusHeaderType.Ktss; + break; default: Console.WriteLine("Invalid header type"); return null; @@ -361,6 +364,9 @@ public static Options Parse(string[] args) options.NxOpusHeaderType = nxHeaderType; i++; continue; + case "-CBR": + options.EncodeCbr = true; + continue; } } @@ -560,12 +566,14 @@ private static void PrintUsage() Console.WriteLine("\nHCA Options:"); Console.WriteLine(" --hcaquality The quality level to use for the HCA file"); - Console.WriteLine(" --bitrate The bitrate in bps of the output HCA file"); + Console.WriteLine(" --bitrate The bitrate in bits per second of the output HCA file"); Console.WriteLine(" --bitrate takes precedence over --hcaquality"); Console.WriteLine(" --limit-bitrate This flag sets a limit on how low the bitrate can go"); Console.WriteLine(" This limit depends on the properties of the input file"); Console.WriteLine("\nSwitch Opus Options:"); + Console.WriteLine(" --bitrate The bitrate in bits per second of the output file"); + Console.WriteLine(" --cbr Encode the file using a constant bitrate"); Console.WriteLine(" --opusheader The type of header to use for the generated Opus file"); Console.WriteLine(" Available types: " + opusHeaderTypes); } diff --git a/src/VGAudio.Cli/ContainerTypes.cs b/src/VGAudio.Cli/ContainerTypes.cs index 7a161202..0f5a3a30 100644 --- a/src/VGAudio.Cli/ContainerTypes.cs +++ b/src/VGAudio.Cli/ContainerTypes.cs @@ -26,7 +26,7 @@ internal static class ContainerTypes [FileType.Bcstm] = new ContainerType(new[] { "bcstm" }, () => new BCFstmReader(), () => new BCFstmWriter(NwTarget.Ctr), CreateConfiguration.Bxstm), [FileType.Bfstm] = new ContainerType(new[] { "bfstm" }, () => new BCFstmReader(), () => new BCFstmWriter(NwTarget.Cafe), CreateConfiguration.Bxstm), [FileType.Brwav] = new ContainerType(new[] { "brwav", "rwav" }, () => new BrwavReader(), null, CreateConfiguration.Bxstm), - [FileType.Bcwav] = new ContainerType(new[] { "bcwav", "cwav" }, () => new BCFstmReader(), null, CreateConfiguration.Bxstm), + [FileType.Bcwav] = new ContainerType(new[] { "bcwav", "cwav", "bms" }, () => new BCFstmReader(), null, CreateConfiguration.Bxstm), [FileType.Bfwav] = new ContainerType(new[] { "bfwav" }, () => new BCFstmReader(), null, CreateConfiguration.Bxstm), [FileType.Bcstp] = new ContainerType(new[] { "bcstp" }, () => new BCFstmReader(), null, CreateConfiguration.Bxstm), [FileType.Bfstp] = new ContainerType(new[] { "bfstp" }, () => new BCFstmReader(), null, CreateConfiguration.Bxstm), @@ -35,7 +35,7 @@ internal static class ContainerTypes [FileType.Hca] = new ContainerType(new[] { "hca" }, () => new HcaReader(), () => new HcaWriter(), CreateConfiguration.Hca), [FileType.Genh] = new ContainerType(new[] { "genh" }, () => new GenhReader(), null, null), [FileType.Atrac9] = new ContainerType(new[] { "at9" }, () => new At9Reader(), null, null), - [FileType.NxOpus] = new ContainerType(new[] { "lopus", "nop" }, () => new NxOpusReader(), () => new NxOpusWriter(), CreateConfiguration.NxOpus), + [FileType.NxOpus] = new ContainerType(new[] { "lopus", "nop", "ktss", "kns" }, () => new NxOpusReader(), () => new NxOpusWriter(), CreateConfiguration.NxOpus), [FileType.OggOpus] = new ContainerType(new[] { "opus" }, () => new OggOpusReader(), () => new OggOpusWriter(), CreateConfiguration.NxOpus) }; diff --git a/src/VGAudio.Cli/CreateConfiguration.cs b/src/VGAudio.Cli/CreateConfiguration.cs index 17c3f715..3367c57c 100644 --- a/src/VGAudio.Cli/CreateConfiguration.cs +++ b/src/VGAudio.Cli/CreateConfiguration.cs @@ -144,6 +144,8 @@ public static Configuration Hca(Options options, Configuration inConfig = null) config.LimitBitrate = options.LimitBitrate; if (options.Bitrate != 0) config.Bitrate = options.Bitrate; + if (options.KeyCode != 0) config.EncryptionKey = new CriHcaKey(options.KeyCode); + return config; } @@ -152,6 +154,7 @@ public static Configuration NxOpus(Options options, Configuration inConfig = nul NxOpusConfiguration config = inConfig as NxOpusConfiguration ?? new NxOpusConfiguration(); config.HeaderType = options.NxOpusHeaderType; + config.EncodeCbr = options.EncodeCbr; if (options.Bitrate != 0) config.Bitrate = options.Bitrate; return config; diff --git a/src/VGAudio.Cli/Options.cs b/src/VGAudio.Cli/Options.cs index db213669..daa205de 100644 --- a/src/VGAudio.Cli/Options.cs +++ b/src/VGAudio.Cli/Options.cs @@ -42,6 +42,7 @@ internal class Options public bool LimitBitrate { get; set; } public NxOpusHeaderType NxOpusHeaderType { get; set; } // Switch Opus + public bool EncodeCbr { get; set; } } internal class JobFiles diff --git a/src/VGAudio/Codecs/CriHca/CriHcaEncryption.cs b/src/VGAudio/Codecs/CriHca/CriHcaEncryption.cs index 1ba61717..3b16639e 100644 --- a/src/VGAudio/Codecs/CriHca/CriHcaEncryption.cs +++ b/src/VGAudio/Codecs/CriHca/CriHcaEncryption.cs @@ -7,20 +7,28 @@ public static partial class CriHcaEncryption { private const int FramesToTest = 10; - public static void Decrypt(HcaInfo hca, byte[][] audio, CriHcaKey key) + private static Crc16 Crc { get; } = new Crc16(0x8005); + + public static void Crypt(HcaInfo hca, byte[][] audio, CriHcaKey key, bool doDecrypt) { for (int frame = 0; frame < hca.FrameCount; frame++) { - DecryptFrame(hca, audio[frame], key); + CryptFrame(hca, audio[frame], key, doDecrypt); } } - public static void DecryptFrame(HcaInfo hca, byte[] audio, CriHcaKey key) + public static void CryptFrame(HcaInfo hca, byte[] audio, CriHcaKey key, bool doDecrypt) { - for (int b = 0; b < hca.FrameSize; b++) + byte[] substitutionTable = doDecrypt ? key.DecryptionTable : key.EncryptionTable; + + for (int b = 0; b < hca.FrameSize - 2; b++) { - audio[b] = key.DecryptionTable[audio[b]]; + audio[b] = substitutionTable[audio[b]]; } + + ushort crc = Crc.Compute(audio, hca.FrameSize - 2); + audio[hca.FrameSize - 2] = (byte)(crc >> 8); + audio[hca.FrameSize - 1] = (byte)crc; } public static CriHcaKey FindKey(HcaInfo hca, byte[][] audio) @@ -44,7 +52,7 @@ private static bool TestKey(CriHcaFrame frame, byte[][] audio, CriHcaKey key, by for (int i = startFrame; i < endFrame; i++) { Array.Copy(audio[i], buffer, audio[i].Length); - DecryptFrame(frame.Hca, buffer, key); + CryptFrame(frame.Hca, buffer, key, true); var reader = new BitReader(buffer); if (!CriHcaPacking.UnpackFrame(frame, reader)) { diff --git a/src/VGAudio/Codecs/CriHca/CriHcaKey.cs b/src/VGAudio/Codecs/CriHca/CriHcaKey.cs index dae9d396..61b1edca 100644 --- a/src/VGAudio/Codecs/CriHca/CriHcaKey.cs +++ b/src/VGAudio/Codecs/CriHca/CriHcaKey.cs @@ -10,6 +10,7 @@ public CriHcaKey(ulong keyCode) KeyCode = keyCode; DecryptionTable = CreateDecryptionTable(keyCode); EncryptionTable = InvertTable(DecryptionTable); + KeyType = 56; } public CriHcaKey(Type type) @@ -18,9 +19,11 @@ public CriHcaKey(Type type) { case Type.Type0: DecryptionTable = CreateDecryptionTableType0(); + KeyType = 0; break; case Type.Type1: DecryptionTable = CreateDecryptionTableType1(); + KeyType = 1; break; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); @@ -29,6 +32,7 @@ public CriHcaKey(Type type) EncryptionTable = InvertTable(DecryptionTable); } + public int KeyType { get; } public ulong KeyCode { get; } public byte[] DecryptionTable { get; } public byte[] EncryptionTable { get; } diff --git a/src/VGAudio/Codecs/CriHca/HcaInfo.cs b/src/VGAudio/Codecs/CriHca/HcaInfo.cs index 9cf98823..8fa70e1b 100644 --- a/src/VGAudio/Codecs/CriHca/HcaInfo.cs +++ b/src/VGAudio/Codecs/CriHca/HcaInfo.cs @@ -54,5 +54,7 @@ public void CalculateHfrValues() HfrBandCount = TotalBandCount - BaseBandCount - StereoBandCount; HfrGroupCount = HfrBandCount.DivideByRoundUp(BandsPerHfrGroup); } + + public HcaInfo GetClone() => (HcaInfo)MemberwiseClone(); } } diff --git a/src/VGAudio/Codecs/Opus/OpusParameters.cs b/src/VGAudio/Codecs/Opus/OpusParameters.cs index 083f6cb8..84f9e295 100644 --- a/src/VGAudio/Codecs/Opus/OpusParameters.cs +++ b/src/VGAudio/Codecs/Opus/OpusParameters.cs @@ -5,5 +5,6 @@ public class OpusParameters : CodecParameters public OpusParameters() { } public OpusParameters(CodecParameters source) : base(source) { } public int Bitrate { get; set; } + public bool EncodeCbr { get; set; } } } diff --git a/src/VGAudio/Containers/Hca/HcaReader.cs b/src/VGAudio/Containers/Hca/HcaReader.cs index 68e2a964..95da72d2 100644 --- a/src/VGAudio/Containers/Hca/HcaReader.cs +++ b/src/VGAudio/Containers/Hca/HcaReader.cs @@ -40,7 +40,9 @@ protected override IAudioFormat ToAudioStream(HcaStructure structure) { if (structure.EncryptionKey != null) { - CriHcaEncryption.Decrypt(structure.Hca, structure.AudioData, structure.EncryptionKey); + CriHcaEncryption.Crypt(structure.Hca, structure.AudioData, structure.EncryptionKey, true); + + structure.Hca.EncryptionType = 0; } return new CriHcaFormatBuilder(structure.AudioData, structure.Hca).Build(); @@ -60,6 +62,7 @@ private static void ReadHcaHeader(BinaryReader reader, HcaStructure structure) string signature = ReadChunkId(reader); structure.Version = reader.ReadInt16(); structure.HeaderSize = reader.ReadInt16(); + hca.HeaderSize = structure.HeaderSize; if (signature != "HCA\0") { diff --git a/src/VGAudio/Containers/Hca/HcaWriter.cs b/src/VGAudio/Containers/Hca/HcaWriter.cs index 9b39a45d..88370404 100644 --- a/src/VGAudio/Containers/Hca/HcaWriter.cs +++ b/src/VGAudio/Containers/Hca/HcaWriter.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text; using VGAudio.Codecs.CriHca; using VGAudio.Formats; using VGAudio.Formats.CriHca; @@ -34,6 +35,13 @@ protected override void SetupWriter(AudioData audio) var hcaFormat = audio.GetFormat(encodingConfig); Hca = hcaFormat.Hca; AudioData = hcaFormat.AudioData; + + if (Configuration.EncryptionKey != null) + { + CriHcaEncryption.Crypt(Hca, AudioData, Configuration.EncryptionKey, false); + + Hca.EncryptionType = Configuration.EncryptionKey.KeyType; + } } protected override void WriteStream(Stream stream) @@ -72,14 +80,14 @@ private void WriteHeader(BinaryWriter writer) private void WriteHcaChunk(BinaryWriter writer) { - writer.WriteUTF8("HCA\0"); + WriteChunkId(writer, "HCA\0"); writer.Write(Version); writer.Write((short)HeaderSize); } private void WriteFmtChunk(BinaryWriter writer) { - writer.WriteUTF8("fmt\0"); + WriteChunkId(writer, "fmt\0"); writer.Write((byte)Hca.ChannelCount); // Sample Rate is 24-bit @@ -93,7 +101,7 @@ private void WriteFmtChunk(BinaryWriter writer) private void WriteCompChunk(BinaryWriter writer) { - writer.WriteUTF8("comp"); + WriteChunkId(writer, "comp"); writer.Write((short)Hca.FrameSize); writer.Write((byte)Hca.MinResolution); writer.Write((byte)Hca.MaxResolution); @@ -110,7 +118,7 @@ private void WriteLoopChunk(BinaryWriter writer) { if (!Hca.Looping) return; - writer.WriteUTF8("loop"); + WriteChunkId(writer, "loop"); writer.Write(Hca.LoopStartFrame); writer.Write(Hca.LoopEndFrame); writer.Write((short)Hca.PreLoopSamples); @@ -119,7 +127,7 @@ private void WriteLoopChunk(BinaryWriter writer) private void WriteCiphChunk(BinaryWriter writer) { - writer.WriteUTF8("ciph"); + WriteChunkId(writer, "ciph"); writer.Write((short)Hca.EncryptionType); } @@ -129,20 +137,35 @@ private void WriteRvaChunk(BinaryWriter writer) // ReSharper disable once CompareOfFloatsByEqualityOperator if (volume != 1) { - writer.WriteUTF8("rva\0"); + WriteChunkId(writer, "rva\0"); writer.Write(volume); } } private void WriteCommChunk(BinaryWriter writer) { - writer.WriteUTF8("comm\0"); + WriteChunkId(writer, "comm\0"); writer.WriteUTF8Z(Hca.Comment); } private void WritePadChunk(BinaryWriter writer) { - writer.WriteUTF8("pad"); + WriteChunkId(writer, "pad"); + } + + private void WriteChunkId(BinaryWriter writer, string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value); + + if (Configuration.EncryptionKey != null) + { + for (int i = 0; i < bytes.Length; i++) + { + if (bytes[i] != 0) bytes[i] |= 0x80; + } + } + + writer.Write(bytes); } private void WriteData(BinaryWriter writer) diff --git a/src/VGAudio/Containers/Opus/NxOpusConfiguration.cs b/src/VGAudio/Containers/Opus/NxOpusConfiguration.cs index 0a07834c..396723b3 100644 --- a/src/VGAudio/Containers/Opus/NxOpusConfiguration.cs +++ b/src/VGAudio/Containers/Opus/NxOpusConfiguration.cs @@ -4,5 +4,6 @@ public class NxOpusConfiguration : Configuration { public NxOpusHeaderType HeaderType { get; set; } public int Bitrate { get; set; } + public bool EncodeCbr { get; set; } } } diff --git a/src/VGAudio/Containers/Opus/NxOpusReader.cs b/src/VGAudio/Containers/Opus/NxOpusReader.cs index d560bc57..5c4c21a5 100644 --- a/src/VGAudio/Containers/Opus/NxOpusReader.cs +++ b/src/VGAudio/Containers/Opus/NxOpusReader.cs @@ -35,6 +35,9 @@ protected override NxOpusStructure ReadFile(Stream stream, bool readAudioData = stream.Position = startPos + structure.SadfDataOffset; ReadStandardHeader(GetBinaryReader(stream, Endianness.LittleEndian), structure); break; + case NxOpusHeaderType.Ktss: + ReadKtssHeader(GetBinaryReader(stream, Endianness.LittleEndian), structure); + break; } BinaryReader reader = GetBinaryReader(stream, Endianness.BigEndian); @@ -70,6 +73,7 @@ private static NxOpusHeaderType DetectHeader(Stream stream) case 0x80000001: return NxOpusHeaderType.Standard; case 0x5355504F: return NxOpusHeaderType.Namco; // OPUS case 0x66646173: return NxOpusHeaderType.Sadf; // sadf + case 0x5353544B: return NxOpusHeaderType.Ktss; // KTSS default: throw new NotImplementedException("This Opus header is not supported"); } } @@ -134,6 +138,41 @@ private static void ReadSadfHeader(BinaryReader reader, NxOpusStructure structur structure.LoopEnd = reader.ReadInt32(); } + private static void ReadKtssHeader(BinaryReader reader, NxOpusStructure structure) + { + if (reader.ReadUInt32() != 0x5353544B) throw new InvalidDataException(); + reader.ReadInt32(); // fileSize + reader.BaseStream.Position += 0x18; // padding + reader.ReadUInt16(); // Codec ID + reader.ReadUInt16(); // Unknown + reader.ReadUInt32(); // Subsection start offset + reader.ReadByte(); // Layer count + structure.ChannelCount = reader.ReadByte(); + reader.ReadUInt16(); // Unknown + structure.SampleRate = reader.ReadInt32(); + structure.SampleCount = reader.ReadInt32(); + + structure.LoopStart = reader.ReadInt32(); + int loopLength = reader.ReadInt32(); + structure.LoopEnd = structure.LoopStart + loopLength; + structure.Looping = loopLength != 0; + + reader.ReadInt32(); // Padding. Moaaar padding. Koei Tecmo loves padding. + structure.DataOffset = reader.ReadInt32(); // Audio section address + structure.DataSize = reader.ReadInt32(); // Audio section size + reader.ReadInt32(); // Unknown + reader.ReadInt32(); // Frame count + structure.FrameSize = reader.ReadInt16(); + reader.ReadInt16(); // Unknown. Always 0x3C0 + reader.ReadInt32(); // Original sample rate? + structure.PreSkip = reader.ReadUInt16(); // Pre-skip + reader.ReadByte(); // Stream count + reader.ReadByte(); // Coupled count + reader.ReadBytes(structure.ChannelCount); // Channel mapping + + reader.BaseStream.Position = structure.DataOffset; + } + private static void ReadData(BinaryReader reader, NxOpusStructure structure) { long startPos = reader.BaseStream.Position; @@ -165,6 +204,7 @@ public enum NxOpusHeaderType { Standard, Namco, - Sadf + Sadf, + Ktss } } diff --git a/src/VGAudio/Containers/Opus/NxOpusWriter.cs b/src/VGAudio/Containers/Opus/NxOpusWriter.cs index 95442de2..425c5ca3 100644 --- a/src/VGAudio/Containers/Opus/NxOpusWriter.cs +++ b/src/VGAudio/Containers/Opus/NxOpusWriter.cs @@ -22,6 +22,8 @@ protected override int FileSize return StandardFileSize; case NxOpusHeaderType.Namco: return NamcoHeaderSize + StandardFileSize; + case NxOpusHeaderType.Ktss: + return KtssHeaderSize; default: return 0; } @@ -30,6 +32,7 @@ protected override int FileSize private const int StandardHeaderSize = 0x28; private const int NamcoHeaderSize = 0x40; + private const int KtssHeaderSize = 0x70; private int StandardFileSize => StandardHeaderSize + DataSize; private int DataSize { get; set; } @@ -39,6 +42,7 @@ protected override void SetupWriter(AudioData audio) var encodingConfig = new OpusParameters { Bitrate = Configuration.Bitrate, + EncodeCbr = Configuration.EncodeCbr, Progress = Configuration.Progress }; @@ -61,6 +65,10 @@ protected override void WriteStream(Stream stream) WriteStandardHeader(GetBinaryWriter(stream, Endianness.LittleEndian)); WriteData(GetBinaryWriter(stream, Endianness.BigEndian)); break; + case NxOpusHeaderType.Ktss: + WriteKtssHeader(GetBinaryWriter(stream, Endianness.LittleEndian)); + WriteData(GetBinaryWriter(stream, Endianness.BigEndian)); + break; default: throw new NotImplementedException("Writing this Opus header is not supported"); } @@ -72,7 +80,21 @@ private void WriteStandardHeader(BinaryWriter writer) writer.Write(0x18); writer.Write((byte)0); writer.Write((byte)Format.ChannelCount); - writer.Write((short)0); + // If frame length is inconsistent, frameLength = 0 + int frameLength = 0; + if (Format.Frames.Count > 0) + { + frameLength = Format.Frames[0].Length; + foreach (OpusFrame frame in Format.Frames) + { + if (frame.Length != frameLength) + { + frameLength = 0; + break; + } + } + } + writer.Write((short)(frameLength + 8)); writer.Write(Format.SampleRate); writer.Write(0x20); writer.Write(0); @@ -94,13 +116,50 @@ private void WriteNamcoHeader(BinaryWriter writer) writer.Write(Format.SampleRate); writer.Write(Format.LoopStart); writer.Write(Format.LoopEnd); - writer.Write(0); + writer.Write(Format.Looping ? 0x10 : 0); writer.Write(NamcoHeaderSize); writer.Write(StandardFileSize); writer.BaseStream.Position = startPos + NamcoHeaderSize; } + private void WriteKtssHeader(BinaryWriter writer) + { + long startPos = writer.BaseStream.Position; + + writer.WriteUTF8("KTSS"); + writer.Write(KtssHeaderSize + DataSize); + writer.Seek(0x20, SeekOrigin.Begin); + writer.Write((short)9); + writer.Write((byte)3); + writer.Write((byte)3); + writer.Write(0x50); + writer.Write((byte)1); + writer.Write((byte)Format.ChannelCount); + writer.Write((short)0); + writer.Write(Format.SampleRate); + writer.Write(Format.SampleCount); + writer.Write(Format.LoopStart); + writer.Write(Format.Looping ? Format.LoopEnd - Format.LoopStart : 0); + writer.Write(0); // Padding + writer.Write(0x70); + writer.Write(DataSize); + writer.Write(0); + writer.Write(Format.Frames.Count); + writer.Write((short)(DataSize / Format.Frames.Count)); // Frame size + writer.Write((short)0x3C0); // Some constant + writer.Write(Format.SampleRate); // "Original" sample rate + writer.Write((short)Format.PreSkipCount); + writer.Write((byte)1); + writer.Write((byte)1); + + // Channel mapping, Koei Tecmo doesn't seem to care about the order so we don't either + for (int i = 0; i < Format.ChannelCount; i++) + writer.Write((byte)i); + + writer.BaseStream.Position = startPos + KtssHeaderSize; + } + private void WriteData(BinaryWriter writer) { foreach (OpusFrame frame in Format.Frames) @@ -109,6 +168,10 @@ private void WriteData(BinaryWriter writer) writer.Write(frame.FinalRange); writer.Write(frame.Data); } + + // KTSS need to be 0x40 aligned + if (Configuration.HeaderType == NxOpusHeaderType.Ktss) + writer.Write(new byte[0x40 - writer.BaseStream.Position % 0x40]); } } } diff --git a/src/VGAudio/Containers/Wave/WaveWriter.cs b/src/VGAudio/Containers/Wave/WaveWriter.cs index f6a77329..52422265 100644 --- a/src/VGAudio/Containers/Wave/WaveWriter.cs +++ b/src/VGAudio/Containers/Wave/WaveWriter.cs @@ -58,9 +58,9 @@ protected override void WriteStream(Stream stream) stream.Position = 0; WriteRiffHeader(writer); WriteFmtChunk(writer); - WriteDataChunk(writer); if (Looping) WriteSmplChunk(writer); + WriteDataChunk(writer); } } diff --git a/src/VGAudio/Formats/CriHca/CriHcaFormat.cs b/src/VGAudio/Formats/CriHca/CriHcaFormat.cs index ddd13d4b..3d5c35fe 100644 --- a/src/VGAudio/Formats/CriHca/CriHcaFormat.cs +++ b/src/VGAudio/Formats/CriHca/CriHcaFormat.cs @@ -106,7 +106,7 @@ protected override CriHcaFormat AddInternal(CriHcaFormat format) public override CriHcaFormatBuilder GetCloneBuilder() { - throw new NotImplementedException(); + return new CriHcaFormatBuilder(AudioData, Hca.GetClone()); } } } diff --git a/src/VGAudio/Formats/CriHca/CriHcaFormatBuilder.cs b/src/VGAudio/Formats/CriHca/CriHcaFormatBuilder.cs index 9daeeb04..48a97187 100644 --- a/src/VGAudio/Formats/CriHca/CriHcaFormatBuilder.cs +++ b/src/VGAudio/Formats/CriHca/CriHcaFormatBuilder.cs @@ -1,4 +1,5 @@ -using VGAudio.Codecs.CriHca; +using System; +using VGAudio.Codecs.CriHca; namespace VGAudio.Formats.CriHca { @@ -23,6 +24,37 @@ public CriHcaFormatBuilder(byte[][] audioData, HcaInfo hca) } } + public override CriHcaFormatBuilder WithLoop(bool loop, int loopStart, int loopEnd) + { + base.WithLoop(loop, loopStart, loopEnd); + + if (loop && (loopStart != Hca.LoopStartSample || loopEnd != Hca.LoopEndSample)) + { + throw new NotSupportedException("Changing the loop points on HCA audio without re-encoding is not supported."); + } + + WithLoopImpl(loop); + return this; + } + + public override CriHcaFormatBuilder WithLoop(bool loop) + { + base.WithLoop(loop); + + WithLoopImpl(loop); + return this; + } + + private void WithLoopImpl(bool loop) + { + if (loop && !Hca.Looping) + { + throw new NotSupportedException("Adding a loop to HCA audio without re-encoding is not supported."); + } + + Hca.Looping = loop; + } + public override CriHcaFormat Build() => new CriHcaFormat(this); } } diff --git a/src/VGAudio/Formats/Opus/OpusFormat.cs b/src/VGAudio/Formats/Opus/OpusFormat.cs index f097069e..b3da8b60 100644 --- a/src/VGAudio/Formats/Opus/OpusFormat.cs +++ b/src/VGAudio/Formats/Opus/OpusFormat.cs @@ -3,6 +3,7 @@ using System.Linq; using Concentus.Enums; using Concentus.Structs; +using VGAudio.Codecs; using VGAudio.Codecs.Opus; using VGAudio.Formats.Pcm16; using VGAudio.Utilities; @@ -26,16 +27,18 @@ internal OpusFormat(OpusFormatBuilder b) : base(b) HasFinalRange = b.HasFinalRangeSet; } - public override Pcm16Format ToPcm16() + public override Pcm16Format ToPcm16() => ToPcm16(null); + public override Pcm16Format ToPcm16(CodecParameters config) => ToPcm16(new OpusParameters(config)); + public override Pcm16Format ToPcm16(OpusParameters config) { - short[][] audio = Decode(); + short[][] audio = Decode(config); return new Pcm16FormatBuilder(audio, SampleRate) .WithLoop(Looping, UnalignedLoopStart, UnalignedLoopEnd) .Build(); } - private short[][] Decode() + private short[][] Decode(OpusParameters config) { var dec = new OpusDecoder(SampleRate, ChannelCount); @@ -46,6 +49,8 @@ private short[][] Decode() int outPos = 0; int remaining = SampleCount + PreSkipCount; + config.Progress?.SetTotal(Frames.Count); + for (int i = 0; i < Frames.Count; i++) { int frameSamples = Math.Min(remaining, Frames[i].SampleCount); @@ -57,8 +62,12 @@ private short[][] Decode() outPos += frameSamples; remaining -= frameSamples; + + config.Progress?.ReportAdd(1); } + config.Progress?.SetTotal(0); + return pcmOut; } @@ -92,7 +101,9 @@ private static void CopyBuffer(short[][] bufferIn, int inputLength, short[][] bu public override OpusFormat EncodeFromPcm16(Pcm16Format pcm16, OpusParameters config) { const int frameSize = 960; + var encoder = new OpusEncoder(pcm16.SampleRate, pcm16.ChannelCount, OpusApplication.OPUS_APPLICATION_AUDIO); + encoder.UseVBR = !config.EncodeCbr; if (config.Bitrate > 0) encoder.Bitrate = config.Bitrate; @@ -107,6 +118,8 @@ public override OpusFormat EncodeFromPcm16(Pcm16Format pcm16, OpusParameters con short[] encodeInput = pcmData; + config.Progress?.SetTotal(pcm16.SampleCount.DivideByRoundUp(frameSize)); + while (remaining >= 0) { int encodeCount = Math.Min(frameSize, remaining); @@ -114,7 +127,7 @@ public override OpusFormat EncodeFromPcm16(Pcm16Format pcm16, OpusParameters con if (remaining < frameSize) { encodeInput = new short[frameSize * pcm16.ChannelCount]; - Array.Copy(pcmData, inPos, encodeInput, 0, encodeCount); + Array.Copy(pcmData, inPos, encodeInput, 0, encodeCount * pcm16.ChannelCount); inPos = 0; } @@ -134,12 +147,16 @@ public override OpusFormat EncodeFromPcm16(Pcm16Format pcm16, OpusParameters con remaining -= encodeCount; inPos += encodeCount * pcm16.ChannelCount; + + config.Progress?.ReportAdd(1); } OpusFormat format = new OpusFormatBuilder(pcm16.SampleCount, pcm16.SampleRate, pcm16.ChannelCount, encoder.Lookahead, frames) .WithLoop(pcm16.Looping, pcm16.LoopStart, pcm16.LoopEnd) .Build(); + config.Progress?.SetTotal(0); + return format; } diff --git a/src/VGAudio/Utilities/BitReader.cs b/src/VGAudio/Utilities/BitReader.cs index fd53ec3e..ae044f7f 100644 --- a/src/VGAudio/Utilities/BitReader.cs +++ b/src/VGAudio/Utilities/BitReader.cs @@ -122,7 +122,7 @@ private int PeekIntFallback(int bitCount) /// 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 + /// A 4-bit offset binary value with a negative bias can store /// the values 7 through -8 inclusive. public enum OffsetBias {