using System; using System.Collections.Generic; using System.Text; using System.IO; using BizHawk.Common.IOExtensions; using BizHawk.Emulation.Common; namespace BizHawk.Client.EmuHawk { /// /// writes MS standard riff files containing uncompressed PCM wav data /// supports 16 bit signed data only /// public class WavWriter : IDisposable { /// /// underlying file being written to /// private BinaryWriter _file; /// /// sequence of files to write to (split on 32 bit limit) /// private IEnumerator _fileChain; /// /// samplerate in HZ /// private int _sampleRate; /// /// number of audio channels /// private int _numChannels; /// /// number of bytes of PCM data written to current file /// private ulong _numBytes; /// /// number of bytes after which a file split should be made /// private const ulong SplitPoint = 2 * 1000 * 1000 * 1000; /// /// write riff headers to current file /// private void WriteHeaders() { _file.Write(Encoding.ASCII.GetBytes("RIFF")); // ChunkID _file.Write(0U); // ChunkSize _file.Write(Encoding.ASCII.GetBytes("WAVE")); // Format _file.Write(Encoding.ASCII.GetBytes("fmt ")); // SubchunkID _file.Write(16U); // SubchunkSize _file.Write((ushort)1U); // AudioFormat (PCM) _file.Write((ushort)_numChannels); // NumChannels _file.Write((uint)_sampleRate); // SampleRate _file.Write((uint)(_sampleRate * _numChannels * 2)); // ByteRate _file.Write((ushort)(_numChannels * 2)); // BlockAlign _file.Write((ushort)16U); // BitsPerSample _file.Write(Encoding.ASCII.GetBytes("data")); // SubchunkID _file.Write(0U); // SubchunkSize } /// /// seek back to beginning of file and fix header sizes (if possible) /// private void FinalizeHeaders() { if (_numBytes + 36 >= 0x100000000) { // passed 4G limit, nothing to be done return; } try { _file.Seek(4, SeekOrigin.Begin); _file.Write((uint)(36 + _numBytes)); _file.Seek(40, SeekOrigin.Begin); _file.Write((uint)(_numBytes)); } catch (NotSupportedException) { // unseekable; oh well } } /// /// close current underlying stream /// private void CloseCurrent() { if (_file != null) { FinalizeHeaders(); _file.Close(); _file.Dispose(); } _file = null; } /// /// open a new underlying stream /// private void OpenCurrent(Stream next) { _file = new BinaryWriter(next, Encoding.ASCII); _numBytes = 0; WriteHeaders(); } /// /// write samples to file /// /// samples to write; should contain one for each channel public void WriteSamples(short[] samples) { _file.Write(samples); _numBytes += (ulong)(samples.Length * sizeof(short)); // try splitting if we can if (_numBytes >= SplitPoint && _fileChain != null) { if (!_fileChain.MoveNext()) { // out of files, just keep on writing to this one _fileChain = null; } else { Stream next = _fileChain.Current; CloseCurrent(); OpenCurrent(next); } } } public void Dispose() { Close(); } /// /// finishes writing /// public void Close() { CloseCurrent(); } /// /// checks sampling rate, number of channels for validity /// private void CheckArgs() { if (_sampleRate < 1 || _numChannels < 1) { throw new ArgumentException("Bad samplerate/numchannels"); } } /// /// initializes WavWriter with a single output stream /// no attempt is made to split /// /// WavWriter now owns this stream /// sampling rate in HZ /// number of audio channels public WavWriter(Stream s, int sampleRate, int numChannels) { _sampleRate = sampleRate; _numChannels = numChannels; _fileChain = null; CheckArgs(); OpenCurrent(s); } /// /// initializes WavWriter with an enumeration of Streams /// one is consumed every time 2G is hit /// if the enumerator runs out before the audio stream does, the last file could be >2G /// /// WavWriter now owns any of these streams that it enumerates /// sampling rate in HZ /// number of audio channels /// cannot be progressed public WavWriter(IEnumerator ss, int sampleRate, int numChannels) { _sampleRate = sampleRate; _numChannels = numChannels; CheckArgs(); _fileChain = ss; // advance to first if (!_fileChain.MoveNext()) { throw new ArgumentException("Iterator was empty!"); } OpenCurrent(ss.Current); } } /// /// slim wrapper on WavWriter that implements IVideoWriter (discards all video!) /// [VideoWriter("wave", "WAV writer", "Writes a series of standard RIFF wav files containing uncompressed audio. Does not write video. Splits every 2G.")] public class WavWriterV : IVideoWriter { public void SetVideoCodecToken(IDisposable token) { } public void AddFrame(IVideoProvider source) { } public void SetMovieParameters(int fpsNum, int fpsDen) { } public void SetVideoParameters(int width, int height) { } public void SetFrame(int frame) { } public bool UsesAudio => true; public bool UsesVideo => false; private class WavWriterVToken : IDisposable { public void Dispose() { } } public IDisposable AcquireVideoCodecToken(System.Windows.Forms.IWin32Window hwnd) { // don't care return new WavWriterVToken(); } /// is not 16 public void SetAudioParameters(int sampleRate, int channels, int bits) { this._sampleRate = sampleRate; this._channels = channels; if (bits != 16) { throw new ArgumentException("Only support 16bit audio!"); } } public void SetMetaData(string gameName, string authors, ulong lengthMs, ulong rerecords) { // not implemented } public void Dispose() { _wavWriter?.Dispose(); } private WavWriter _wavWriter; private int _sampleRate; private int _channels; /// /// create a simple wav stream iterator /// private static IEnumerator CreateStreamIterator(string template) { string dir = Path.GetDirectoryName(template) ?? ""; string baseName = Path.GetFileNameWithoutExtension(template) ?? ""; string ext = Path.GetExtension(template); yield return new FileStream(template, FileMode.Create); int counter = 1; while (true) { yield return new FileStream($"{Path.Combine(dir, baseName)}_{counter}{ext}", FileMode.Create); counter++; } } public void OpenFile(string baseName) { _wavWriter = new WavWriter(CreateStreamIterator(baseName), _sampleRate, _channels); } public void CloseFile() { _wavWriter.Close(); _wavWriter.Dispose(); _wavWriter = null; } public void AddSamples(short[] samples) { _wavWriter.WriteSamples(samples); } public string DesiredExtension() => "wav"; public void SetDefaultVideoCodecToken() { // don't use codec tokens, so don't care } } }