using BizHawk.Common; using BizHawk.Emulation.Common; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { /// /// Logical Beeper class /// Represents the piezoelectric buzzer used in the Spectrum to produce sound /// The beeper is controlled by rapidly toggling bit 4 of port &FE /// /// It is instantiated twice, once for speccy beeper output, and once tape buzzer emulation /// /// This implementation uses BlipBuffer and should *always* output at 44100 with 882 samples per frame /// (so that it can be mixed easily further down the line) /// public class Beeper : ISoundProvider, IBeeperDevice { #region Fields and Properties /// /// Sample Rate /// This usually has to be 44100 for ISoundProvider /// private int _sampleRate; public int SampleRate { get { return _sampleRate; } set { _sampleRate = value; } } /// /// Buzzer volume /// Accepts an int 0-100 value /// private int _volume; public int Volume { get { return VolumeConverterOut(_volume); } set { var newVol = VolumeConverterIn(value); if (newVol != _volume) blip.Clear(); _volume = VolumeConverterIn(value); } } /// /// The last used volume (used to modify blipbuffer delta values) /// private int lastVolume; /// /// The number of cpu cycles per frame /// private long _tStatesPerFrame; /// /// The parent emulated machine /// private SpectrumBase _machine; /// /// The last pulse /// private bool LastPulse; /// /// The last T-State (cpu cycle) that the last pulse was received /// private long LastPulseTState; /// /// Device blipbuffer /// private readonly BlipBuffer blip = new BlipBuffer(882); #endregion #region Private Methods /// /// Takes an int 0-100 and returns the relevant short volume to output /// /// /// private int VolumeConverterIn(int vol) { int maxLimit = short.MaxValue / 3; int increment = maxLimit / 100; return vol * increment; } /// /// Takes an short volume and returns the relevant int value 0-100 /// /// /// private int VolumeConverterOut(int shortvol) { int maxLimit = short.MaxValue / 3; int increment = maxLimit / 100; if (shortvol > maxLimit) shortvol = maxLimit; return shortvol / increment; } #endregion #region Construction & Initialisation public Beeper(SpectrumBase machine) { _machine = machine; } /// /// Initialises the beeper /// public void Init(int sampleRate, int tStatesPerFrame) { blip.SetRates((tStatesPerFrame * 50), sampleRate); _sampleRate = sampleRate; _tStatesPerFrame = tStatesPerFrame; } #endregion #region IBeeperDevice /// /// Processes an incoming pulse value and adds it to the blipbuffer /// /// public void ProcessPulseValue(bool pulse) { if (!_machine._renderSound) return; if (LastPulse == pulse) { // no change blip.AddDelta((uint)_machine.CurrentFrameCycle, 0); } else { if (pulse) blip.AddDelta((uint)_machine.CurrentFrameCycle, (short)(_volume)); else blip.AddDelta((uint)_machine.CurrentFrameCycle, -(short)(_volume)); lastVolume = _volume; } LastPulse = pulse; } public void StartFrame() { } public void EndFrame() { } #endregion #region ISoundProvider public bool CanProvideAsync => false; public SyncSoundMode SyncMode => SyncSoundMode.Sync; public void SetSyncMode(SyncSoundMode mode) { if (mode != SyncSoundMode.Sync) throw new InvalidOperationException("Only Sync mode is supported."); } public void GetSamplesAsync(short[] samples) { throw new NotSupportedException("Async is not available"); } public void DiscardSamples() { blip.Clear(); } public void GetSamplesSync(out short[] samples, out int nsamp) { blip.EndFrame((uint)_tStatesPerFrame); nsamp = blip.SamplesAvailable(); samples = new short[nsamp * 2]; blip.ReadSamples(samples, nsamp, true); for (int i = 0; i < nsamp * 2; i += 2) { samples[i + 1] = samples[i]; } } #endregion #region State Serialization public void SyncState(Serializer ser) { ser.BeginSection("Buzzer"); ser.Sync("_tStatesPerFrame", ref _tStatesPerFrame); ser.Sync("_sampleRate", ref _sampleRate); ser.Sync("LastPulse", ref LastPulse); ser.Sync("LastPulseTState", ref LastPulseTState); ser.EndSection(); } #endregion } }