diff --git a/BizHawk.MultiClient/AVOut/GifWriter.cs b/BizHawk.MultiClient/AVOut/GifWriter.cs
new file mode 100644
index 0000000000..bc0a507a51
--- /dev/null
+++ b/BizHawk.MultiClient/AVOut/GifWriter.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.IO;
+using System.Drawing;
+
+namespace BizHawk.MultiClient.AVOut
+{
+ public class GifWriter : IVideoWriter
+ {
+ public class GifToken : IDisposable
+ {
+ ///
+ /// how many frames to skip for each frame deposited
+ ///
+ public int frameskip { get; private set; }
+ public void Dispose() { }
+
+ public GifToken(int frameskip)
+ {
+ this.frameskip = frameskip;
+ }
+
+ public static GifToken LoadFromConfig()
+ {
+ GifToken ret = new GifToken(0);
+ ret.frameskip = Global.Config.GifWriterFrameskip;
+ return ret;
+ }
+ }
+ GifToken token;
+
+ public void SetVideoCodecToken(IDisposable token)
+ {
+ if (token is GifToken)
+ {
+ this.token = (GifToken)token;
+ CalcDelay();
+ }
+ else
+ throw new ArgumentException("GifWriter only takes its own tokens!");
+ }
+
+ public void SetDefaultVideoCodecToken()
+ {
+ token = GifToken.LoadFromConfig();
+ CalcDelay();
+ }
+
+ ///
+ /// true if the first frame has been written to the file; false otherwise
+ ///
+ bool firstdone = false;
+
+ ///
+ /// the underlying stream we're writing to
+ ///
+ Stream f;
+
+ ///
+ /// a final byte we must write before closing the stream
+ ///
+ byte lastbyte;
+
+ ///
+ /// keep track of skippable frames
+ ///
+ int skipindex = 0;
+
+ int fpsnum = 1, fpsden = 1;
+
+
+ public void OpenFile(string baseName)
+ {
+ f = new FileStream(baseName, FileMode.OpenOrCreate, FileAccess.Write);
+ skipindex = token.frameskip;
+ }
+
+ public void CloseFile()
+ {
+ f.WriteByte(lastbyte);
+ f.Close();
+ }
+
+ ///
+ /// precooked gif header
+ ///
+ static byte[] GifAnimation = {33, 255, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48, 3, 1, 0, 0, 0};
+ ///
+ /// little endian frame length in 10ms units
+ ///
+ byte[] Delay = {100, 0};
+ public void AddFrame(IVideoProvider source)
+ {
+ if (skipindex == token.frameskip)
+ skipindex = 0;
+ else
+ {
+ skipindex++;
+ return; // skip this frame
+ }
+
+
+ Bitmap bmp = new Bitmap(source.BufferWidth, source.BufferHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
+ {
+ var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
+ System.Runtime.InteropServices.Marshal.Copy(source.GetVideoBuffer(), 0, data.Scan0, bmp.Width * bmp.Height);
+ bmp.UnlockBits(data);
+ }
+
+ MemoryStream ms = new MemoryStream();
+ bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Gif);
+ byte[] b = ms.GetBuffer();
+ if (!firstdone)
+ {
+ firstdone = true;
+ b[10] = (byte)(b[10] & 0x78); // no global color table
+ f.Write(b, 0, 13);
+ f.Write(GifAnimation, 0, GifAnimation.Length);
+ }
+ b[785] = Delay[0];
+ b[786] = Delay[1];
+ b[798] = (byte)(b[798] | 0x87);
+ f.Write(b, 781, 18);
+ f.Write(b, 13, 768);
+ f.Write(b, 799, (int)(ms.Length - 800));
+
+ lastbyte = b[ms.Length - 1];
+ }
+
+ public void AddSamples(short[] samples)
+ {
+ // ignored
+ }
+
+ public IDisposable AcquireVideoCodecToken(System.Windows.Forms.IWin32Window hwnd)
+ {
+ return GifWriterForm.DoTokenForm(hwnd);
+ }
+
+ void CalcDelay()
+ {
+ if (token == null)
+ return;
+ int delay = (100 * fpsden * (token.frameskip + 1) + (fpsnum / 2)) / fpsnum;
+ Delay[0] = (byte)(delay & 0xff);
+ Delay[1] = (byte)(delay >> 8 & 0xff);
+ }
+
+ public void SetMovieParameters(int fpsnum, int fpsden)
+ {
+ this.fpsnum = fpsnum;
+ this.fpsden = fpsden;
+ CalcDelay();
+ }
+
+ public void SetVideoParameters(int width, int height)
+ {
+ // we read them directly from each individual frame, ignore the rest
+ }
+
+ public void SetAudioParameters(int sampleRate, int channels, int bits)
+ {
+ // ignored
+ }
+
+ public void SetMetaData(string gameName, string authors, ulong lengthMS, ulong rerecords)
+ {
+ // gif can't support this
+ }
+
+ public string WriterDescription()
+ {
+ return "Creates an animated .gif";
+ }
+
+ public string DesiredExtension()
+ {
+ return "gif";
+ }
+
+ public string ShortName()
+ {
+ return "gif";
+ }
+
+ public void Dispose()
+ {
+ if (f != null)
+ {
+ f.Dispose();
+ f = null;
+ }
+ }
+
+ public override string ToString()
+ {
+ return "gif writer";
+ }
+ }
+}
diff --git a/BizHawk.MultiClient/AVOut/GifWriterForm.Designer.cs b/BizHawk.MultiClient/AVOut/GifWriterForm.Designer.cs
new file mode 100644
index 0000000000..454fc66fc4
--- /dev/null
+++ b/BizHawk.MultiClient/AVOut/GifWriterForm.Designer.cs
@@ -0,0 +1,105 @@
+namespace BizHawk.MultiClient.AVOut
+{
+ partial class GifWriterForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.button1 = new System.Windows.Forms.Button();
+ this.button2 = new System.Windows.Forms.Button();
+ this.numericUpDown1 = new System.Windows.Forms.NumericUpDown();
+ this.label1 = new System.Windows.Forms.Label();
+ ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
+ this.SuspendLayout();
+ //
+ // button1
+ //
+ this.button1.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.button1.Location = new System.Drawing.Point(12, 51);
+ this.button1.Name = "button1";
+ this.button1.Size = new System.Drawing.Size(75, 23);
+ this.button1.TabIndex = 0;
+ this.button1.Text = "OK";
+ this.button1.UseVisualStyleBackColor = true;
+ //
+ // button2
+ //
+ this.button2.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.button2.Location = new System.Drawing.Point(93, 51);
+ this.button2.Name = "button2";
+ this.button2.Size = new System.Drawing.Size(75, 23);
+ this.button2.TabIndex = 1;
+ this.button2.Text = "Cancel";
+ this.button2.UseVisualStyleBackColor = true;
+ //
+ // numericUpDown1
+ //
+ this.numericUpDown1.Location = new System.Drawing.Point(12, 25);
+ this.numericUpDown1.Maximum = new decimal(new int[] {
+ 999,
+ 0,
+ 0,
+ 0});
+ this.numericUpDown1.Name = "numericUpDown1";
+ this.numericUpDown1.Size = new System.Drawing.Size(120, 20);
+ this.numericUpDown1.TabIndex = 2;
+ //
+ // label1
+ //
+ this.label1.AutoSize = true;
+ this.label1.Location = new System.Drawing.Point(12, 9);
+ this.label1.Name = "label1";
+ this.label1.Size = new System.Drawing.Size(232, 13);
+ this.label1.TabIndex = 3;
+ this.label1.Text = "Number of frames to skip for each frame written:";
+ //
+ // GifWriterForm
+ //
+ this.AcceptButton = this.button1;
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.CancelButton = this.button2;
+ this.ClientSize = new System.Drawing.Size(269, 83);
+ this.Controls.Add(this.label1);
+ this.Controls.Add(this.numericUpDown1);
+ this.Controls.Add(this.button2);
+ this.Controls.Add(this.button1);
+ this.Name = "GifWriterForm";
+ this.Text = "GifWriter Options";
+ ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Button button1;
+ private System.Windows.Forms.Button button2;
+ private System.Windows.Forms.NumericUpDown numericUpDown1;
+ private System.Windows.Forms.Label label1;
+ }
+}
\ No newline at end of file
diff --git a/BizHawk.MultiClient/AVOut/GifWriterForm.cs b/BizHawk.MultiClient/AVOut/GifWriterForm.cs
new file mode 100644
index 0000000000..777fc10727
--- /dev/null
+++ b/BizHawk.MultiClient/AVOut/GifWriterForm.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Windows.Forms;
+
+namespace BizHawk.MultiClient.AVOut
+{
+ public partial class GifWriterForm : Form
+ {
+ public GifWriterForm()
+ {
+ InitializeComponent();
+ }
+
+ public static GifWriter.GifToken DoTokenForm(IWin32Window parent)
+ {
+ using (var dlg = new GifWriterForm())
+ {
+ dlg.numericUpDown1.Value = Global.Config.GifWriterFrameskip;
+
+ var result = dlg.ShowDialog(parent);
+ if (result == DialogResult.OK)
+ {
+ Global.Config.GifWriterFrameskip = (int)dlg.numericUpDown1.Value;
+ return GifWriter.GifToken.LoadFromConfig();
+ }
+ else
+ return null;
+ }
+ }
+ }
+}
diff --git a/BizHawk.MultiClient/AVOut/GifWriterForm.resx b/BizHawk.MultiClient/AVOut/GifWriterForm.resx
new file mode 100644
index 0000000000..29dcb1b3a3
--- /dev/null
+++ b/BizHawk.MultiClient/AVOut/GifWriterForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/BizHawk.MultiClient/AVOut/IVideoWriter.cs b/BizHawk.MultiClient/AVOut/IVideoWriter.cs
index fe1f4fc3cf..d539dad857 100644
--- a/BizHawk.MultiClient/AVOut/IVideoWriter.cs
+++ b/BizHawk.MultiClient/AVOut/IVideoWriter.cs
@@ -109,7 +109,8 @@ namespace BizHawk
new JMDWriter(),
new WavWriterV(),
new FFmpegWriter(),
- new NutWriter()
+ new NutWriter(),
+ new BizHawk.MultiClient.AVOut.GifWriter()
};
return ret;
}
diff --git a/BizHawk.MultiClient/BizHawk.MultiClient.csproj b/BizHawk.MultiClient/BizHawk.MultiClient.csproj
index 3653210010..f8b4d7d499 100644
--- a/BizHawk.MultiClient/BizHawk.MultiClient.csproj
+++ b/BizHawk.MultiClient/BizHawk.MultiClient.csproj
@@ -122,6 +122,13 @@
FFmpegWriterForm.cs
+
+
+ Form
+
+
+ GifWriterForm.cs
+
Form
@@ -392,6 +399,9 @@
FFmpegWriterForm.cs
+
+ GifWriterForm.cs
+
JMDForm.cs
diff --git a/BizHawk.MultiClient/Config.cs b/BizHawk.MultiClient/Config.cs
index fae9f8c5a2..216bbc47b7 100644
--- a/BizHawk.MultiClient/Config.cs
+++ b/BizHawk.MultiClient/Config.cs
@@ -326,6 +326,7 @@ namespace BizHawk.MultiClient
public string FFmpegFormat = "";
public string FFmpegCustomCommand = "-c:a foo -c:v bar -f baz";
public string AVICodecToken = "";
+ public int GifWriterFrameskip = 3;
// NESPPU Settings
public bool AutoLoadNESPPU = false;