diff --git a/BizHawk.MultiClient/AviWriter.cs b/BizHawk.MultiClient/AviWriter.cs
index 7f406bcf21..17a623d6be 100644
--- a/BizHawk.MultiClient/AviWriter.cs
+++ b/BizHawk.MultiClient/AviWriter.cs
@@ -350,6 +350,14 @@ namespace BizHawk.MultiClient
}
}
+ ///
+ /// set metadata parameters; should be called before opening file
+ /// NYI
+ ///
+ public void SetMetaData(string gameName, string authors, UInt64 lengthMS, UInt64 rerecords)
+ {
+ }
+
unsafe class AviWriterSegment : IDisposable
{
static AviWriterSegment()
diff --git a/BizHawk.MultiClient/BizHawk.MultiClient.csproj b/BizHawk.MultiClient/BizHawk.MultiClient.csproj
index 5f336bb03e..58c2bcbbbb 100644
--- a/BizHawk.MultiClient/BizHawk.MultiClient.csproj
+++ b/BizHawk.MultiClient/BizHawk.MultiClient.csproj
@@ -82,6 +82,7 @@
3.5
+
3.5
@@ -174,6 +175,7 @@
+
Form
diff --git a/BizHawk.MultiClient/IVideoWriter.cs b/BizHawk.MultiClient/IVideoWriter.cs
index ace087e2bd..c6317a77d3 100644
--- a/BizHawk.MultiClient/IVideoWriter.cs
+++ b/BizHawk.MultiClient/IVideoWriter.cs
@@ -33,6 +33,7 @@ namespace BizHawk
///
/// adds audio samples to the stream
/// no attempt is made to sync this to the video
+ /// reccomendation: try not to have the size or pacing of the audio chunks be too "weird"
///
void AddSamples(short[] samples);
@@ -63,5 +64,15 @@ namespace BizHawk
///
void SetAudioParameters(int sampleRate, int channels, int bits);
+ ///
+ /// set metadata parameters; should be called before opening file
+ /// ok to not set at all, if not applicable
+ ///
+ /// The name of the game loaded
+ /// Authors on movie file
+ /// Length of movie file in milliseconds
+ /// Number of rerecords on movie file
+ void SetMetaData(string gameName, string authors, UInt64 lengthMS, UInt64 rerecords);
+
}
}
diff --git a/BizHawk.MultiClient/JMDWriter.cs b/BizHawk.MultiClient/JMDWriter.cs
new file mode 100644
index 0000000000..51e6722636
--- /dev/null
+++ b/BizHawk.MultiClient/JMDWriter.cs
@@ -0,0 +1,658 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
+using ICSharpCode.SharpZipLib.Zip.Compression;
+
+namespace BizHawk.MultiClient
+{
+ class JMDWriter : IVideoWriter
+ {
+ ///
+ /// carries private compression information data
+ /// NYI
+ ///
+ class CodecToken : IDisposable
+ {
+ public void Dispose()
+ {
+ }
+
+ // get constants from Deflater
+ public int compressionlevel
+ {
+ get;
+ private set;
+ }
+
+ public int numthreads
+ {
+ get;
+ private set;
+ }
+
+ public CodecToken()
+ {
+ compressionlevel = Deflater.DEFAULT_COMPRESSION;
+ numthreads = 3;
+ }
+ }
+
+ CodecToken token;
+ ///
+ /// fps numerator, constant
+ ///
+ int fpsnum;
+ ///
+ /// fps denominator, constant
+ ///
+ int fpsden;
+
+ ///
+ /// audio samplerate, constant
+ ///
+ int audiosamplerate;
+ ///
+ /// audio number of channels, constant; 1 or 2 only
+ ///
+ int audiochannels;
+ ///
+ /// audio bits per sample, constant; only 16 supported
+ ///
+ int audiobits;
+
+ ///
+ /// actual disk file being written
+ ///
+ FileStream JMDfile;
+
+ ///
+ /// current timestamp offset in JMD
+ /// ie, (number of ffffffffff appearances) * (ffffffff)
+ ///
+ UInt64 timestampoff;
+ ///
+ /// total number of video frames, used to calculate timestamps
+ ///
+ UInt64 totalframes;
+
+ ///
+ /// total number of audio samples, used to calculate timestamps
+ ///
+ UInt64 totalsamples;
+
+ // movie metadata
+
+ string gamename;
+ string authors;
+ UInt64 lengthms;
+ UInt64 rerecords;
+
+
+
+ ///
+ /// sets default (probably wrong) parameters
+ ///
+ public JMDWriter()
+ {
+ fpsnum = 25;
+ fpsden = 1;
+ audiosamplerate = 22050;
+ audiochannels = 1;
+ audiobits = 8;
+ token = null;
+
+ gamename = "";
+ authors = "";
+ lengthms = 0;
+ rerecords = 0;
+ }
+
+ public void Dispose()
+ {
+ }
+
+
+ ///
+ /// sets the codec token to be used for video compression
+ ///
+ public void SetVideoCodecToken(IDisposable token)
+ {
+ if (token is CodecToken)
+ this.token = (CodecToken)token;
+ else
+ throw new ArgumentException("codec token must be of right type");
+ }
+
+ ///
+ /// obtain a set of recording compression parameters
+ ///
+ /// hwnd to attach to if the user is shown config dialog
+ /// codec token, dispose of it when you're done with it
+ public IDisposable AcquireVideoCodecToken(IntPtr hwnd)
+ {
+ // no user interaction for now
+ return new CodecToken();
+ }
+
+ ///
+ /// set framerate to fpsnum/fpsden (assumed to be unchanging over the life of the stream)
+ ///
+ public void SetMovieParameters(int fpsnum, int fpsden)
+ {
+ this.fpsnum = fpsnum;
+ this.fpsden = fpsden;
+ }
+
+ ///
+ /// set resolution parameters (width x height)
+ /// must be set before file is opened
+ /// can be changed in future
+ /// should always match IVideoProvider
+ ///
+ ///
+ ///
+ public void SetVideoParameters(int width, int height)
+ {
+ // each frame is dumped with its resolution, so we don't care to store this or monitor it
+ }
+
+ ///
+ /// set audio parameters. cannot change later
+ ///
+ public void SetAudioParameters(int sampleRate, int channels, int bits)
+ {
+ // these are pretty arbitrary
+ if (sampleRate < 8000 || sampleRate > 96000 || channels < 1 || channels > 2 || bits != 16)
+ throw new ArgumentException("Audio parameters out of range!");
+ audiosamplerate = sampleRate;
+ audiochannels = channels;
+ audiobits = bits;
+ }
+
+
+ ///
+ /// opens a recording stream
+ /// set a video codec token first.
+ ///
+ public void OpenFile(string baseName)
+ {
+ string ext = Path.GetExtension(baseName);
+ if (ext == null || ext.ToLower() != "jmd")
+ baseName = baseName + ".jmd";
+
+ JMDfile = File.Open(baseName, FileMode.OpenOrCreate);
+ timestampoff = 0;
+ totalframes = 0;
+ totalsamples = 0;
+
+ // write JPC MAGIC
+ writeBE16(0xffff);
+ JMDfile.Write(Encoding.ASCII.GetBytes("JPCRRMULTIDUMP"), 0, 14);
+
+ // write channel table
+ writeBE16(3); // number of streams
+
+ // for each stream
+ writeBE16(0); // channel 0
+ writeBE16(0); // video
+ writeBE16(0); // no name
+
+ writeBE16(1); // channel 1
+ writeBE16(1); // pcm audio
+ writeBE16(0); // no name
+
+ writeBE16(2); // channel 2
+ writeBE16(5); // metadata
+ writeBE16(0); // no name
+
+ if (gamename != null && gamename != String.Empty)
+ {
+ byte[] temp;
+ // write metadatas
+ writeBE16(2); // data channel
+ writeBE32(0); // timestamp 0;
+ JMDfile.WriteByte(71); // gamename
+ temp = System.Text.Encoding.UTF8.GetBytes(gamename);
+ writeVar(temp.Length);
+ JMDfile.Write(temp, 0, temp.Length);
+
+ writeBE16(2);
+ writeBE32(0);
+ JMDfile.WriteByte(65); // authors
+ temp = System.Text.Encoding.UTF8.GetBytes(authors);
+ writeVar(temp.Length);
+ JMDfile.Write(temp, 0, temp.Length);
+
+ writeBE16(2);
+ writeBE32(0);
+ JMDfile.WriteByte(76); // length
+ writeVar(8);
+ writeBE64(lengthms * 1000000);
+
+ writeBE16(2);
+ writeBE32(0);
+ JMDfile.WriteByte(82); // rerecords
+ writeVar(8);
+ writeBE64(rerecords);
+ }
+
+ // start up thread
+ // problem: since audio chunks and video frames both go through here, exactly how many worker gzips this
+ // gives is not known without knowing how the emulator will chunk audio packets
+ // this shouldn't affect results though, just performance
+ threadQ = new System.Collections.Concurrent.BlockingCollection