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);
+ }
+ }
+}