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:
parent
7bf325cb81
commit
a348acc1f2
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -109,7 +109,8 @@ namespace BizHawk
|
|||
new JMDWriter(),
|
||||
new WavWriterV(),
|
||||
new FFmpegWriter(),
|
||||
new NutWriter()
|
||||
new NutWriter(),
|
||||
new BizHawk.MultiClient.AVOut.GifWriter()
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue