using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Numerics; namespace BizHawk.Client.EmuHawk { /// /// implements a simple muxer for the NUT media format /// http://ffmpeg.org/~michael/nut.txt /// public class NutMuxer { // this code isn't really any good for general purpose nut creation #region simple buffer reuser public class ReusableBufferPool { private readonly List _available = new List(); private readonly ICollection _inUse = new HashSet(); private readonly int _capacity; /// total number of buffers to keep around public ReusableBufferPool(int capacity) { _capacity = capacity; } private T[] GetBufferInternal(int length, bool zerofill, Func criteria) { if (_inUse.Count == _capacity) { throw new InvalidOperationException(); } T[] candidate = _available.FirstOrDefault(criteria); if (candidate == null) { if (_available.Count + _inUse.Count == _capacity) { // out of space! should not happen often Console.WriteLine("Purging"); _available.Clear(); } candidate = new T[length]; } else { if (zerofill) { Array.Clear(candidate, 0, candidate.Length); } _available.Remove(candidate); } _inUse.Add(candidate); return candidate; } public T[] GetBuffer(int length, bool zerofill = false) { return GetBufferInternal(length, zerofill, a => a.Length == length); } public T[] GetBufferAtLeast(int length, bool zerofill = false) { return GetBufferInternal(length, zerofill, a => a.Length >= length && a.Length / (float)length <= 2.0f); } /// is not in use public void ReleaseBuffer(T[] buffer) { if (!_inUse.Remove(buffer)) { throw new ArgumentException(); } _available.Add(buffer); } } #endregion #region binary write helpers /// /// variable length value, unsigned /// private static void WriteVarU(ulong v, Stream stream) { byte[] b = new byte[10]; int i = 0; do { if (i > 0) b[i++] = (byte)((v & 127) | 128); else b[i++] = (byte)(v & 127); v /= 128; } while (v > 0); for (; i > 0; i--) { stream.WriteByte(b[i - 1]); } } /// /// variable length value, unsigned /// static void WriteVarU(int v, Stream stream) { if (v < 0) { throw new ArgumentOutOfRangeException(nameof(v), "unsigned must be non-negative"); } WriteVarU((ulong)v, stream); } /// /// variable length value, unsigned /// private static void WriteVarU(long v, Stream stream) { if (v < 0) { throw new ArgumentOutOfRangeException(nameof(v), "unsigned must be non-negative"); } WriteVarU((ulong)v, stream); } /// /// utf-8 string with length prepended /// private static void WriteString(string s, Stream stream) { WriteBytes(Encoding.UTF8.GetBytes(s), stream); } /// /// arbitrary sequence of bytes with length prepended /// static void WriteBytes(byte[] b, Stream stream) { WriteVarU(b.Length, stream); stream.Write(b, 0, b.Length); } /// /// big endian 64 bit unsigned /// private static void WriteBe64(ulong v, Stream stream) { byte[] b = new byte[8]; for (int i = 7; i >= 0; i--) { b[i] = (byte)(v & 255); v >>= 8; } stream.Write(b, 0, 8); } /// /// big endian 32 bit unsigned /// private static void WriteBe32(uint v, Stream stream) { byte[] b = new byte[4]; for (int i = 3; i >= 0; i--) { b[i] = (byte)(v & 255); v >>= 8; } stream.Write(b, 0, 4); } #endregion #region CRC calculator private static readonly uint[] CrcTable = { 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, }; /// /// seems to be different than standard CRC32????? /// /// crc32, nut variant private static uint NutCRC32(byte[] buf) { uint crc = 0; foreach (var b in buf) { crc ^= (uint)b << 24; crc = (crc << 4) ^ CrcTable[crc >> 28]; crc = (crc << 4) ^ CrcTable[crc >> 28]; } return crc; } #endregion /// /// writes a single packet out, including CheckSums /// private class NutPacket : Stream { public enum StartCode : ulong { Main = 0x4e4d7a561f5f04ad, Stream = 0x4e5311405bf2f9db, Syncpoint = 0x4e4be4adeeca4569, Index = 0x4e58dd672f23e64e, Info = 0x4e49ab68b596ba78 } private MemoryStream _data; private readonly StartCode _startCode; private readonly Stream _underlying; /// /// create a new NutPacket /// /// startCode for this packet /// stream to write to public NutPacket(StartCode startCode, Stream underlying) { _data = new MemoryStream(); _startCode = startCode; _underlying = underlying; } public override bool CanRead => false; public override bool CanSeek => false; public override bool CanWrite => true; /// /// write data out to underlying stream, including header, footer, checksums /// this cannot be done more than once! /// public override void Flush() { // first, prep header var header = new MemoryStream(); WriteBe64((ulong)_startCode, header); WriteVarU(_data.Length + 4, header); // +4 for checksum if (_data.Length > 4092) { WriteBe32(NutCRC32(header.ToArray()), header); } var tmp = header.ToArray(); _underlying.Write(tmp, 0, tmp.Length); tmp = _data.ToArray(); _underlying.Write(tmp, 0, tmp.Length); WriteBe32(NutCRC32(tmp), _underlying); _data = null; } public override long Length => throw new NotImplementedException(); public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { _data.Write(buffer, offset, count); } } #region fields /// /// stores basic AV parameters /// private class AVParams { public int Width { get; set; } public int Height { get; set; } public int Samplerate { get; set; } public int FpsNum { get; set; } public int FpsDen { get; set; } public int Channels { get; set; } /// /// puts fpsNum, fpsDen in lowest terms /// public void Reduce() { int gcd = (int)BigInteger.GreatestCommonDivisor(new BigInteger(FpsNum), new BigInteger(FpsDen)); FpsNum /= gcd; FpsDen /= gcd; } } // stores basic AV parameters private readonly AVParams _avParams; // target output for nut stream private Stream _output; // PTS of video stream. timebase is 1/framerate, so this is equal to number of frames private ulong _videoOpts; // PTS of audio stream. timebase is 1/samplerate, so this is equal to number of samples private ulong _audioPts; // has EOR been written on this stream? private bool _videoDone; // has EOR been written on this stream? private bool _audioDone; // video packets waiting to be written private readonly Queue _videoQueue; // audio packets waiting to be written private readonly Queue _audioQueue; readonly ReusableBufferPool _bufferPool = new ReusableBufferPool(12); #endregion #region header writers /// /// write out the main header /// private void WriteMainHeader() { // note: this file starttag not actually part of main headers var tmp = Encoding.ASCII.GetBytes("nut/multimedia container\0"); _output.Write(tmp, 0, tmp.Length); var header = new NutPacket(NutPacket.StartCode.Main, _output); WriteVarU(3, header); // version WriteVarU(2, header); // stream_count WriteVarU(65536, header); // max_distance WriteVarU(2, header); // time_base_count // timebase is length of single frame, so reversed num+den is intentional WriteVarU(_avParams.FpsDen, header); // time_base_num[0] WriteVarU(_avParams.FpsNum, header); // time_base_den[0] WriteVarU(1, header); // time_base_num[1] WriteVarU(_avParams.Samplerate, header); // time_base_den[1] // frame flag compression is ignored for simplicity for (int i = 0; i < 255; i++) // not 256 because entry 0x4e is skipped (as it would indicate a startcode) { WriteVarU(1 << 12, header); // tmp_flag = FLAG_CODED WriteVarU(0, header); // tmp_fields } // header compression ignored because it's not useful to us WriteVarU(0, header); // header_count_minus1 // BROADCAST_MODE only useful for realtime transmission clock recovery WriteVarU(0, header); // main_flags header.Flush(); } // write out the 0th stream header (video) private void WriteVideoHeader() { var header = new NutPacket(NutPacket.StartCode.Stream, _output); WriteVarU(0, header); // stream_id WriteVarU(0, header); // stream_class = video WriteString("BGRA", header); // fourcc = "BGRA" WriteVarU(0, header); // time_base_id = 0 WriteVarU(8, header); // msb_pts_shift WriteVarU(1, header); // max_pts_distance WriteVarU(0, header); // decode_delay WriteVarU(1, header); // stream_flags = FLAG_FIXED_FPS WriteBytes(new byte[0], header); // codec_specific_data // stream_class = video WriteVarU(_avParams.Width, header); // width WriteVarU(_avParams.Height, header); // height WriteVarU(1, header); // sample_width WriteVarU(1, header); // sample_height WriteVarU(18, header); // colorspace_type = full range rec709 (avisynth's "PC.709") header.Flush(); } // write out the 1st stream header (audio) private void WriteAudioHeader() { var header = new NutPacket(NutPacket.StartCode.Stream, _output); WriteVarU(1, header); // stream_id WriteVarU(1, header); // stream_class = audio WriteString("\x01\x00\x00\x00", header); // fourcc = 01 00 00 00 WriteVarU(1, header); // time_base_id = 1 WriteVarU(8, header); // msb_pts_shift WriteVarU(_avParams.Samplerate, header); // max_pts_distance WriteVarU(0, header); // decode_delay WriteVarU(0, header); // stream_flags = none; no FIXED_FPS because we aren't guaranteeing same-size audio chunks WriteBytes(new byte[0], header); // codec_specific_data // stream_class = audio WriteVarU(_avParams.Samplerate, header); // samplerate_num WriteVarU(1, header); // samplerate_den WriteVarU(_avParams.Channels, header); // channel_count header.Flush(); } #endregion /// /// stores a single frame with syncpoint, in mux-ready form /// used because reordering of audio and video can be needed for proper interleave /// private class NutFrame { /// /// data ready to be written to stream/disk /// private readonly byte[] _data; /// /// valid length of the data /// private readonly int _actualLength; /// /// presentation timestamp /// private readonly ulong _pts; /// /// fraction of the specified timebase /// private readonly ulong _ptsNum; /// /// fraction of the specified timebase /// private readonly ulong _ptsDen; private readonly ReusableBufferPool _pool; /// frame data /// actual length of frame data /// presentation timestamp /// numerator of timebase /// denominator of timebase /// which timestamp base is used, assumed to be also stream number public NutFrame(byte[] payload, int payLoadLen, ulong pts, ulong ptsNum, ulong ptsDen, int ptsIndex, ReusableBufferPool pool) { _pts = pts; _ptsNum = ptsNum; _ptsDen = ptsDen; _pool = pool; _data = pool.GetBufferAtLeast(payLoadLen + 2048); var frame = new MemoryStream(_data); // create syncpoint var sync = new NutPacket(NutPacket.StartCode.Syncpoint, frame); WriteVarU(pts * 2 + (ulong)ptsIndex, sync); // global_key_pts WriteVarU(1, sync); // back_ptr_div_16, this is wrong sync.Flush(); var frameHeader = new MemoryStream(); frameHeader.WriteByte(0); // frame_code // frame_flags = FLAG_CODED, so: int flags = 0; flags |= 1 << 0; // FLAG_KEY if (payLoadLen == 0) { flags |= 1 << 1; // FLAG_EOR } flags |= 1 << 3; // FLAG_CODED_PTS flags |= 1 << 4; // FLAG_STREAM_ID flags |= 1 << 5; // FLAG_SIZE_MSB flags |= 1 << 6; // FLAG_CHECKSUM WriteVarU(flags, frameHeader); WriteVarU(ptsIndex, frameHeader); // stream_id WriteVarU(pts + 256, frameHeader); // coded_pts = pts + 1 << msb_pts_shift WriteVarU(payLoadLen, frameHeader); // data_size_msb var frameHeaderArr = frameHeader.ToArray(); frame.Write(frameHeaderArr, 0, frameHeaderArr.Length); WriteBe32(NutCRC32(frameHeaderArr), frame); // checksum frame.Write(payload, 0, payLoadLen); _actualLength = (int)frame.Position; } /// /// compare two NutFrames by pts /// public static bool operator <=(NutFrame lhs, NutFrame rhs) { BigInteger left = new BigInteger(lhs._pts); left = left * lhs._ptsNum * rhs._ptsDen; BigInteger right = new BigInteger(rhs._pts); right = right * rhs._ptsNum * lhs._ptsDen; return left <= right; } public static bool operator >=(NutFrame lhs, NutFrame rhs) { BigInteger left = new BigInteger(lhs._pts); left = left * lhs._ptsNum * rhs._ptsDen; BigInteger right = new BigInteger(rhs._pts); right = right * rhs._ptsNum * lhs._ptsDen; return left >= right; } /// /// write out frame, with syncpoint and all headers /// public void WriteData(Stream dest) { dest.Write(_data, 0, _actualLength); _pool.ReleaseBuffer(_data); } } /// write a video frame to the stream /// raw video data; if length 0, write EOR /// internal error, possible A/V desync /// already written EOR public void WriteVideoFrame(int[] video) { if (_videoDone) throw new InvalidOperationException("Can't write data after end of relevance!"); if (_audioQueue.Count > 5) throw new Exception("A/V Desync?"); int dataLen = video.Length * sizeof(int); byte[] data = _bufferPool.GetBufferAtLeast(dataLen); Buffer.BlockCopy(video, 0, data, 0, dataLen); if (dataLen == 0) { _videoDone = true; } var f = new NutFrame(data, dataLen, _videoOpts, (ulong) _avParams.FpsDen, (ulong) _avParams.FpsNum, 0, _bufferPool); _bufferPool.ReleaseBuffer(data); _videoOpts++; _videoQueue.Enqueue(f); while (_audioQueue.Count > 0 && f >= _audioQueue.Peek()) { _audioQueue.Dequeue().WriteData(_output); } } /// write an audio frame to the stream /// raw audio data; if length 0, write EOR /// internal error, possible A/V desync /// already written EOR public void WriteAudioFrame(short[] samples) { if (_audioDone) { throw new Exception("Can't write audio after end of relevance!"); } if (_videoQueue.Count > 5) { throw new Exception("A/V Desync?"); } int dataLen = samples.Length * sizeof(short); byte[] data = _bufferPool.GetBufferAtLeast(dataLen); Buffer.BlockCopy(samples, 0, data, 0, dataLen); if (dataLen == 0) { _audioDone = true; } var f = new NutFrame(data, dataLen, _audioPts, 1, (ulong)_avParams.Samplerate, 1, _bufferPool); _bufferPool.ReleaseBuffer(data); _audioPts += (ulong)samples.Length / (ulong)_avParams.Channels; _audioQueue.Enqueue(f); while (_videoQueue.Count > 0 && f >= _videoQueue.Peek()) { _videoQueue.Dequeue().WriteData(_output); } } /// /// create a new NutMuxer /// /// video width /// video height /// fps numerator /// fps denominator /// audio samplerate /// audio number of channels /// Stream to write to public NutMuxer(int width, int height, int fpsNum, int fpsDen, int samplerate, int channels, Stream underlying) { _avParams = new AVParams { Width = width, Height = height, FpsNum = fpsNum, FpsDen = fpsDen }; _avParams.Reduce(); // TimeBases in nut MUST be relatively prime _avParams.Samplerate = samplerate; _avParams.Channels = channels; _output = underlying; _audioPts = 0; _videoOpts = 0; _audioQueue = new Queue(); _videoQueue = new Queue(); WriteMainHeader(); WriteVideoHeader(); WriteAudioHeader(); _videoDone = false; _audioDone = false; } /// /// finish and flush everything /// closes underlying stream!! /// public void Finish() { if (!_videoDone) { WriteVideoFrame(new int[0]); } if (!_audioDone) { WriteAudioFrame(new short[0]); } // flush any remaining queued packets while (_audioQueue.Count > 0 && _videoQueue.Count > 0) { if (_audioQueue.Peek() <= _videoQueue.Peek()) { _audioQueue.Dequeue().WriteData(_output); } else { _videoQueue.Dequeue().WriteData(_output); } } while (_audioQueue.Count > 0) { _audioQueue.Dequeue().WriteData(_output); } while (_videoQueue.Count > 0) { _videoQueue.Dequeue().WriteData(_output); } _output.Close(); _output = null; } } }