Skip to content

Instantly share code, notes, and snippets.

@SirusDoma
Last active January 6, 2023 03:25
Show Gist options
  • Select an option

  • Save SirusDoma/d27c746a25a6788fe2f7d598ef1034be to your computer and use it in GitHub Desktop.

Select an option

Save SirusDoma/d27c746a25a6788fe2f7d598ef1034be to your computer and use it in GitHub Desktop.
2DX MS-ADPCM Wave Encoder
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using NAudio.Utils;
using NAudio.Vorbis;
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using NVorbis;
namespace DxWavEncoder
{
// We need a custom MS-ADPCM format to encode the wav to .2dx file later
[StructLayout(LayoutKind.Sequential, Pack = 2)]
class DxAdpcmWaveFormat : AdpcmWaveFormat
{
public DxAdpcmWaveFormat(int sampleRate, int channels)
: base(sampleRate, channels)
{
// The parameters must be:
// blockAlign = 256
// samplePerBlock = 244
// averageBytesPerSecond = (blockAlign * sampleRate / samplesPerBlock + 0.5)
blockAlign = 256;
// We could define the wave format from the scratch so we can assign custom samplesPerBlock
// But Im too lazy for that, so I had to resort reflection to get this done
var samplesPerBlockProp = typeof(AdpcmWaveFormat).GetField("samplesPerBlock", BindingFlags.NonPublic | BindingFlags.Instance);
samplesPerBlockProp?.SetValue(this, (short)244);
averageBytesPerSecond = (int)(blockAlign * sampleRate / (double)SamplesPerBlock + 0.5D);
}
}
private class Program
{
static void Main(string[] args)
{
// The conversion happen in 3 steps:
// 1. Convert our source into a temporary PCM WAV, here I use 44.1khz 16bit @ 2 channels
// 2. If the file is preview file, apply preview effects (trim and fading) to our temporary PCM, otherwise skip this step
// 3. Convert our temporary PCM to MS-ADPCM (44.1khz 4bit @ 2channels)
//
// The `WaveFormatConversionStream` will likely to fail if we convert our source straight to MS-ADPCM
// Also, NAudio doesn't support writing samples smaller than 16bit (our MS-ADPCM use 4 bit), so we had to put our preview effects while in 16bit format
// Zero, define our target format and preview flag
var format = new DxAdpcmWaveFormat(44100, 2); // it's 4 bits per sample
bool preview = args.Length >= 2 && args[1] == "preview";
int previewStart = args.Length >= 3 ? int.Parse(args[2]) : 0; // in seconds format
// First, convert OGG to PCM WAV 44.1k 16bit @ 2 channels
using var source = new VorbisWaveReader("/some/path/to/music.ogg");
// Or use mp3, MediaFoundationReader is not supported in WinXP and subject to NAudio limitation, refer to their doc for more info
// using var source = new MediaFoundationReader("/some/path/to/music.mp3");
// Initializes memory stream to store our temporary PCM 16bit
using var ms = new MemoryStream();
// Use IgnoreDisposeStream, otherwise our memory stream will be disposed when disposing wav writer
var tmp = new IgnoreDisposeStream(ms); // No need `using`, disposing this thing won't dispose our MemoryStream
// Initialize our PCM 44.1khz 16bit @ 2 channels
using (var wav = new WaveFileWriter(tmp, new WaveFormat(44100, 16, 2)))
{
// We can use either IWaveProvider or ISampleProvider here doesn't matter
// Just be aware the values you feed during the conversion (Read() use byte[] buffer vs float[] samples)
IWaveProvider provider = source;
if (preview)
{
// Trim audio for preview purpose
provider = new OffsetSampleProvider(provider.ToSampleProvider())
{
SkipOver = TimeSpan.FromSeconds(previewStart), // not sure whether it need + 1 or not
Take = TimeSpan.FromSeconds(11) // idk but it always desiredSeconds + 1
}.ToWaveProvider();
// Add fade in effect in the beginning of audio for preview purpose
var fader = new FadeInOutSampleProvider(provider.ToSampleProvider());
fader.BeginFadeIn(1000);
provider = fader.ToWaveProvider();
}
// Convert our Source to 16bit Wave
provider = new SampleToWaveProvider16(provider.ToSampleProvider());
// You could probably use AverageBytesPerSecond * 2 here to store the whole second for 2 channels samples
// Just don't set it too much, otherwise our fade-out checking won't hit
byte[] buffer = new byte[provider.WaveFormat.AverageBytesPerSecond];
bool fadeOut = false;
for (int read; (read = provider.Read(buffer, 0, buffer.Length)) > 0;)
{
// Check whether it's preview or not, and if it is, we want to add fade out effect for the last second
if (preview && !fadeOut && wav.TotalTime.TotalMilliseconds >= 9000)
{
// Add fade out effect at the end of audio for preview purpose
var fader = new FadeInOutSampleProvider(provider.ToSampleProvider());
fader.BeginFadeOut(1000);
fadeOut = true;
// Sample it to 16 bit again to avoid distortion after adding fade out effect
provider = new SampleToWaveProvider16(fader);
}
// Write converted 16bit sample to the temporary memory stream
wav.Write(buffer, 0, read);
}
} // Ensure writer disposed so the data is completely flushed to our memory stream
// Finally, convert PCM Wav to ADPCM Wav
tmp.Seek(0, SeekOrigin.Begin); // reset offset so our WaveFileReader read our header correctly
using (var pcm = new WaveFileReader(tmp.SourceStream))
using (var adpcm = new WaveFormatConversionStream(format, pcm))
WaveFileWriter.CreateWaveFile("/some/path/to/output.wav", adpcm);
Console.WriteLine("Finish!");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment