1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-01-24 19:31:26 +00:00

236 lines
7.9 KiB
C#

/*
This file is part of ContrAlto.
ContrAlto is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ContrAlto is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with ContrAlto. If not, see <http://www.gnu.org/licenses/>.
*/
using Contralto.CPU;
using Contralto.Logging;
using Contralto.Memory;
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
namespace Contralto.IO
{
/// <summary>
/// Implements the hardware for the Audio DAC used by Ted Kaehler's
/// ST-74 Music System (either the FM-synthesis "TWANG" system or the
/// Sampling system.)
/// </summary>
public class AudioDAC : IMemoryMappedDevice
{
public AudioDAC(AltoSystem system)
{
_system = system;
_dacOutput = new Queue<ushort>(16384);
}
public void Shutdown()
{
if (_waveOut != null)
{
_waveOut.Stop();
_waveOut.Dispose();
}
if (_waveFile != null)
{
_waveFile.Close();
}
}
/// <summary>
/// Comments in the FM synthesis microcode indicate:
/// "240 SAMPLES = 18 msec"
/// Which works out to about 13.3Khz.
///
/// Unsure if this value also applies to the Sampling microcode, but
/// it sounds about right in action.
/// </summary>
public static readonly int AudioDACSamplingRate = 13000;
/// <summary>
/// Reads a word from the specified address.
/// </summary>
/// <param name="address"></param>
/// <param name="extendedMemory"></param>
/// <returns></returns>
public ushort Read(int address, TaskType task, bool extendedMemory)
{
// The DAC is, as far as I can tell, write-only.
return 0;
}
/// <summary>
/// Writes a word to the specified address.
/// </summary>
/// <param name="address"></param>
/// <param name="data"></param>
public void Load(int address, ushort data, TaskType task, bool extendedMemory)
{
//
// This is only supported on Windows platforms at this time.
//
if (Configuration.EnableAudioDAC &&
Configuration.Platform == PlatformType.Windows)
{
// Ensure we have a sink for audio output capture if so configured.
if (Configuration.EnableAudioDACCapture && _waveFile == null)
{
string outputFile = Path.Combine(
Configuration.AudioDACCapturePath,
string.Format("AltoAudio-{0}.wav", DateTime.Now.ToString("yyyyMMdd-hhmmss")));
try
{
_waveFile = new WaveFileWriter(outputFile, new WaveFormat(AudioDAC.AudioDACSamplingRate, 1));
}
catch (Exception e)
{
Log.Write(LogType.Error,
LogComponent.DAC,
"Failed to create DAC output file {0}. Error: {1}", outputFile, e.Message);
}
}
// Ensure we have something to generate audio output with.
if (_waveOut == null)
{
_waveOut = new WaveOut();
_dacLock = new ReaderWriterLockSlim();
_waveOut.Init(new DACOutputWaveProvider(_dacOutput, _waveFile, _dacLock));
_waveOut.Play();
}
//
// Enter the Write lock to ensure consistency with the
// consumer (the DACOutputWaveProvider).
//
_dacLock.EnterWriteLock();
_dacOutput.Enqueue(data);
_dacLock.ExitWriteLock();
}
}
/// <summary>
/// Specifies the range (or ranges) of addresses decoded by this device.
/// </summary>
public MemoryRange[] Addresses
{
get { return _addresses; }
}
/// <summary>
/// From: http://bitsavers.org/pdf/xerox/alto/memos_1975/Reserved_Alto_Memory_Locations_Jan75.pdf
///
/// #177776: Digital-Analog Converter (DAC Hardware - Kaehler)
/// </summary>
private readonly MemoryRange[] _addresses =
{
new MemoryRange(0xfffe, 0xfffe),
};
private Queue<ushort> _dacOutput;
private ReaderWriterLockSlim _dacLock;
private AltoSystem _system;
private WaveOut _waveOut;
private WaveFileWriter _waveFile;
}
//
// Basic implementation of the NAudio WaveProvider32 class.
// on a Read, we fill the DAC with the samples the Alto has been generated.
// If it hasn't generated enough (it's fallen behind) we'll pad it to try
// and reduce popping/stuttering.
//
// It will also flush the output to a WaveFileWriter if it has been enabled in
// the configuration.
//
public class DACOutputWaveProvider : WaveProvider32
{
public DACOutputWaveProvider(Queue<ushort> dacOutput, WaveFileWriter waveFile, ReaderWriterLockSlim dacLock)
{
_dacOutput = dacOutput;
_waveFile = waveFile;
_dacLock = dacLock;
SetWaveFormat(AudioDAC.AudioDACSamplingRate, 1);
_lastSample = 0;
}
public override int Read(float[] buffer, int offset, int sampleCount)
{
int outSamples = 0;
_dacLock.EnterReadLock();
while(_dacOutput.Count > 0 && outSamples < sampleCount)
{
float sample = (float)_dacOutput.Dequeue() / 32768.0f - 1.0f;
buffer[offset + outSamples] = sample;
outSamples++;
_lastSample = sample;
}
_dacLock.ExitReadLock();
// Commit the Alto-generated samples to disk if we're saving them.
if (_waveFile != null)
{
_waveFile.WriteSamples(buffer, offset, outSamples);
}
//
// If we didn't have enough samples to fill the requested buffer,
// This means the Alto has fallen behind; pad the remaining buffer with the
// last written sample.
//
for (; outSamples < sampleCount; outSamples++)
{
buffer[offset + outSamples] = _lastSample;
}
return sampleCount;
}
/// <summary>
/// Queue containing the samples generated by the Alto.
/// We pull them off as WaveOut requests audio data.
/// </summary>
private Queue<ushort> _dacOutput;
/// <summary>
/// The last sample written. Used to pad the buffer if
/// the Alto falls behind.
/// </summary>
private float _lastSample;
/// <summary>
/// Used to ensure thread-safety of the _dacOutput queue
/// between the Alto emulation and the WaveOut thread.
/// </summary>
private ReaderWriterLockSlim _dacLock;
/// <summary>
/// Used to write the output to a WAV file, if the option
/// is enabled.
/// </summary>
private WaveFileWriter _waveFile;
}
}