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