diff --git a/BizHawk.MultiClient/BizHawk.MultiClient.csproj b/BizHawk.MultiClient/BizHawk.MultiClient.csproj index dd99f68b99..e9a8a41aa9 100644 --- a/BizHawk.MultiClient/BizHawk.MultiClient.csproj +++ b/BizHawk.MultiClient/BizHawk.MultiClient.csproj @@ -304,6 +304,7 @@ + GifAnimator.cs diff --git a/BizHawk.MultiClient/MainForm.cs b/BizHawk.MultiClient/MainForm.cs index 0e46b545d2..6730d01b06 100644 --- a/BizHawk.MultiClient/MainForm.cs +++ b/BizHawk.MultiClient/MainForm.cs @@ -2668,7 +2668,7 @@ namespace BizHawk.MultiClient sfd.FileName = "NULL"; sfd.InitialDirectory = PathManager.MakeAbsolutePath(Global.Config.AVIPath, ""); } - sfd.Filter = "AVI (*.avi)|*.avi|JMD (*.jmd)|*.jmd|All Files|*.*"; + sfd.Filter = "AVI (*.avi)|*.avi|JMD (*.jmd)|*.jmd|WAV (*.wav)|*.wav|All Files|*.*"; Global.Sound.StopSound(); var result = sfd.ShowDialog(); Global.Sound.StartSound(); @@ -2682,12 +2682,14 @@ namespace BizHawk.MultiClient IVideoWriter aw; string ext = Path.GetExtension(sfd.FileName).ToLower(); - if (ext == ".jmd") - aw = new JMDWriter(); - else if (ext == ".avi") - aw = new AviWriter(); - else // hmm? - aw = new AviWriter(); + if (ext == ".jmd") + aw = new JMDWriter(); + else if (ext == ".avi") + aw = new AviWriter(); + else if (ext == ".wav") + aw = new WavWriterV(); + else // hmm? + aw = new AviWriter(); try { aw.SetMovieParameters(fps, 0x01000000); diff --git a/BizHawk.MultiClient/WavWriter.cs b/BizHawk.MultiClient/WavWriter.cs new file mode 100644 index 0000000000..22ff286ec2 --- /dev/null +++ b/BizHawk.MultiClient/WavWriter.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +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(IntPtr 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); + } + } +}