diff --git a/build/SplitProject/Split.cs b/build/SplitProject/Split.cs index 21a525a2..4f5ba80c 100644 --- a/build/SplitProject/Split.cs +++ b/build/SplitProject/Split.cs @@ -52,7 +52,7 @@ public override void Run(Context context) "[assembly: InternalsVisibleTo(\"VGAudio.Benchmark\")]"; private static readonly string DefaultProjectFile = - @"netstandard1.1;net45"; + @"netstandard1.3;net45"; private static readonly string[] OldProjects = { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 82cac9a6..8134c9fe 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -17,7 +17,7 @@ - + diff --git a/src/VGAudio.Cli/AudioFormat.cs b/src/VGAudio.Cli/AudioFormat.cs index 6f2ab1ff..e811a26c 100644 --- a/src/VGAudio.Cli/AudioFormat.cs +++ b/src/VGAudio.Cli/AudioFormat.cs @@ -6,6 +6,7 @@ internal enum AudioFormat Pcm16, Pcm8, GcAdpcm, + McAdpcm, ImaAdpcm, CriAdx, CriAdxFixed, diff --git a/src/VGAudio.Cli/CliArguments.cs b/src/VGAudio.Cli/CliArguments.cs index 2d46b9fe..07dde24b 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 "SKYRIM": + nxHeaderType = NxOpusHeaderType.Skyrim; + break; default: Console.WriteLine("Invalid header type"); return null; @@ -583,4 +586,4 @@ private static string GetProgramVersion() return $"{version.Major}.{version.Minor}.{version.Build}"; } } -} +} \ No newline at end of file diff --git a/src/VGAudio.Cli/ContainerTypes.cs b/src/VGAudio.Cli/ContainerTypes.cs index 7a161202..89ecfefb 100644 --- a/src/VGAudio.Cli/ContainerTypes.cs +++ b/src/VGAudio.Cli/ContainerTypes.cs @@ -12,6 +12,7 @@ using VGAudio.Containers.NintendoWare; using VGAudio.Containers.Opus; using VGAudio.Containers.Wave; +using VGAudio.Containers.McAdpcm; namespace VGAudio.Cli { @@ -21,6 +22,7 @@ internal static class ContainerTypes { [FileType.Wave] = new ContainerType(new[] { "wav", "wave", "lwav" }, () => new WaveReader(), () => new WaveWriter(), CreateConfiguration.Wave), [FileType.Dsp] = new ContainerType(new[] { "dsp", "mdsp" }, () => new DspReader(), () => new DspWriter(), CreateConfiguration.Dsp), + [FileType.McAdpcm] = new ContainerType(new[] { "mcadpcm" }, () => new McAdpcmReader(), () => new McAdpcmWriter(), CreateConfiguration.McAdpcm), [FileType.Idsp] = new ContainerType(new[] { "idsp" }, () => new IdspReader(), () => new IdspWriter(), CreateConfiguration.Idsp), [FileType.Brstm] = new ContainerType(new[] { "brstm" }, () => new BrstmReader(), () => new BrstmWriter(), CreateConfiguration.Bxstm), [FileType.Bcstm] = new ContainerType(new[] { "bcstm" }, () => new BCFstmReader(), () => new BCFstmWriter(NwTarget.Ctr), CreateConfiguration.Bxstm), @@ -35,7 +37,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", "fuz" }, () => 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 3367c57c..1eddbdbc 100644 --- a/src/VGAudio.Cli/CreateConfiguration.cs +++ b/src/VGAudio.Cli/CreateConfiguration.cs @@ -4,6 +4,7 @@ using VGAudio.Containers; using VGAudio.Containers.Adx; using VGAudio.Containers.Dsp; +using VGAudio.Containers.McAdpcm; using VGAudio.Containers.Hca; using VGAudio.Containers.Hps; using VGAudio.Containers.Idsp; @@ -54,6 +55,26 @@ public static Configuration Dsp(Options options, Configuration inConfig = null) return config; } + public static Configuration McAdpcm(Options options, Configuration inConfig = null) + { + McAdpcmConfiguration config = inConfig as McAdpcmConfiguration ?? new McAdpcmConfiguration(); + + switch (options.OutFormat) + { + case AudioFormat.Pcm16: + throw new InvalidDataException("Can't use format PCM16 with DSP files"); + case AudioFormat.Pcm8: + throw new InvalidDataException("Can't use format PCM8 with DSP files"); + } + + if (options.LoopAlignment > 0) + { + config.LoopPointAlignment = options.LoopAlignment; + } + + return config; + } + public static Configuration Idsp(Options options, Configuration inConfig = null) { IdspConfiguration config = inConfig as IdspConfiguration ?? new IdspConfiguration(); @@ -156,7 +177,7 @@ public static Configuration NxOpus(Options options, Configuration inConfig = nul config.HeaderType = options.NxOpusHeaderType; config.EncodeCbr = options.EncodeCbr; if (options.Bitrate != 0) config.Bitrate = options.Bitrate; - + return config; } } diff --git a/src/VGAudio.Cli/Metadata/Containers/McAdpcm.cs b/src/VGAudio.Cli/Metadata/Containers/McAdpcm.cs new file mode 100644 index 00000000..4f57c0d1 --- /dev/null +++ b/src/VGAudio.Cli/Metadata/Containers/McAdpcm.cs @@ -0,0 +1,41 @@ +using System.IO; +using System.Text; +using VGAudio.Containers.McAdpcm; + +namespace VGAudio.Cli.Metadata.Containers +{ + internal class McAdpcm : MetadataReader + { + public override Common ToCommon(object structure) + { + if (!(structure is McAdpcmStructure mcAdpcm)) throw new InvalidDataException("Could not parse file metadata."); + + return new Common + { + SampleCount = mcAdpcm.SampleCount, + SampleRate = mcAdpcm.SampleRate, + ChannelCount = mcAdpcm.ChannelCount, + Format = AudioFormat.McAdpcm, + Looping = mcAdpcm.Looping, + LoopStart = mcAdpcm.LoopStart, + LoopEnd = mcAdpcm.LoopEnd + }; + } + + public override object ReadMetadata(Stream stream) => new McAdpcmReader().ReadMetadata(stream); + + public override void PrintSpecificMetadata(object structure, StringBuilder builder) + { + if (!(structure is McAdpcmStructure mcAdpcm)) throw new InvalidDataException("Could not parse file metadata."); + + builder.AppendLine(); + + builder.AppendLine($"Nibble Count: {mcAdpcm.NibbleCount}"); + builder.AppendLine($"Start Address: 0x{mcAdpcm.StartAddress:X8}"); + builder.AppendLine($"End Address: 0x{mcAdpcm.EndAddress:X8}"); + builder.AppendLine($"Current Address: 0x{mcAdpcm.CurrentAddress:X8}"); + + GcAdpcm.PrintAdpcmMetadata(mcAdpcm.Channels, builder); + } + } +} diff --git a/src/VGAudio.Cli/Metadata/Print.cs b/src/VGAudio.Cli/Metadata/Print.cs index 5d3572c9..d34f5962 100644 --- a/src/VGAudio.Cli/Metadata/Print.cs +++ b/src/VGAudio.Cli/Metadata/Print.cs @@ -50,6 +50,7 @@ public static void PrintCommonMetadata(Common common, StringBuilder builder) [AudioFormat.Pcm16] = "16-bit PCM", [AudioFormat.Pcm8] = "8-bit PCM", [AudioFormat.GcAdpcm] = "GameCube \"DSP\" 4-bit ADPCM", + [AudioFormat.McAdpcm] = "Nintendo Switch Multi-Channel \"DSP\" 4-bit ADPCM", [AudioFormat.ImaAdpcm] = "IMA 4-bit ADPCM", [AudioFormat.CriAdx] = "CRI ADX 4-bit ADPCM", [AudioFormat.CriAdxFixed] = "CRI ADX 4-bit ADPCM with fixed coefficients", @@ -62,6 +63,7 @@ public static void PrintCommonMetadata(Common common, StringBuilder builder) { [FileType.Wave] = new Wave(), [FileType.Dsp] = new Dsp(), + [FileType.McAdpcm] = new McAdpcm(), [FileType.Idsp] = new Idsp(), [FileType.Brstm] = new Brstm(), [FileType.Bcstm] = new Bcstm(), diff --git a/src/VGAudio.Cli/Options.cs b/src/VGAudio.Cli/Options.cs index daa205de..204e6b88 100644 --- a/src/VGAudio.Cli/Options.cs +++ b/src/VGAudio.Cli/Options.cs @@ -93,6 +93,8 @@ internal enum FileType Genh, Atrac9, NxOpus, - OggOpus + OggOpus, + McAdpcm, + Fuz } } diff --git a/src/VGAudio/Containers/McAdpcm/McAdpcmConfiguration.cs b/src/VGAudio/Containers/McAdpcm/McAdpcmConfiguration.cs new file mode 100644 index 00000000..1dbf2936 --- /dev/null +++ b/src/VGAudio/Containers/McAdpcm/McAdpcmConfiguration.cs @@ -0,0 +1,54 @@ +using System; +using static VGAudio.Codecs.GcAdpcm.GcAdpcmMath; + +namespace VGAudio.Containers.McAdpcm +{ + /// + /// Contains the options used to build the McAdpcm file. + /// + public class McAdpcmConfiguration : Configuration + { + private int _samplesPerInterleave = 0x3800; + /// + /// If true, recalculates the loop context when building the McAdpcm. + /// If false, reuses the loop context read from an imported McAdpcm + /// if available. + /// Default is true. + /// + public bool RecalculateLoopContext { get; set; } = true; + + /// + /// The number of samples in each block when interleaving + /// the audio data in the audio file. + /// Must be divisible by 14. + /// Default is 14,336 (0x3800). + /// + /// Thrown if value is negative + /// or not divisible by 14. + public int SamplesPerInterleave + { + get => _samplesPerInterleave; + set + { + if (value < 1) + { + throw new ArgumentOutOfRangeException(nameof(value), value, + "Number of samples per interleave must be positive"); + } + if (value % SamplesPerFrame != 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, + "Number of samples per interleave must be divisible by 14"); + } + _samplesPerInterleave = value; + } + } + + /// + /// When building the McAdpcm file, the loop points and audio will + /// be adjusted so that the start loop point is a multiple of + /// this number. Default is 1. + /// + public int LoopPointAlignment { get; set; } = 1; + } +} \ No newline at end of file diff --git a/src/VGAudio/Containers/McAdpcm/McAdpcmReader.cs b/src/VGAudio/Containers/McAdpcm/McAdpcmReader.cs new file mode 100644 index 00000000..d356830e --- /dev/null +++ b/src/VGAudio/Containers/McAdpcm/McAdpcmReader.cs @@ -0,0 +1,132 @@ +using System.IO; +using System.Linq; +using VGAudio.Formats; +using VGAudio.Formats.GcAdpcm; +using VGAudio.Utilities; +using static VGAudio.Codecs.GcAdpcm.GcAdpcmMath; +using static VGAudio.Utilities.Helpers; + +namespace VGAudio.Containers.McAdpcm +{ + public class McAdpcmReader : AudioReader + { + private static int DspHeaderCoefOffset = 0x1C; + private static int DspHeaderSize => 0x60; + + protected override McAdpcmStructure ReadFile(Stream stream, bool readAudioData = true) + { + var structure = new McAdpcmStructure(); + + ReadHeader(GetBinaryReader(stream, Endianness.LittleEndian), structure); + + if (readAudioData) { + using (BinaryReader reader = GetBinaryReader(stream, Endianness.BigEndian)) + { + reader.BaseStream.Position = structure.McadpcmHeaderSize; + ReadData(reader, structure); + } + } + + return structure; + } + + protected override IAudioFormat ToAudioStream(McAdpcmStructure structure) + { + var channels = new GcAdpcmChannel[structure.ChannelCount]; + + for (int c = 0; c < structure.ChannelCount; c++) + { + var channelBuilder = new GcAdpcmChannelBuilder(structure.AudioData[c], structure.Channels[c].Coefs, structure.SampleCount) + { + Gain = structure.Channels[c].Gain, + StartContext = structure.Channels[c].Start + }; + + channelBuilder + .WithLoop(structure.Looping, structure.LoopStart, structure.LoopEnd) + .WithLoopContext(structure.LoopStart, structure.Channels[c].Loop.PredScale, + structure.Channels[c].Loop.Hist1, structure.Channels[c].Loop.Hist2); + + channels[c] = channelBuilder.Build(); + } + + return new GcAdpcmFormatBuilder(channels, structure.SampleRate) + .WithLoop(structure.Looping, structure.LoopStart, structure.LoopEnd) + .Build(); + } + + private static void ReadHeader(BinaryReader reader, McAdpcmStructure structure) + { + structure.ChannelCount = reader.ReadInt32(); + + if (structure.ChannelCount > 2) + { + throw new InvalidDataException($"McAdpcm cannot have more than 2 channels."); + } + + structure.McadpcmHeaderSize = reader.ReadInt32(); + structure.DspChannelDataSize = reader.ReadInt32(); // channel 0 data size + + // A Stereo MCADPCM has 2 additional 32 bit fields we don't need for computation + // - channel1 data offset from file start + // - channel1 data size (never found a case where <> than Channel0 data size) + reader.BaseStream.Position += (structure.ChannelCount - 1) * 0x02 * sizeof(int); + + structure.SampleCount = reader.ReadInt32(); + structure.NibbleCount = reader.ReadInt32(); + structure.SampleRate = reader.ReadInt32(); + structure.Looping = reader.ReadInt16() == 1; + structure.Format = reader.ReadInt16(); + structure.StartAddress = reader.ReadInt32(); + structure.EndAddress = reader.ReadInt32(); + structure.CurrentAddress = reader.ReadInt32(); + + structure.Channels.Add(new GcAdpcmChannelInfo + { + Coefs = Enumerable.Range(0, 16).Select(x => reader.ReadInt16()).ToArray(), + Gain = reader.ReadInt16(), + Start = new GcAdpcmContext(reader), + Loop = new GcAdpcmContext(reader) + }); + + if (structure.ChannelCount == 2) + { + reader.BaseStream.Position = structure.McadpcmHeaderSize + structure.DspChannelDataSize + DspHeaderCoefOffset; + + structure.Channels.Add(new GcAdpcmChannelInfo + { + Coefs = Enumerable.Range(0, 16).Select(x => reader.ReadInt16()).ToArray(), + Gain = reader.ReadInt16(), + Start = new GcAdpcmContext(reader), + Loop = new GcAdpcmContext(reader) + }); + } + + if (reader.BaseStream.Length < structure.McadpcmHeaderSize + structure.DspChannelDataSize * structure.ChannelCount) + { + throw new InvalidDataException($"File doesn't contain enough data for {structure.SampleCount} samples"); + } + + if (SampleCountToNibbleCount(structure.SampleCount) != structure.NibbleCount) + { + throw new InvalidDataException("Sample count and nibble count do not match"); + } + + if (structure.Format != 0) + { + throw new InvalidDataException($"File does not contain ADPCM audio. Specified format is {structure.Format}"); + } + } + + private static void ReadData(BinaryReader reader, McAdpcmStructure structure) + { + structure.AudioData = new byte[structure.ChannelCount][]; + + for (int i = 0; i < structure.ChannelCount; i++) + { + reader.BaseStream.Position = structure.McadpcmHeaderSize + DspHeaderSize + (structure.DspChannelDataSize) * i; + structure.AudioData[i] = reader.ReadBytes(structure.DspChannelDataSize - DspHeaderSize); + } + } + } +} \ No newline at end of file diff --git a/src/VGAudio/Containers/McAdpcm/McAdpcmStructure.cs b/src/VGAudio/Containers/McAdpcm/McAdpcmStructure.cs new file mode 100644 index 00000000..f6668ee6 --- /dev/null +++ b/src/VGAudio/Containers/McAdpcm/McAdpcmStructure.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using VGAudio.Codecs.GcAdpcm; +using VGAudio.Formats.GcAdpcm; + +namespace VGAudio.Containers.McAdpcm +{ + /// + /// Defines the structure of a McAdpcm file. + /// + public class McAdpcmStructure + { + internal McAdpcmStructure() { } + + /// + /// The McAdpcm header size can be 0x0C 0r 0X14 large. + /// + public int McadpcmHeaderSize { get; set; } + + /// + /// The McAdpcm DspAdpcm channel data size. + /// + public int DspChannelDataSize { get; set; } + + /// + /// The number of samples in the McAdpcm. + /// + public int SampleCount { get; set; } + /// + /// The number of ADPCM nibbles in the McAdpcm. + /// + public int NibbleCount { get; set; } + /// + /// The sample rate of the audio. + /// + public int SampleRate { get; set; } + /// + /// This flag is set if the McAdpcm loops. + /// + public bool Looping { get; set; } + /// + /// The format of + /// + public short Format { get; set; } + /// + /// The address, in nibbles, of the start + /// loop point. + /// + public int StartAddress { get; set; } + /// + /// The address, in nibbles, of the end + /// loop point. + /// + public int EndAddress { get; set; } + /// + /// The address, in nibbles, of the initial + /// playback position. + /// + public int CurrentAddress { get; set; } + /// + /// The number of channels in the McAdpcm file. + /// Only used in multi-channel McAdpcm files. + /// + public int ChannelCount { get; set; } + /// + /// The number of ADPCM frames in each + /// interleaved audio data block. + /// Only used in multi-channel McAdpcm files. + /// + public int FramesPerInterleave { get; set; } + /// + /// The ADPCM information for each channel. + /// + public IList Channels { get; } = new List(); + + /// + /// The start loop point in samples. + /// + public int LoopStart => GcAdpcmMath.NibbleToSample(StartAddress); + /// + /// The end loop point in samples. + /// + public int LoopEnd => GcAdpcmMath.NibbleToSample(EndAddress); + internal byte[][] AudioData { get; set; } + } +} diff --git a/src/VGAudio/Containers/McAdpcm/McAdpcmWriter.cs b/src/VGAudio/Containers/McAdpcm/McAdpcmWriter.cs new file mode 100644 index 00000000..dd9491ef --- /dev/null +++ b/src/VGAudio/Containers/McAdpcm/McAdpcmWriter.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using VGAudio.Codecs.GcAdpcm; +using VGAudio.Formats; +using VGAudio.Formats.GcAdpcm; +using VGAudio.Utilities; +using static VGAudio.Codecs.GcAdpcm.GcAdpcmMath; +using static VGAudio.Utilities.Helpers; + +namespace VGAudio.Containers.McAdpcm +{ + public class McAdpcmWriter : AudioWriter + { + private static int DspHeaderSize => 0x60; + + private GcAdpcmFormat Adpcm { get; set; } + + protected override int FileSize => (ChannelCount == 1 ? 0x0C : 0x014) + (DspHeaderSize + AudioDataSize) * ChannelCount; + + private int ChannelCount => Adpcm.ChannelCount; + + private int McAdpcmHeaderSize => (ChannelCount == 1 ? 0x0C : 0x14); + + private int SampleCount => (Configuration.TrimFile && Adpcm.Looping ? LoopEnd : Math.Max(Adpcm.SampleCount, LoopEnd)); + private short Format { get; } = 0; /* 0 for ADPCM */ + + private int AlignmentSamples => GetNextMultiple(Adpcm.LoopStart, Configuration.LoopPointAlignment) - Adpcm.LoopStart; + private int LoopStart => Adpcm.LoopStart + AlignmentSamples; + private int LoopEnd => Adpcm.LoopEnd + AlignmentSamples; + + private int StartAddr => SampleToNibble(Adpcm.Looping ? LoopStart : 0); + private int EndAddr => SampleToNibble(Adpcm.Looping ? LoopEnd : SampleCount - 1); + private static int CurAddr => SampleToNibble(0); + + protected override void SetupWriter(AudioData audio) + { + Adpcm = audio.GetFormat(new GcAdpcmParameters { Progress = Configuration.Progress }); + } + + protected override void WriteStream(Stream stream) + { + WriteMcAdpcmHeader(GetBinaryWriter(stream, Endianness.LittleEndian)); + for (int i = 0; i < ChannelCount; i++) + { + WriteDspMcAdpcmHeader(GetBinaryWriter(stream, Endianness.LittleEndian), i); + WriteData(GetBinaryWriter(stream, Endianness.BigEndian), i); + } + } + + private void WriteMcAdpcmHeader(BinaryWriter writer) + { + writer.BaseStream.Position = 0; + + writer.Write(ChannelCount); // channel count + writer.Write(McAdpcmHeaderSize); // header size + writer.Write(DspHeaderSize + AudioDataSize); // channel 0 data size + if (ChannelCount == 2) + { + writer.Write(McAdpcmHeaderSize + DspHeaderSize + AudioDataSize); // channel 1 offset + writer.Write(DspHeaderSize + AudioDataSize); // channel 1 data size + } + } + + private void WriteDspMcAdpcmHeader(BinaryWriter writer, int i) + { + writer.BaseStream.Position = McAdpcmHeaderSize + (DspHeaderSize + AudioDataSize) * i; + + GcAdpcmChannel channel = Adpcm.Channels[i]; + writer.Write(SampleCount); + writer.Write(SampleCountToNibbleCount(SampleCount)); + writer.Write(Adpcm.SampleRate); + writer.Write((short)(Adpcm.Looping ? 1 : 0)); + writer.Write(Format); + writer.Write(StartAddr); + writer.Write(EndAddr); + writer.Write(CurAddr); + writer.Write(channel.Coefs.ToByteArray()); + writer.Write(channel.Gain); + channel.StartContext.Write(writer); + if (!Adpcm.Looping) + { + channel.LoopContext.Write(writer); + } + else + { + writer.Write(new byte[3 * sizeof(short)]); + } + writer.Write((short)(0)); + writer.Write((short)(0)); + } + + private void WriteData(BinaryWriter writer, int i) + { + writer.BaseStream.Position = McAdpcmHeaderSize + DspHeaderSize * (i+1) + AudioDataSize * i; + writer.Write(Adpcm.Channels[i].GetAdpcmAudio(), 0, SampleCountToByteCount(SampleCount)); + } + + /// + /// Size of a single channel's ADPCM audio data with padding when written to a file + /// + private int AudioDataSize => SampleCountToByteCount(SampleCount); + } +} \ No newline at end of file diff --git a/src/VGAudio/Containers/Opus/NxOpusReader.cs b/src/VGAudio/Containers/Opus/NxOpusReader.cs index d560bc57..93a8ba02 100644 --- a/src/VGAudio/Containers/Opus/NxOpusReader.cs +++ b/src/VGAudio/Containers/Opus/NxOpusReader.cs @@ -35,6 +35,12 @@ protected override NxOpusStructure ReadFile(Stream stream, bool readAudioData = stream.Position = startPos + structure.SadfDataOffset; ReadStandardHeader(GetBinaryReader(stream, Endianness.LittleEndian), structure); break; + case NxOpusHeaderType.Skyrim: + ReadSkyrimHeader(GetBinaryReader(stream, Endianness.LittleEndian), structure); + + // stream.Position = startPos + structure.SkyrimHeaderSize; + ReadStandardHeader(GetBinaryReader(stream, Endianness.LittleEndian), structure); + break; } BinaryReader reader = GetBinaryReader(stream, Endianness.BigEndian); @@ -70,6 +76,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 0x455A5546: return NxOpusHeaderType.Skyrim; default: throw new NotImplementedException("This Opus header is not supported"); } } @@ -95,6 +102,18 @@ private static void ReadStandardHeader(BinaryReader reader, NxOpusStructure stru structure.DataSize = reader.ReadInt32(); } + private static void ReadSkyrimHeader(BinaryReader reader, NxOpusStructure structure) + { + if (reader.ReadUInt32() != 0x455A5546) throw new InvalidDataException(); + reader.BaseStream.Position += 8; + uint audioOffset = reader.ReadUInt32(); + reader.BaseStream.Position = audioOffset + 0x04; + structure.SkyrimSoundDurationMs = reader.ReadInt32(); + structure.ChannelCount = reader.ReadInt32(); + structure.SkyrimHeaderSize = reader.ReadInt32(); // Skyrim NX OPUS Header Size == 0x14 + structure.SkyrimOpusDataSize = reader.ReadInt32(); + } + private static void ReadNamcoHeader(BinaryReader reader, NxOpusStructure structure) { if (reader.ReadUInt32() != 0x4F505553) throw new InvalidDataException(); @@ -165,6 +184,7 @@ public enum NxOpusHeaderType { Standard, Namco, - Sadf + Sadf, + Skyrim } } diff --git a/src/VGAudio/Containers/Opus/NxOpusStructure.cs b/src/VGAudio/Containers/Opus/NxOpusStructure.cs index dd0eee84..bb2bcbf7 100644 --- a/src/VGAudio/Containers/Opus/NxOpusStructure.cs +++ b/src/VGAudio/Containers/Opus/NxOpusStructure.cs @@ -30,6 +30,10 @@ public class NxOpusStructure public int NamcoCoreDataLength { get; set; } public int SadfDataOffset { get; set; } + public int SkyrimSoundDurationMs { get; set; } + public int SkyrimHeaderSize { get; set; } + public int SkyrimOpusDataSize { get; set; } + public List Frames { get; set; } = new List(); } } diff --git a/src/VGAudio/Containers/Opus/NxOpusWriter.cs b/src/VGAudio/Containers/Opus/NxOpusWriter.cs index c840358d..ebf4e68d 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.Skyrim: + return SkyrimHeaderSize + StandardFileSize; default: return 0; } @@ -30,6 +32,7 @@ protected override int FileSize private const int StandardHeaderSize = 0x28; private const int NamcoHeaderSize = 0x40; + private const int SkyrimHeaderSize = 0x14; private int StandardFileSize => StandardHeaderSize + DataSize; private int DataSize { get; set; } @@ -62,6 +65,23 @@ protected override void WriteStream(Stream stream) WriteStandardHeader(GetBinaryWriter(stream, Endianness.LittleEndian)); WriteData(GetBinaryWriter(stream, Endianness.BigEndian)); break; + case NxOpusHeaderType.Skyrim: + using (BinaryWriter writer = GetBinaryWriter(stream, Endianness.LittleEndian)) + { + WriteFuzLipHeader(writer); + WriteSkyrimHeader(writer); + WriteStandardHeader(writer); + } + using (BinaryWriter writer = GetBinaryWriter(stream, Endianness.BigEndian)) + { + WriteData(writer); + int fuzPadding = (int)(stream.Position % 4); + if (fuzPadding != 0) + { + while (fuzPadding++ < 4) writer.Write((byte)0x00); + } + } + break; default: throw new NotImplementedException("Writing this Opus header is not supported"); } @@ -98,6 +118,53 @@ private void WriteStandardHeader(BinaryWriter writer) writer.Write(DataSize); } + private void WriteSkyrimHeader(BinaryWriter writer) + { + long startPos = writer.BaseStream.Position; + + writer.Write(0xFFD58D0A); + int durationMs = (int)(Format.SampleCount * 1000 / (float)(Format.SampleRate)); + writer.Write(durationMs); + writer.Write(Format.ChannelCount); + writer.Write(SkyrimHeaderSize); + writer.Write(StandardHeaderSize + DataSize); // OPUS payload size + writer.BaseStream.Position = startPos + SkyrimHeaderSize; + } + + private void WriteFuzLipHeader(BinaryWriter writer) + { + const int fuzHeaderSize = 0x10; + + string fileName = (writer.BaseStream as FileStream).Name; + string lipFileName = fileName.Substring(0, fileName.Length - 3) + "lip"; + int lipSize = 0; + int lipPadding = 0; + byte[] lipData = { }; + + if (File.Exists(lipFileName)) + { + lipData = File.ReadAllBytes(lipFileName); + lipSize = lipData.Length; + lipPadding = lipSize % 4; + if (lipPadding != 0) + { + lipPadding = 4 - lipPadding; + } + } + + writer.Write(0x455A5546); // string FUZE + writer.Write(0x01); // FUZE version = 0x00000001 + writer.Write(lipSize); // lip size + writer.Write(fuzHeaderSize + lipSize + lipPadding); // offset to audio data + + // add the LIP payload + if (lipSize > 0) + { + writer.Write(lipData); + while (lipPadding-- > 0) writer.Write((byte)0x00); + } + } + private void WriteNamcoHeader(BinaryWriter writer) { long startPos = writer.BaseStream.Position; diff --git a/src/VGAudio/VGAudio.csproj b/src/VGAudio/VGAudio.csproj index e3923753..6743d997 100644 --- a/src/VGAudio/VGAudio.csproj +++ b/src/VGAudio/VGAudio.csproj @@ -1,7 +1,7 @@  - netstandard1.1;net45 + netstandard1.3;net45 VGAudio Alex Barney @@ -16,8 +16,10 @@ true - + + + \ No newline at end of file