using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using BizHawk.Common; namespace BizHawk.MultiClient { /// /// 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 /// BinaryWriter file; /// /// sequence of files to write to (split on 32 bit limit) /// IEnumerator filechain; /// /// samplerate in HZ /// int samplerate; /// /// number of audio channels /// int numchannels; /// /// number of bytes of PCM data written to current file /// UInt64 numbytes; /// /// number of bytes after which a file split should be made /// const UInt64 splitpoint = 2 * 1000 * 1000 * 1000; /// /// write riff headers to current file /// void writeheaders() { file.Write(Encoding.ASCII.GetBytes("RIFF")); // ChunkID file.Write((UInt32)0); // ChunkSize file.Write(Encoding.ASCII.GetBytes("WAVE")); // Format file.Write(Encoding.ASCII.GetBytes("fmt ")); // SubchunkID file.Write((UInt32)16); // SubchunkSize file.Write((UInt16)1); // AudioFormat (PCM) file.Write((UInt16)numchannels); // NumChannels file.Write((UInt32)samplerate); // SampleRate file.Write((UInt32)(samplerate * numchannels * 2)); // ByteRate file.Write((UInt16)(numchannels * 2)); // BlockAlign file.Write((UInt16)16); // BitsPerSample file.Write(Encoding.ASCII.GetBytes("data")); // SubchunkID file.Write((UInt32)0); // SubchunkSize } /// /// seek back to beginning of file and fix header sizes (if possible) /// void finalizeheaders() { if (numbytes + 36 >= 0x100000000) // passed 4G limit, nothing to be done return; try { file.Seek(4, SeekOrigin.Begin); file.Write((UInt32)(36 + numbytes)); file.Seek(40, SeekOrigin.Begin); file.Write((UInt32)(numbytes)); } catch (NotSupportedException) { // unseekable; oh well } } /// /// close current underlying stream /// void closecurrent () { if (file != null) { finalizeheaders(); file.Close(); file.Dispose(); } file = null; } /// /// open a new underlying stream /// /// 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 += (UInt64)(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 /// 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) { this.samplerate = samplerate; this.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 public WavWriter(IEnumerator ss, int samplerate, int numchannels) { this.samplerate = samplerate; this.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!) /// 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) { } class WavWriterVToken : IDisposable { public void Dispose() { } } public IDisposable AcquireVideoCodecToken(System.Windows.Forms.IWin32Window hwnd) { // don't care return new WavWriterVToken(); } 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() { if (wavwriter != null) wavwriter.Dispose(); } WavWriter wavwriter = null; int sampleRate = 0; int channels = 0; /// /// create a simple wav stream iterator /// /// /// 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 override string ToString() { return "WAV writer"; } public string WriterDescription() { return "Writes a series of standard RIFF wav files containing uncompressed audio. Does not write video. Splits every 2G."; } public string DesiredExtension() { return "wav"; } public void SetDefaultVideoCodecToken() { // don't use codec tokens, so don't care } public string ShortName() { return "wave"; } } }