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
{