Refactor JMDWriter to be a bit less painful to read
This commit is contained in:
parent
d0ada9dbb3
commit
35a9edc018
|
@ -9,6 +9,12 @@ using ICSharpCode.SharpZipLib.Zip.Compression;
|
|||
|
||||
namespace BizHawk.MultiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// implements IVideoWriter, outputting to format "JMD"
|
||||
/// this is the JPC-rr multidump format; there are no filesize limits, and resolution can switch dynamically
|
||||
/// so each dump is always one file
|
||||
/// they can be processed with JPC-rr streamtools or JMDSource (avisynth)
|
||||
/// </summary>
|
||||
class JMDWriter : IVideoWriter
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -20,12 +26,18 @@ namespace BizHawk.MultiClient
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// how hard the zlib compressor works
|
||||
/// </summary>
|
||||
public int compressionlevel
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// number of threads to be used for video compression (sort of)
|
||||
/// </summary>
|
||||
public int numthreads
|
||||
{
|
||||
get;
|
||||
|
@ -71,31 +83,424 @@ namespace BizHawk.MultiClient
|
|||
/// <summary>
|
||||
/// actual disk file being written
|
||||
/// </summary>
|
||||
FileStream JMDfile;
|
||||
JMDfile jmdfile;
|
||||
|
||||
/// <summary>
|
||||
/// current timestamp offset in JMD
|
||||
/// ie, (number of ffffffffff appearances) * (ffffffff)
|
||||
/// metadata for a movie
|
||||
/// not needed if we aren't dumping something that's not a movie
|
||||
/// </summary>
|
||||
UInt64 timestampoff;
|
||||
class MovieMetaData
|
||||
{
|
||||
/// <summary>
|
||||
/// name of the game (rom)
|
||||
/// </summary>
|
||||
public string gamename;
|
||||
/// <summary>
|
||||
/// author(s) names
|
||||
/// </summary>
|
||||
public string authors;
|
||||
/// <summary>
|
||||
/// total length of the movie: ms
|
||||
/// </summary>
|
||||
public UInt64 lengthms;
|
||||
/// <summary>
|
||||
/// number of rerecords
|
||||
/// </summary>
|
||||
public UInt64 rerecords;
|
||||
}
|
||||
/// <summary>
|
||||
/// total number of video frames, used to calculate timestamps
|
||||
/// represents the metadata for the active movie (if applicable)
|
||||
/// </summary>
|
||||
UInt64 totalframes;
|
||||
MovieMetaData moviemetadata;
|
||||
|
||||
/// <summary>
|
||||
/// total number of audio samples, used to calculate timestamps
|
||||
/// represents a JMD file packet ready to be written except for sorting and timestamp offset
|
||||
/// </summary>
|
||||
UInt64 totalsamples;
|
||||
class JMDPacket
|
||||
{
|
||||
public UInt16 stream;
|
||||
public UInt64 timestamp; // final muxed timestamp will be relative to previous
|
||||
public byte subtype;
|
||||
public byte[] data;
|
||||
}
|
||||
|
||||
// movie metadata
|
||||
/// <summary>
|
||||
/// writes JMDfile packets to an underlying bytestream
|
||||
/// handles one video, one pcm audio, and one metadata track
|
||||
/// </summary>
|
||||
class JMDfile
|
||||
{
|
||||
/// <summary>
|
||||
/// current timestamp position
|
||||
/// </summary>
|
||||
UInt64 timestampoff;
|
||||
/// <summary>
|
||||
/// total number of video frames written
|
||||
/// </summary>
|
||||
UInt64 totalframes;
|
||||
/// <summary>
|
||||
/// total number of sample pairs written
|
||||
/// </summary>
|
||||
UInt64 totalsamples;
|
||||
|
||||
string gamename;
|
||||
string authors;
|
||||
UInt64 lengthms;
|
||||
UInt64 rerecords;
|
||||
/// <summary>
|
||||
/// fps of the video stream is fpsnum/fpsden
|
||||
/// </summary>
|
||||
int fpsnum;
|
||||
/// <summary>
|
||||
/// fps of the video stream is fpsnum/fpsden
|
||||
/// </summary>
|
||||
int fpsden;
|
||||
/// <summary>
|
||||
/// audio samplerate in hz
|
||||
/// </summary>
|
||||
int audiosamplerate;
|
||||
/// <summary>
|
||||
/// true if input will be stereo; mono otherwise
|
||||
/// output stream is always stereo
|
||||
/// </summary>
|
||||
bool stereo;
|
||||
|
||||
/// <summary>
|
||||
/// underlying bytestream that is being written to
|
||||
/// </summary>
|
||||
Stream f;
|
||||
public JMDfile(Stream f, int fpsnum, int fpsden, int audiosamplerate, bool stereo)
|
||||
{
|
||||
if (!f.CanWrite)
|
||||
throw new ArgumentException("Stream must be writable!");
|
||||
|
||||
this.f = f;
|
||||
this.fpsnum = fpsnum;
|
||||
this.fpsden = fpsden;
|
||||
this.audiosamplerate = audiosamplerate;
|
||||
this.stereo = stereo;
|
||||
|
||||
timestampoff = 0;
|
||||
totalframes = 0;
|
||||
totalsamples = 0;
|
||||
|
||||
astorage = new Queue<JMDPacket>();
|
||||
vstorage = new Queue<JMDPacket>();
|
||||
|
||||
writeheader();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write header to the JPC file
|
||||
/// assumes one video, one audio, and one metadata stream, with hardcoded IDs
|
||||
/// </summary>
|
||||
void writeheader()
|
||||
{
|
||||
// write JPC MAGIC
|
||||
writeBE16(0xffff);
|
||||
f.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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write metadata for a movie file
|
||||
/// can be called at any time
|
||||
/// </summary>
|
||||
/// <param name="mmd">metadata to write</param>
|
||||
public void writemetadata(MovieMetaData mmd)
|
||||
{
|
||||
byte[] temp;
|
||||
// write metadatas
|
||||
writeBE16(2); // data channel
|
||||
writeBE32(0); // timestamp (same time as previous packet)
|
||||
f.WriteByte(71); // gamename
|
||||
temp = System.Text.Encoding.UTF8.GetBytes(mmd.gamename);
|
||||
writeVar(temp.Length);
|
||||
f.Write(temp, 0, temp.Length);
|
||||
|
||||
writeBE16(2);
|
||||
writeBE32(0);
|
||||
f.WriteByte(65); // authors
|
||||
temp = System.Text.Encoding.UTF8.GetBytes(mmd.authors);
|
||||
writeVar(temp.Length);
|
||||
f.Write(temp, 0, temp.Length);
|
||||
|
||||
writeBE16(2);
|
||||
writeBE32(0);
|
||||
f.WriteByte(76); // length
|
||||
writeVar(8);
|
||||
writeBE64(mmd.lengthms * 1000000);
|
||||
|
||||
writeBE16(2);
|
||||
writeBE32(0);
|
||||
f.WriteByte(82); // rerecords
|
||||
writeVar(8);
|
||||
writeBE64(mmd.rerecords);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write big endian 16 bit unsigned
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeBE16(UInt16 v)
|
||||
{
|
||||
byte[] b = new byte[2];
|
||||
b[0] = (byte)(v >> 8);
|
||||
b[1] = (byte)(v & 255);
|
||||
f.Write(b, 0, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write big endian 32 bit unsigned
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeBE32(UInt32 v)
|
||||
{
|
||||
byte[] b = new byte[4];
|
||||
b[0] = (byte)(v >> 24);
|
||||
b[1] = (byte)(v >> 16);
|
||||
b[2] = (byte)(v >> 8);
|
||||
b[3] = (byte)(v & 255);
|
||||
f.Write(b, 0, 4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write big endian 64 bit unsigned
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeBE64(UInt64 v)
|
||||
{
|
||||
byte[] b = new byte[8];
|
||||
for (int i = 7; i >= 0; i--)
|
||||
{
|
||||
b[i] = (byte)(v & 255);
|
||||
v >>= 8;
|
||||
}
|
||||
f.Write(b, 0, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write variable length value
|
||||
/// encoding is similar to MIDI
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeVar(UInt64 v)
|
||||
{
|
||||
byte[] b = new byte[10];
|
||||
int i = 0;
|
||||
while (v > 0)
|
||||
{
|
||||
if (i > 0)
|
||||
b[i++] = (byte)((v & 127) | 128);
|
||||
else
|
||||
b[i++] = (byte)(v & 127);
|
||||
v /= 128;
|
||||
}
|
||||
if (i == 0)
|
||||
f.WriteByte(0);
|
||||
else
|
||||
for (; i > 0; i--)
|
||||
f.WriteByte(b[i - 1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write variable length value
|
||||
/// encoding is similar to MIDI
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeVar(int v)
|
||||
{
|
||||
if (v < 0)
|
||||
throw new ArgumentException("length cannot be less than 0!");
|
||||
writeVar((UInt64)v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// creates a timestamp out of fps value
|
||||
/// </summary>
|
||||
/// <param name="rate">fpsnum</param>
|
||||
/// <param name="scale">fpsden</param>
|
||||
/// <param name="pos">frame position</param>
|
||||
/// <returns>timestamp in nanoseconds</returns>
|
||||
static UInt64 timestampcalc(int rate, int scale, UInt64 pos)
|
||||
{
|
||||
// rate/scale events per second
|
||||
// timestamp is in nanoseconds
|
||||
// round down, consistent with JPC-rr apparently?
|
||||
var b = new System.Numerics.BigInteger(pos) * scale * 1000000000 / rate;
|
||||
|
||||
return (UInt64)b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// actually write a packet to file
|
||||
/// timestamp sequence must be nondecreasing
|
||||
/// </summary>
|
||||
/// <param name="j"></param>
|
||||
void writeActual(JMDPacket j)
|
||||
{
|
||||
if (j.timestamp < timestampoff)
|
||||
throw new ArithmeticException("JMD Timestamp problem?");
|
||||
UInt64 timestampout = j.timestamp - timestampoff;
|
||||
while (timestampout > 0xffffffff)
|
||||
{
|
||||
timestampout -= 0xffffffff;
|
||||
// write timestamp skipper
|
||||
for (int i = 0; i < 6; i++)
|
||||
f.WriteByte(0xff);
|
||||
}
|
||||
timestampoff = j.timestamp;
|
||||
writeBE16(j.stream);
|
||||
writeBE32((UInt32)timestampout);
|
||||
f.WriteByte(j.subtype);
|
||||
writeVar((UInt64)j.data.LongLength);
|
||||
f.Write(j.data, 0, j.data.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// assemble JMDPacket and send to packetqueue
|
||||
/// </summary>
|
||||
/// <param name="source">zlibed frame with width and height prepended</param>
|
||||
public void AddVideo(byte[] source)
|
||||
{
|
||||
var j = new JMDPacket();
|
||||
j.stream = 0;
|
||||
j.subtype = 1; // zlib compressed, other possibility is 0 = uncompressed
|
||||
j.data = source;
|
||||
j.timestamp = timestampcalc(fpsnum, fpsden, (UInt64)totalframes);
|
||||
totalframes++;
|
||||
writevideo(j);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// assemble JMDPacket and send to packetqueue
|
||||
/// one audio packet is split up into many many JMD packets, since JMD requires only 2 samples (1 left, 1 right) per packet
|
||||
/// </summary>
|
||||
/// <param name="samples"></param>
|
||||
public void AddSamples(short[] samples)
|
||||
{
|
||||
if (!stereo)
|
||||
for (int i = 0; i < samples.Length; i++)
|
||||
doaudiopacket(samples[i], samples[i]);
|
||||
else
|
||||
for (int i = 0; i < samples.Length / 2; i++)
|
||||
doaudiopacket(samples[2 * i], samples[2 * i + 1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// helper function makes a JMDPacket out of one sample pair and adds it to the order queue
|
||||
/// </summary>
|
||||
/// <param name="l">left sample</param>
|
||||
/// <param name="r">right sample</param>
|
||||
void doaudiopacket(short l, short r)
|
||||
{
|
||||
var j = new JMDPacket();
|
||||
j.stream = 1;
|
||||
j.subtype = 1; // raw PCM audio
|
||||
j.data = new byte[4];
|
||||
j.data[0] = (byte)(l >> 8);
|
||||
j.data[1] = (byte)(l & 255);
|
||||
j.data[2] = (byte)(r >> 8);
|
||||
j.data[3] = (byte)(r & 255);
|
||||
|
||||
j.timestamp = timestampcalc(audiosamplerate, 1, totalsamples);
|
||||
totalsamples++;
|
||||
writesound(j);
|
||||
}
|
||||
|
||||
// ensure outputs are in order
|
||||
// JMD packets must be in nondecreasing timestamp order, but there's no obligation
|
||||
// for us to get handed that. this code is a bit overcomplex to handle edge cases
|
||||
// that may not be a problem with the current system?
|
||||
|
||||
/// <summary>
|
||||
/// collection of JMDpackets yet to be written (audio)
|
||||
/// </summary>
|
||||
Queue<JMDPacket> astorage;
|
||||
/// <summary>
|
||||
/// collection of JMDpackets yet to be written (video)
|
||||
/// </summary>
|
||||
Queue<JMDPacket> vstorage;
|
||||
|
||||
/// <summary>
|
||||
/// add a sound packet to the file write queue
|
||||
/// will be written when order-appropriate wrt video
|
||||
/// the sound packets added must be internally ordered (but need not match video order)
|
||||
/// </summary>
|
||||
/// <param name="j"></param>
|
||||
void writesound(JMDPacket j)
|
||||
{
|
||||
while (vstorage.Count > 0)
|
||||
{
|
||||
var p = vstorage.Peek();
|
||||
if (p.timestamp <= j.timestamp)
|
||||
writeActual(vstorage.Dequeue());
|
||||
else
|
||||
break;
|
||||
}
|
||||
astorage.Enqueue(j);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// add a video packet to the file write queue
|
||||
/// will be written when order-appropriate wrt audio
|
||||
/// the video packets added must be internally ordered (but need not match audio order)
|
||||
/// </summary>
|
||||
/// <param name="j"></param>
|
||||
void writevideo(JMDPacket j)
|
||||
{
|
||||
while (astorage.Count > 0)
|
||||
{
|
||||
var p = astorage.Peek();
|
||||
if (p.timestamp <= j.timestamp)
|
||||
writeActual(astorage.Dequeue());
|
||||
else
|
||||
break;
|
||||
}
|
||||
vstorage.Enqueue(j);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// flush all remaining JMDPackets to file
|
||||
/// call before closing the file
|
||||
/// </summary>
|
||||
void flushpackets()
|
||||
{
|
||||
while (astorage.Count > 0 && vstorage.Count > 0)
|
||||
{
|
||||
var ap = astorage.Peek();
|
||||
var av = vstorage.Peek();
|
||||
if (ap.timestamp <= av.timestamp)
|
||||
writeActual(astorage.Dequeue());
|
||||
else
|
||||
writeActual(vstorage.Dequeue());
|
||||
}
|
||||
while (astorage.Count > 0)
|
||||
writeActual(astorage.Dequeue());
|
||||
while (vstorage.Count > 0)
|
||||
writeActual(vstorage.Dequeue());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// flush any remaining packets and close underlying stream
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
flushpackets();
|
||||
f.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// sets default (probably wrong) parameters
|
||||
|
@ -109,10 +514,7 @@ namespace BizHawk.MultiClient
|
|||
audiobits = 8;
|
||||
token = null;
|
||||
|
||||
gamename = "";
|
||||
authors = "";
|
||||
lengthms = 0;
|
||||
rerecords = 0;
|
||||
moviemetadata = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -120,7 +522,6 @@ namespace BizHawk.MultiClient
|
|||
// we have no unmanaged resources
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// sets the codec token to be used for video compression
|
||||
/// </summary>
|
||||
|
@ -191,7 +592,6 @@ namespace BizHawk.MultiClient
|
|||
audiobits = bits;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// opens a recording stream
|
||||
/// set a video codec token first.
|
||||
|
@ -202,61 +602,11 @@ namespace BizHawk.MultiClient
|
|||
if (ext == null || ext.ToLower() != ".jmd")
|
||||
baseName = baseName + ".jmd";
|
||||
|
||||
JMDfile = File.Open(baseName, FileMode.OpenOrCreate);
|
||||
timestampoff = 0;
|
||||
totalframes = 0;
|
||||
totalsamples = 0;
|
||||
jmdfile = new JMDfile(File.Open(baseName, FileMode.OpenOrCreate), fpsnum, fpsden, audiosamplerate, audiochannels == 2);
|
||||
|
||||
// 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);
|
||||
}
|
||||
if (moviemetadata != null)
|
||||
jmdfile.writemetadata(moviemetadata);
|
||||
|
||||
// start up thread
|
||||
// problem: since audio chunks and video frames both go through here, exactly how many zlib workers
|
||||
|
@ -266,8 +616,6 @@ namespace BizHawk.MultiClient
|
|||
workerT = new System.Threading.Thread(new System.Threading.ThreadStart(threadproc));
|
||||
workerT.Start();
|
||||
GzipFrameDelegate = new GzipFrameD(GzipFrame);
|
||||
astorage = new Queue<JMDPacket>();
|
||||
vstorage = new Queue<JMDPacket>();
|
||||
}
|
||||
|
||||
// some of this code is copied from AviWriter... not sure how if at all it should be abstracted
|
||||
|
@ -291,9 +639,9 @@ namespace BizHawk.MultiClient
|
|||
{
|
||||
Object o = threadQ.Take();
|
||||
if (o is IAsyncResult)
|
||||
AddFrameEx(GzipFrameDelegate.EndInvoke((IAsyncResult)o));
|
||||
jmdfile.AddVideo(GzipFrameDelegate.EndInvoke((IAsyncResult)o));
|
||||
else if (o is short[])
|
||||
AddSamplesEx((short[])o);
|
||||
jmdfile.AddSamples((short[])o);
|
||||
else
|
||||
// anything else is assumed to be quit time
|
||||
return;
|
||||
|
@ -306,180 +654,6 @@ namespace BizHawk.MultiClient
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write big endian 16 bit unsigned to JMDfile
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeBE16(UInt16 v)
|
||||
{
|
||||
byte[] b = new byte[2];
|
||||
b[0] = (byte)(v >> 8);
|
||||
b[1] = (byte)(v & 255);
|
||||
JMDfile.Write(b, 0, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write big endian 32 bit unsigned to JMDfile
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeBE32(UInt32 v)
|
||||
{
|
||||
byte[] b = new byte[4];
|
||||
b[0] = (byte)(v >> 24);
|
||||
b[1] = (byte)(v >> 16);
|
||||
b[2] = (byte)(v >> 8);
|
||||
b[3] = (byte)(v & 255);
|
||||
JMDfile.Write(b, 0, 4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write big endian 64 bit unsigned to JMDfile
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeBE64(UInt64 v)
|
||||
{
|
||||
byte[] b = new byte[8];
|
||||
for (int i = 7; i >= 0; i--)
|
||||
{
|
||||
b[i] = (byte)(v & 255);
|
||||
v >>= 8;
|
||||
}
|
||||
JMDfile.Write(b, 0, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write variable length number to file
|
||||
/// encoding is similar to MIDI
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeVar(UInt64 v)
|
||||
{
|
||||
byte[] b = new byte[10];
|
||||
int i = 0;
|
||||
while (v > 0)
|
||||
{
|
||||
if (i > 0)
|
||||
b[i++] = (byte)((v & 127) | 128);
|
||||
else
|
||||
b[i++] = (byte)(v & 127);
|
||||
v /= 128;
|
||||
}
|
||||
if (i == 0)
|
||||
JMDfile.WriteByte(0);
|
||||
else
|
||||
for (; i > 0; i--)
|
||||
JMDfile.WriteByte(b[i - 1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write variable length number to file
|
||||
/// encoding is similar to MIDI
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void writeVar(int v)
|
||||
{
|
||||
if (v < 0)
|
||||
throw new ArgumentException("length cannot be less than 0!");
|
||||
writeVar((UInt64)v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write packet, but they have to be in order!
|
||||
/// </summary>
|
||||
/// <param name="j"></param>
|
||||
void writeActual(JMDPacket j)
|
||||
{
|
||||
if (j.timestamp < timestampoff)
|
||||
throw new ArithmeticException("JMD Timestamp problem?");
|
||||
UInt64 timestampout = j.timestamp - timestampoff;
|
||||
while (timestampout > 0xffffffff)
|
||||
{
|
||||
timestampout -= 0xffffffff;
|
||||
// write timestamp skipper
|
||||
for (int i = 0; i < 6; i++)
|
||||
JMDfile.WriteByte(0xff);
|
||||
}
|
||||
timestampoff = j.timestamp;
|
||||
writeBE16(j.stream);
|
||||
writeBE32((UInt32)timestampout);
|
||||
JMDfile.WriteByte(j.subtype);
|
||||
writeVar((UInt64)j.data.LongLength);
|
||||
JMDfile.Write(j.data, 0, j.data.Length);
|
||||
}
|
||||
|
||||
|
||||
// ensure outputs are in order
|
||||
// JMD packets must be in nondecreasing timestamp order, but there's no obligation
|
||||
// for us to get handed that. this code is a bit overcomplex to handle edge cases
|
||||
// that may not be a problem with the current system?
|
||||
|
||||
/// <summary>
|
||||
/// collection of JMDpackets yet to be written (audio)
|
||||
/// assumed to be in order
|
||||
/// </summary>
|
||||
Queue<JMDPacket> astorage;
|
||||
/// <summary>
|
||||
/// collection of JMDpackets yet to be written (video)
|
||||
/// assumed to be in order
|
||||
/// </summary>
|
||||
Queue<JMDPacket> vstorage;
|
||||
|
||||
/// <summary>
|
||||
/// add a sound packet to the file write queue
|
||||
/// will be written when order-appropriate wrt video
|
||||
/// </summary>
|
||||
/// <param name="j"></param>
|
||||
void writesound(JMDPacket j)
|
||||
{
|
||||
while (vstorage.Count > 0)
|
||||
{
|
||||
var p = vstorage.Peek();
|
||||
if (p.timestamp <= j.timestamp)
|
||||
writeActual(vstorage.Dequeue());
|
||||
else
|
||||
break;
|
||||
}
|
||||
astorage.Enqueue(j);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// add a video packet to the file write queue
|
||||
/// will be written when order-appropriate wrt audio
|
||||
/// </summary>
|
||||
/// <param name="j"></param>
|
||||
void writevideo(JMDPacket j)
|
||||
{
|
||||
while (astorage.Count > 0)
|
||||
{
|
||||
var p = astorage.Peek();
|
||||
if (p.timestamp <= j.timestamp)
|
||||
writeActual(astorage.Dequeue());
|
||||
else
|
||||
break;
|
||||
}
|
||||
vstorage.Enqueue(j);
|
||||
}
|
||||
/// <summary>
|
||||
/// flush all remaining JMDPackets to file
|
||||
/// call before closing the file
|
||||
/// </summary>
|
||||
void flushpackets()
|
||||
{
|
||||
while (astorage.Count > 0 && vstorage.Count > 0)
|
||||
{
|
||||
var ap = astorage.Peek();
|
||||
var av = vstorage.Peek();
|
||||
if (ap.timestamp <= av.timestamp)
|
||||
writeActual(astorage.Dequeue());
|
||||
else
|
||||
writeActual(vstorage.Dequeue());
|
||||
}
|
||||
while (astorage.Count > 0)
|
||||
writeActual(astorage.Dequeue());
|
||||
while (vstorage.Count > 0)
|
||||
writeActual(vstorage.Dequeue());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// close recording stream
|
||||
/// </summary>
|
||||
|
@ -488,9 +662,7 @@ namespace BizHawk.MultiClient
|
|||
threadQ.Add(new Object()); // acts as stop message
|
||||
workerT.Join();
|
||||
|
||||
flushpackets();
|
||||
|
||||
JMDfile.Close();
|
||||
jmdfile.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -503,7 +675,6 @@ namespace BizHawk.MultiClient
|
|||
|
||||
public int BufferWidth;
|
||||
public int BufferHeight;
|
||||
public int BackgroundColor;
|
||||
public VideoCopy(IVideoProvider c)
|
||||
{
|
||||
int[] vb = c.GetVideoBuffer();
|
||||
|
@ -519,7 +690,6 @@ namespace BizHawk.MultiClient
|
|||
//Buffer.BlockCopy(vb, 0, VideoBuffer, 0, VideoBuffer.Length);
|
||||
BufferWidth = c.BufferWidth;
|
||||
BufferHeight = c.BufferHeight;
|
||||
BackgroundColor = c.BackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -528,8 +698,8 @@ namespace BizHawk.MultiClient
|
|||
/// the byte array includes width and height dimensions at the beginning
|
||||
/// this is run asynchronously for speedup, as compressing can be slow
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="v">video frame to compress</param>
|
||||
/// <returns>zlib compressed frame, with width and height prepended</returns>
|
||||
byte[] GzipFrame(VideoCopy v)
|
||||
{
|
||||
MemoryStream m = new MemoryStream();
|
||||
|
@ -555,6 +725,9 @@ namespace BizHawk.MultiClient
|
|||
/// <param name="v">VideoCopy to compress</param>
|
||||
/// <returns>gzipped stream with width and height prepended</returns>
|
||||
delegate byte[] GzipFrameD(VideoCopy v);
|
||||
/// <summary>
|
||||
/// delegate for GzipFrame
|
||||
/// </summary>
|
||||
GzipFrameD GzipFrameDelegate;
|
||||
|
||||
/// <summary>
|
||||
|
@ -580,89 +753,16 @@ namespace BizHawk.MultiClient
|
|||
threadQ.Add((short[])samples.Clone());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// assemble JMDPacket and send to packetqueue
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
void AddFrameEx(byte[] source)
|
||||
{
|
||||
// at this point, VideoCopy contains a zlib compressed bytestream
|
||||
var j = new JMDPacket();
|
||||
j.stream = 0;
|
||||
j.subtype = 1; // zlib compressed, other possibility is 0 = uncompressed
|
||||
j.data = source;
|
||||
j.timestamp = timestampcalc(fpsnum, fpsden, (UInt64)totalframes);
|
||||
totalframes++;
|
||||
writevideo(j);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// assemble JMDPacket and send to packetqueue
|
||||
/// one audio packet is split up into many many JMD packets, since JMD requires only 2 samples (1 left, 1 right) per packet
|
||||
/// </summary>
|
||||
/// <param name="samples"></param>
|
||||
void AddSamplesEx(short[] samples)
|
||||
{
|
||||
if (audiochannels == 1)
|
||||
for (int i = 0; i < samples.Length; i++)
|
||||
doaudiopacket(samples[i], samples[i]);
|
||||
else
|
||||
for (int i = 0; i < samples.Length / 2; i++)
|
||||
doaudiopacket(samples[2 * i], samples[2 * i + 1]);
|
||||
}
|
||||
void doaudiopacket(short l, short r)
|
||||
{
|
||||
var j = new JMDPacket();
|
||||
j.stream = 1;
|
||||
j.subtype = 1; // raw PCM audio
|
||||
j.data = new byte[4];
|
||||
j.data[0] = (byte)(l >> 8);
|
||||
j.data[1] = (byte)(l & 255);
|
||||
j.data[2] = (byte)(r >> 8);
|
||||
j.data[3] = (byte)(r & 255);
|
||||
|
||||
j.timestamp = timestampcalc(audiosamplerate, 1, totalsamples);
|
||||
totalsamples++;
|
||||
writesound(j);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// represents a JMD file packet ready to be written except for sorting and timestamp offset
|
||||
/// </summary>
|
||||
class JMDPacket
|
||||
{
|
||||
public UInt16 stream;
|
||||
public UInt64 timestamp; // haven't subtracted timestampoffs yet
|
||||
public byte subtype;
|
||||
public byte[] data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// creates a timestamp out of fps value
|
||||
/// </summary>
|
||||
/// <param name="rate">fpsnum</param>
|
||||
/// <param name="scale">fpsden</param>
|
||||
/// <param name="pos">frame position</param>
|
||||
/// <returns></returns>
|
||||
static UInt64 timestampcalc(int rate, int scale, UInt64 pos)
|
||||
{
|
||||
// rate/scale events per second
|
||||
// timestamp is in nanoseconds
|
||||
// round down, consistent with JPC-rr apparently?
|
||||
var b = new System.Numerics.BigInteger(pos) * scale * 1000000000 / rate;
|
||||
|
||||
return (UInt64)b;
|
||||
}
|
||||
/// <summary>
|
||||
/// set metadata parameters; should be called before opening file
|
||||
/// NYI
|
||||
/// </summary>
|
||||
public void SetMetaData(string gameName, string authors, UInt64 lengthMS, UInt64 rerecords)
|
||||
{
|
||||
this.gamename = gameName;
|
||||
this.authors = authors;
|
||||
this.lengthms = lengthMS;
|
||||
this.rerecords = rerecords;
|
||||
moviemetadata = new MovieMetaData();
|
||||
moviemetadata.gamename = gameName;
|
||||
moviemetadata.authors = authors;
|
||||
moviemetadata.lengthms = lengthMS;
|
||||
moviemetadata.rerecords = rerecords;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue