303 lines
7.8 KiB
C#
303 lines
7.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using System.IO;
|
|
|
|
using BizHawk.Common.IOExtensions;
|
|
using BizHawk.Emulation.Common;
|
|
|
|
namespace BizHawk.Client.EmuHawk
|
|
{
|
|
/// <summary>
|
|
/// writes MS standard riff files containing uncompressed PCM wav data
|
|
/// supports 16 bit signed data only
|
|
/// </summary>
|
|
public class WavWriter : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// underlying file being written to
|
|
/// </summary>
|
|
private BinaryWriter _file;
|
|
|
|
/// <summary>
|
|
/// sequence of files to write to (split on 32 bit limit)
|
|
/// </summary>
|
|
private IEnumerator<Stream> _fileChain;
|
|
|
|
/// <summary>
|
|
/// samplerate in HZ
|
|
/// </summary>
|
|
private int _sampleRate;
|
|
|
|
/// <summary>
|
|
/// number of audio channels
|
|
/// </summary>
|
|
private int _numChannels;
|
|
|
|
/// <summary>
|
|
/// number of bytes of PCM data written to current file
|
|
/// </summary>
|
|
private ulong _numBytes;
|
|
|
|
/// <summary>
|
|
/// number of bytes after which a file split should be made
|
|
/// </summary>
|
|
private const ulong SplitPoint = 2 * 1000 * 1000 * 1000;
|
|
|
|
/// <summary>
|
|
/// write riff headers to current file
|
|
/// </summary>
|
|
private void WriteHeaders()
|
|
{
|
|
_file.Write(Encoding.ASCII.GetBytes("RIFF")); // ChunkID
|
|
_file.Write(0U); // ChunkSize
|
|
_file.Write(Encoding.ASCII.GetBytes("WAVE")); // Format
|
|
|
|
_file.Write(Encoding.ASCII.GetBytes("fmt ")); // SubchunkID
|
|
_file.Write(16U); // SubchunkSize
|
|
_file.Write((ushort)1U); // AudioFormat (PCM)
|
|
_file.Write((ushort)_numChannels); // NumChannels
|
|
_file.Write((uint)_sampleRate); // SampleRate
|
|
_file.Write((uint)(_sampleRate * _numChannels * 2)); // ByteRate
|
|
_file.Write((ushort)(_numChannels * 2)); // BlockAlign
|
|
_file.Write((ushort)16U); // BitsPerSample
|
|
|
|
_file.Write(Encoding.ASCII.GetBytes("data")); // SubchunkID
|
|
_file.Write(0U); // SubchunkSize
|
|
}
|
|
|
|
/// <summary>
|
|
/// seek back to beginning of file and fix header sizes (if possible)
|
|
/// </summary>
|
|
private void FinalizeHeaders()
|
|
{
|
|
if (_numBytes + 36 >= 0x100000000)
|
|
{
|
|
// passed 4G limit, nothing to be done
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_file.Seek(4, SeekOrigin.Begin);
|
|
_file.Write((uint)(36 + _numBytes));
|
|
_file.Seek(40, SeekOrigin.Begin);
|
|
_file.Write((uint)(_numBytes));
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
// unseekable; oh well
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// close current underlying stream
|
|
/// </summary>
|
|
private void CloseCurrent()
|
|
{
|
|
if (_file != null)
|
|
{
|
|
FinalizeHeaders();
|
|
_file.Close();
|
|
_file.Dispose();
|
|
}
|
|
|
|
_file = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// open a new underlying stream
|
|
/// </summary>
|
|
private void OpenCurrent(Stream next)
|
|
{
|
|
_file = new BinaryWriter(next, Encoding.ASCII);
|
|
_numBytes = 0;
|
|
WriteHeaders();
|
|
}
|
|
|
|
/// <summary>
|
|
/// write samples to file
|
|
/// </summary>
|
|
/// <param name="samples">samples to write; should contain one for each channel</param>
|
|
public void WriteSamples(short[] samples)
|
|
{
|
|
_file.Write(samples);
|
|
_numBytes += (ulong)(samples.Length * sizeof(short));
|
|
|
|
// try splitting if we can
|
|
if (_numBytes >= SplitPoint && _fileChain != null)
|
|
{
|
|
if (!_fileChain.MoveNext())
|
|
{ // out of files, just keep on writing to this one
|
|
_fileChain = null;
|
|
}
|
|
else
|
|
{
|
|
Stream next = _fileChain.Current;
|
|
CloseCurrent();
|
|
OpenCurrent(next);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
/// <summary>
|
|
/// finishes writing
|
|
/// </summary>
|
|
public void Close()
|
|
{
|
|
CloseCurrent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// checks sampling rate, number of channels for validity
|
|
/// </summary>
|
|
private void CheckArgs()
|
|
{
|
|
if (_sampleRate < 1 || _numChannels < 1)
|
|
{
|
|
throw new ArgumentException("Bad samplerate/numchannels");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// initializes WavWriter with a single output stream
|
|
/// no attempt is made to split
|
|
/// </summary>
|
|
/// <param name="s">WavWriter now owns this stream</param>
|
|
/// <param name="sampleRate">sampling rate in HZ</param>
|
|
/// <param name="numChannels">number of audio channels</param>
|
|
public WavWriter(Stream s, int sampleRate, int numChannels)
|
|
{
|
|
_sampleRate = sampleRate;
|
|
_numChannels = numChannels;
|
|
_fileChain = null;
|
|
CheckArgs();
|
|
OpenCurrent(s);
|
|
}
|
|
|
|
/// <summary>
|
|
/// initializes WavWriter with an enumeration of Streams
|
|
/// one is consumed every time 2G is hit
|
|
/// if the enumerator runs out before the audio stream does, the last file could be >2G
|
|
/// </summary>
|
|
/// <param name="ss">WavWriter now owns any of these streams that it enumerates</param>
|
|
/// <param name="sampleRate">sampling rate in HZ</param>
|
|
/// <param name="numChannels">number of audio channels</param>
|
|
/// <exception cref="ArgumentException"><paramref name="ss"/> cannot be progressed</exception>
|
|
public WavWriter(IEnumerator<Stream> ss, int sampleRate, int numChannels)
|
|
{
|
|
_sampleRate = sampleRate;
|
|
_numChannels = numChannels;
|
|
CheckArgs();
|
|
_fileChain = ss;
|
|
|
|
// advance to first
|
|
if (!_fileChain.MoveNext())
|
|
{
|
|
throw new ArgumentException("Iterator was empty!");
|
|
}
|
|
|
|
OpenCurrent(ss.Current);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// slim wrapper on WavWriter that implements IVideoWriter (discards all video!)
|
|
/// </summary>
|
|
[VideoWriter("wave", "WAV writer", "Writes a series of standard RIFF wav files containing uncompressed audio. Does not write video. Splits every 2G.")]
|
|
public class WavWriterV : IVideoWriter
|
|
{
|
|
public void SetVideoCodecToken(IDisposable token) { }
|
|
public void AddFrame(IVideoProvider source) { }
|
|
public void SetMovieParameters(int fpsNum, int fpsDen) { }
|
|
public void SetVideoParameters(int width, int height) { }
|
|
public void SetFrame(int frame) { }
|
|
|
|
public bool UsesAudio => true;
|
|
|
|
public bool UsesVideo => false;
|
|
|
|
private class WavWriterVToken : IDisposable
|
|
{
|
|
public void Dispose() { }
|
|
}
|
|
|
|
public IDisposable AcquireVideoCodecToken(System.Windows.Forms.IWin32Window hwnd)
|
|
{
|
|
// don't care
|
|
return new WavWriterVToken();
|
|
}
|
|
|
|
/// <exception cref="ArgumentOutOfRangeException"><paramref name="bits"/> is not <c>16</c></exception>
|
|
public void SetAudioParameters(int sampleRate, int channels, int bits)
|
|
{
|
|
this._sampleRate = sampleRate;
|
|
this._channels = channels;
|
|
if (bits != 16)
|
|
{
|
|
throw new ArgumentException("Only support 16bit audio!");
|
|
}
|
|
}
|
|
|
|
public void SetMetaData(string gameName, string authors, ulong lengthMs, ulong rerecords)
|
|
{
|
|
// not implemented
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_wavWriter?.Dispose();
|
|
}
|
|
|
|
private WavWriter _wavWriter;
|
|
private int _sampleRate;
|
|
private int _channels;
|
|
|
|
/// <summary>
|
|
/// create a simple wav stream iterator
|
|
/// </summary>
|
|
private static IEnumerator<Stream> CreateStreamIterator(string template)
|
|
{
|
|
string dir = Path.GetDirectoryName(template) ?? "";
|
|
string baseName = Path.GetFileNameWithoutExtension(template) ?? "";
|
|
string ext = Path.GetExtension(template);
|
|
yield return new FileStream(template, FileMode.Create);
|
|
int counter = 1;
|
|
while (true)
|
|
{
|
|
yield return new FileStream($"{Path.Combine(dir, baseName)}_{counter}{ext}", FileMode.Create);
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
public void OpenFile(string baseName)
|
|
{
|
|
_wavWriter = new WavWriter(CreateStreamIterator(baseName), _sampleRate, _channels);
|
|
}
|
|
|
|
public void CloseFile()
|
|
{
|
|
_wavWriter.Close();
|
|
_wavWriter.Dispose();
|
|
_wavWriter = null;
|
|
}
|
|
|
|
public void AddSamples(short[] samples)
|
|
{
|
|
_wavWriter.WriteSamples(samples);
|
|
}
|
|
|
|
public string DesiredExtension() => "wav";
|
|
|
|
public void SetDefaultVideoCodecToken()
|
|
{
|
|
// don't use codec tokens, so don't care
|
|
}
|
|
}
|
|
}
|