From 35a9edc018c4137798c6303ac3f3f95031a7d2a8 Mon Sep 17 00:00:00 2001 From: goyuken Date: Wed, 9 May 2012 15:08:57 +0000 Subject: [PATCH] Refactor JMDWriter to be a bit less painful to read --- BizHawk.MultiClient/JMDWriter.cs | 770 +++++++++++++++++-------------- 1 file changed, 435 insertions(+), 335 deletions(-) diff --git a/BizHawk.MultiClient/JMDWriter.cs b/BizHawk.MultiClient/JMDWriter.cs index 6566f359d9..16d714f464 100644 --- a/BizHawk.MultiClient/JMDWriter.cs +++ b/BizHawk.MultiClient/JMDWriter.cs @@ -9,6 +9,12 @@ using ICSharpCode.SharpZipLib.Zip.Compression; namespace BizHawk.MultiClient { + /// + /// 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) + /// class JMDWriter : IVideoWriter { /// @@ -20,12 +26,18 @@ namespace BizHawk.MultiClient { } + /// + /// how hard the zlib compressor works + /// public int compressionlevel { get; set; } + /// + /// number of threads to be used for video compression (sort of) + /// public int numthreads { get; @@ -71,31 +83,424 @@ namespace BizHawk.MultiClient /// /// actual disk file being written /// - FileStream JMDfile; + JMDfile jmdfile; /// - /// 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 /// - UInt64 timestampoff; + class MovieMetaData + { + /// + /// name of the game (rom) + /// + public string gamename; + /// + /// author(s) names + /// + public string authors; + /// + /// total length of the movie: ms + /// + public UInt64 lengthms; + /// + /// number of rerecords + /// + public UInt64 rerecords; + } /// - /// total number of video frames, used to calculate timestamps + /// represents the metadata for the active movie (if applicable) /// - UInt64 totalframes; + MovieMetaData moviemetadata; /// - /// total number of audio samples, used to calculate timestamps + /// represents a JMD file packet ready to be written except for sorting and timestamp offset /// - 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 + /// + /// writes JMDfile packets to an underlying bytestream + /// handles one video, one pcm audio, and one metadata track + /// + class JMDfile + { + /// + /// current timestamp position + /// + UInt64 timestampoff; + /// + /// total number of video frames written + /// + UInt64 totalframes; + /// + /// total number of sample pairs written + /// + UInt64 totalsamples; - string gamename; - string authors; - UInt64 lengthms; - UInt64 rerecords; + /// + /// fps of the video stream is fpsnum/fpsden + /// + int fpsnum; + /// + /// fps of the video stream is fpsnum/fpsden + /// + int fpsden; + /// + /// audio samplerate in hz + /// + int audiosamplerate; + /// + /// true if input will be stereo; mono otherwise + /// output stream is always stereo + /// + bool stereo; + /// + /// underlying bytestream that is being written to + /// + 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(); + vstorage = new Queue(); + + writeheader(); + } + + /// + /// write header to the JPC file + /// assumes one video, one audio, and one metadata stream, with hardcoded IDs + /// + 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 + } + + /// + /// write metadata for a movie file + /// can be called at any time + /// + /// metadata to write + 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); + } + + /// + /// write big endian 16 bit unsigned + /// + /// + void writeBE16(UInt16 v) + { + byte[] b = new byte[2]; + b[0] = (byte)(v >> 8); + b[1] = (byte)(v & 255); + f.Write(b, 0, 2); + } + + /// + /// write big endian 32 bit unsigned + /// + /// + 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); + } + + /// + /// write big endian 64 bit unsigned + /// + /// + 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); + } + + /// + /// write variable length value + /// encoding is similar to MIDI + /// + /// + 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]); + } + + /// + /// write variable length value + /// encoding is similar to MIDI + /// + /// + void writeVar(int v) + { + if (v < 0) + throw new ArgumentException("length cannot be less than 0!"); + writeVar((UInt64)v); + } + + /// + /// creates a timestamp out of fps value + /// + /// fpsnum + /// fpsden + /// frame position + /// timestamp in nanoseconds + 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; + } + + /// + /// actually write a packet to file + /// timestamp sequence must be nondecreasing + /// + /// + 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); + } + + /// + /// assemble JMDPacket and send to packetqueue + /// + /// zlibed frame with width and height prepended + 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); + } + + /// + /// 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 + /// + /// + 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]); + } + + /// + /// helper function makes a JMDPacket out of one sample pair and adds it to the order queue + /// + /// left sample + /// right sample + 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? + + /// + /// collection of JMDpackets yet to be written (audio) + /// + Queue astorage; + /// + /// collection of JMDpackets yet to be written (video) + /// + Queue vstorage; + + /// + /// 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) + /// + /// + 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); + } + + /// + /// 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) + /// + /// + 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); + } + + /// + /// flush all remaining JMDPackets to file + /// call before closing the file + /// + 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()); + } + + /// + /// flush any remaining packets and close underlying stream + /// + public void Close() + { + flushpackets(); + f.Close(); + } + } /// /// 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 } - /// /// sets the codec token to be used for video compression /// @@ -191,7 +592,6 @@ namespace BizHawk.MultiClient audiobits = bits; } - /// /// 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(); - vstorage = new Queue(); } // 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 } } - /// - /// write big endian 16 bit unsigned to JMDfile - /// - /// - void writeBE16(UInt16 v) - { - byte[] b = new byte[2]; - b[0] = (byte)(v >> 8); - b[1] = (byte)(v & 255); - JMDfile.Write(b, 0, 2); - } - - /// - /// write big endian 32 bit unsigned to JMDfile - /// - /// - 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); - } - - /// - /// write big endian 64 bit unsigned to JMDfile - /// - /// - 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); - } - - /// - /// write variable length number to file - /// encoding is similar to MIDI - /// - /// - 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]); - } - - /// - /// write variable length number to file - /// encoding is similar to MIDI - /// - /// - void writeVar(int v) - { - if (v < 0) - throw new ArgumentException("length cannot be less than 0!"); - writeVar((UInt64)v); - } - - /// - /// write packet, but they have to be in order! - /// - /// - 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? - - /// - /// collection of JMDpackets yet to be written (audio) - /// assumed to be in order - /// - Queue astorage; - /// - /// collection of JMDpackets yet to be written (video) - /// assumed to be in order - /// - Queue vstorage; - - /// - /// add a sound packet to the file write queue - /// will be written when order-appropriate wrt video - /// - /// - 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); - } - - /// - /// add a video packet to the file write queue - /// will be written when order-appropriate wrt audio - /// - /// - 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); - } - /// - /// flush all remaining JMDPackets to file - /// call before closing the file - /// - 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()); - } - /// /// close recording stream /// @@ -488,9 +662,7 @@ namespace BizHawk.MultiClient threadQ.Add(new Object()); // acts as stop message workerT.Join(); - flushpackets(); - - JMDfile.Close(); + jmdfile.Close(); } /// @@ -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 /// - /// - /// + /// video frame to compress + /// zlib compressed frame, with width and height prepended byte[] GzipFrame(VideoCopy v) { MemoryStream m = new MemoryStream(); @@ -555,6 +725,9 @@ namespace BizHawk.MultiClient /// VideoCopy to compress /// gzipped stream with width and height prepended delegate byte[] GzipFrameD(VideoCopy v); + /// + /// delegate for GzipFrame + /// GzipFrameD GzipFrameDelegate; /// @@ -580,89 +753,16 @@ namespace BizHawk.MultiClient threadQ.Add((short[])samples.Clone()); } - /// - /// assemble JMDPacket and send to packetqueue - /// - /// - 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); - } - - /// - /// 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 - /// - /// - 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); - } - - /// - /// represents a JMD file packet ready to be written except for sorting and timestamp offset - /// - class JMDPacket - { - public UInt16 stream; - public UInt64 timestamp; // haven't subtracted timestampoffs yet - public byte subtype; - public byte[] data; - } - - /// - /// creates a timestamp out of fps value - /// - /// fpsnum - /// fpsden - /// frame position - /// - 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; - } /// /// set metadata parameters; should be called before opening file - /// NYI /// 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; } } }