Last active
January 6, 2023 03:25
-
-
Save SirusDoma/d27c746a25a6788fe2f7d598ef1034be to your computer and use it in GitHub Desktop.
2DX MS-ADPCM Wave Encoder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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