add GifWriter, an implementation of AnimatedGif as an IVideoWriter

main advantage is that the emulator can be controlled while it records, like the others
the parameters for it are a bit different though...
This commit is contained in:
goyuken 2012-09-22 00:44:59 +00:00
parent 7bf325cb81
commit a348acc1f2
7 changed files with 476 additions and 1 deletions

View File

@ -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
{
/// <summary>
/// how many frames to skip for each frame deposited
/// </summary>
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();
}
/// <summary>
/// true if the first frame has been written to the file; false otherwise
/// </summary>
bool firstdone = false;
/// <summary>
/// the underlying stream we're writing to
/// </summary>
Stream f;
/// <summary>
/// a final byte we must write before closing the stream
/// </summary>
byte lastbyte;
/// <summary>
/// keep track of skippable frames
/// </summary>
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();
}
/// <summary>
/// precooked gif header
/// </summary>
static byte[] GifAnimation = {33, 255, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48, 3, 1, 0, 0, 0};
/// <summary>
/// little endian frame length in 10ms units
/// </summary>
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";
}
}
}

View File

@ -0,0 +1,105 @@
namespace BizHawk.MultiClient.AVOut
{
partial class GifWriterForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -109,7 +109,8 @@ namespace BizHawk
new JMDWriter(),
new WavWriterV(),
new FFmpegWriter(),
new NutWriter()
new NutWriter(),
new BizHawk.MultiClient.AVOut.GifWriter()
};
return ret;
}

View File

@ -122,6 +122,13 @@
<Compile Include="AVOut\FFmpegWriterForm.Designer.cs">
<DependentUpon>FFmpegWriterForm.cs</DependentUpon>
</Compile>
<Compile Include="AVOut\GifWriter.cs" />
<Compile Include="AVOut\GifWriterForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="AVOut\GifWriterForm.Designer.cs">
<DependentUpon>GifWriterForm.cs</DependentUpon>
</Compile>
<Compile Include="AVOut\IVideoWriter.cs" />
<Compile Include="AVOut\JMDForm.cs">
<SubType>Form</SubType>
@ -392,6 +399,9 @@
<EmbeddedResource Include="AVOut\FFmpegWriterForm.resx">
<DependentUpon>FFmpegWriterForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="AVOut\GifWriterForm.resx">
<DependentUpon>GifWriterForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="AVOut\JMDForm.resx">
<DependentUpon>JMDForm.cs</DependentUpon>
</EmbeddedResource>

View File

@ -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;