Author Topic: [C#] ADPCM > PCM + playing  (Read 394 times)

Maki

  • Software hacker
  • Freak
  • *
  • Posts: 534
  • Karma: 101
  • Paranoid
    • View Profile
[C#] ADPCM > PCM + playing
« on: 2018-11-24 18:11:46 »
so I've been struggling for a day about playing audio.dat sound effects internally/ converting them without using 3rd party tools like Audacity or ffmpeg- tried asking some VIPs here but they didn't answer so I had to came up with it myself:

So, FF7 and FF8 use audio.dat + audio.fmt. It's called an Microsoft audio chunk system and was used in XAudio2. You can find the documentation here:
https://docs.microsoft.com/en-us/windows/desktop/xaudio2/adpcm-overview

There are several methods to start. First one is to do what Qhimm originally used thanks to publishing source code of FF8Audio. Before we do anything let's first read the FMT content. Sample code is:

Code: [Select]
private struct SoundEntry
        {
            public int Size;
            public int Offset;
            public byte[] UNK; //12
            public byte[] WAVFORMATEX; //18
            public ushort SamplesPerBlock;
            public ushort ADPCM;
            public byte[] ADPCMCoefSets; //28
        }

        private struct WAVEFORMATEX
            {
            public ushort wFormatTag;
            public ushort nChannels;
            public uint nSamplesPerSec;
            public uint nAvgBytesPerSec;
            public ushort nBlockAlign;
            public ushort wBitsPerSample;
            public ushort cbSize;
        }

        private static SoundEntry[] soundEntries;
        public static int soundEntriesCount;

internal static void DEBUG_SoundAudio()
        {
            string path = Path.Combine(Memory.FF8DIR, "..\\Sound\\audio.fmt");
            using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            using (BinaryReader br = new BinaryReader(fs))
            {
                soundEntries = new SoundEntry[br.ReadUInt32()];
                fs.Seek(36, SeekOrigin.Current);
                for (int i = 0; i < soundEntries.Length-1; i++)
                {
                    int sz = br.ReadInt32();
                    if(sz == 0) {
                        fs.Seek(34, SeekOrigin.Current); continue; }

                    soundEntries[i] = new SoundEntry()
                    {
                        Size = sz,
                        Offset = br.ReadInt32(),
                        UNK = br.ReadBytes(12),
                        WAVFORMATEX = br.ReadBytes(18),
                        SamplesPerBlock = br.ReadUInt16(),
                        ADPCM = br.ReadUInt16(),
                        ADPCMCoefSets = br.ReadBytes(28)
                    };
                }
            }
            soundEntriesCount = soundEntries.Length;
        }


After grabbing the content of FMT into the array of struct we can work it here on many different ways:


1. Qhimm WAVE header building method: (if you want only to repair the header for 3rd party player)

ADPCM 4 bit is normally headerless and everything is saved into FMT, however that's still not enough for normal player to be able to play it. We need to build the WAVE/RIFF header:
Code: [Select]
                fs.Seek(soundEntries[soundID].Offset, SeekOrigin.Begin); //seek to raw buffer location in audio.dat thanks to audio.fmt pointer
                List<byte[]> sfxBufferList = new List<byte[]>(); //this will be used as an dynamic array, I'm just too lazy
                sfxBufferList.Add(Encoding.ASCII.GetBytes("RIFF")); //let's start with magic RIFF
                sfxBufferList.Add(BitConverter.GetBytes
                    (soundEntries[soundID].Size + 36)); //now the size read from FMT + 36
                sfxBufferList.Add(Encoding.ASCII.GetBytes("WAVEfmt ")); //now the WAVEfmt (there's a space, so it takes eight bytes)
                sfxBufferList.Add(BitConverter.GetBytes
                    (18 + 0)); //eighteen
                sfxBufferList.Add(soundEntries[soundID].WAVFORMATEX); //now encode full WAVEFORMATEX struct packed
                sfxBufferList.Add(Encoding.ASCII.GetBytes("data")); //now add 'data' in ascii
                sfxBufferList.Add(BitConverter.GetBytes(soundEntries[soundID].Size)); //now put the size again from FMT
                byte[] rawBuffer = br.ReadBytes(soundEntries[soundID].Size); //obviously read audio.dat
                sfxBufferList.Add(rawBuffer); //and add it on the bottom
                byte[] sfxBuffer = sfxBufferList.SelectMany(x => x).ToArray(); //now cast every byte in byte list to array

sfxBuffer now contains correct ADPCM WAVE file you can save and play in your favourite software.

2. My ADPCM->PCM + play method using NAudio: (for actually converting the file + playing in new thread)
I tested many different libraries, I mean it- Bass, CSCore, libZplay and some I even forgot about. None works with ADPCM, however NAudio does it. Grab NAudio release from github page (google it). This time it's super easy-
Code: [Select]
using NAudio;
using NAudio.Wave;

internal static void PlaySound(int soundID)
        {
            if (soundEntries == null)
                return;
            if (soundEntries[soundID].Size == 0) return;
            using (FileStream fs = new FileStream(Path.Combine(Memory.FF8DIR, "..\\Sound\\audio.dat"), FileMode.Open, FileAccess.Read))
            using (BinaryReader br = new BinaryReader(fs))
            {
                fs.Seek(soundEntries[soundID].Offset, SeekOrigin.Begin);
                GCHandle gc = GCHandle.Alloc(soundEntries[soundID].WAVFORMATEX, GCHandleType.Pinned); //it's not recommended way in C#, but I'm again- lazy
                WAVEFORMATEX format =  (WAVEFORMATEX)Marshal.PtrToStructure(gc.AddrOfPinnedObject(), typeof(WAVEFORMATEX));
                gc.Free();
                byte[] rawBuffer = br.ReadBytes(soundEntries[soundID].Size);

                //passing WAVEFORMATEX struct params makes playing all sounds possible
                RawSourceWaveStream raw = new RawSourceWaveStream(new MemoryStream(rawBuffer), new AdpcmWaveFormat((int)format.nSamplesPerSec, format.nChannels ));
                var a = WaveFormatConversionStream.CreatePcmStream(raw);
                WaveOut waveout = new WaveOut(); //you have to use new instance for EVERY sound played
                waveout.Init(a); //as said in documentation- init is supposed to be called once.
                waveout.Play();
            }
        }

3. Forcing playing with PCM codec
Totally not recommended- you'll get harsh and all the noise. However it requires no codec or libraries. I'll be using default sound effect player of MonoGame framework.
First get the default raw buffer without header as-is in audio.dat. See the code above to get byte[] rawBuffer. Now:
Code: [Select]
SoundEffect se = new SoundEffect(sfxBuffer, 22050, AudioChannels.Mono);
se.Play(1.0f, 0.0f, 0.0f);
22050Hz + mono are the only parameters that makes playing ADPCM forced sound as natural as possible
« Last Edit: 2018-11-24 18:15:17 by Maki »