Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skyrim NX sound support - MCADPCM and FUZ with OPUS #111

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/SplitProject/Split.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public override void Run(Context context)
"[assembly: InternalsVisibleTo(\"VGAudio.Benchmark\")]";

private static readonly string DefaultProjectFile =
@"<Project Sdk=""Microsoft.NET.Sdk""><PropertyGroup><TargetFrameworks>netstandard1.1;net45</TargetFrameworks></PropertyGroup></Project>";
@"<Project Sdk=""Microsoft.NET.Sdk""><PropertyGroup><TargetFrameworks>netstandard1.3;net45</TargetFrameworks></PropertyGroup></Project>";

private static readonly string[] OldProjects =
{
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<ItemGroup>
<LibraryFrameworks Condition="'$(DoNetFramework)' == 'true'" Include="net45"/>
<LibraryFrameworks Condition="'$(DoNetCore)' == 'true'" Include="netstandard1.1"/>
<LibraryFrameworks Condition="'$(DoNetCore)' == 'true'" Include="netstandard1.3"/>
<CliFrameworks Condition="'$(DoNetFramework)' == 'true'" Include="net451"/>
<CliFrameworks Condition="'$(DoNetCore)' == 'true'" Include="netcoreapp2.0"/>
<TestFrameworks Condition="'$(DoNetFramework)' == 'true'" Include="net46"/>
Expand Down
1 change: 1 addition & 0 deletions src/VGAudio.Cli/AudioFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal enum AudioFormat
Pcm16,
Pcm8,
GcAdpcm,
McAdpcm,
ImaAdpcm,
CriAdx,
CriAdxFixed,
Expand Down
5 changes: 4 additions & 1 deletion src/VGAudio.Cli/CliArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -583,4 +586,4 @@ private static string GetProgramVersion()
return $"{version.Major}.{version.Minor}.{version.Build}";
}
}
}
}
4 changes: 3 additions & 1 deletion src/VGAudio.Cli/ContainerTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using VGAudio.Containers.NintendoWare;
using VGAudio.Containers.Opus;
using VGAudio.Containers.Wave;
using VGAudio.Containers.McAdpcm;

namespace VGAudio.Cli
{
Expand All @@ -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),
Expand All @@ -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)
};

Expand Down
23 changes: 22 additions & 1 deletion src/VGAudio.Cli/CreateConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/VGAudio.Cli/Metadata/Containers/McAdpcm.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
2 changes: 2 additions & 0 deletions src/VGAudio.Cli/Metadata/Print.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(),
Expand Down
4 changes: 3 additions & 1 deletion src/VGAudio.Cli/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ internal enum FileType
Genh,
Atrac9,
NxOpus,
OggOpus
OggOpus,
McAdpcm,
Fuz
}
}
54 changes: 54 additions & 0 deletions src/VGAudio/Containers/McAdpcm/McAdpcmConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using static VGAudio.Codecs.GcAdpcm.GcAdpcmMath;

namespace VGAudio.Containers.McAdpcm
{
/// <summary>
/// Contains the options used to build the McAdpcm file.
/// </summary>
public class McAdpcmConfiguration : Configuration
{
private int _samplesPerInterleave = 0x3800;
/// <summary>
/// If <c>true</c>, recalculates the loop context when building the McAdpcm.
/// If <c>false</c>, reuses the loop context read from an imported McAdpcm
/// if available.
/// Default is <c>true</c>.
/// </summary>
public bool RecalculateLoopContext { get; set; } = true;

/// <summary>
/// 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).
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown if value is negative
/// or not divisible by 14.</exception>
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;
}
}

/// <summary>
/// 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.
/// </summary>
public int LoopPointAlignment { get; set; } = 1;
}
}
132 changes: 132 additions & 0 deletions src/VGAudio/Containers/McAdpcm/McAdpcmReader.cs
Original file line number Diff line number Diff line change
@@ -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<McAdpcmReader, McAdpcmStructure, McAdpcmConfiguration>
{
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);
}
}
}
}
Loading