using System; using BizHawk.Common; using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Sound { /// /// A simple 1-bit (mono) beeper/buzzer implementation using blipbuffer /// Simulating the piezzo-electric buzzer found in many old computers (such as the ZX Spectrum or Amstrad CPC) /// Sound is generated by toggling the single input line ON and OFF rapidly /// public sealed class OneBitBeeper : ISoundProvider { private int _sampleRate; private int _clocksPerFrame; private int _framesPerSecond; private BlipBuffer _blip; private readonly string _beeperId; /// /// Constructor /// /// The sample rate to pass to blipbuffer (this should be 44100 for ISoundProvider) /// The number of (usually CPU) clocked cycles in one frame /// The number of frames per second (usually either 60 or 50) /// Unique name for this instance (needed for serialization as some cores have more than one active instance of the beeper) public OneBitBeeper(int blipSampleRate, int clocksPerFrame, int framesPerSecond, string beeperId) { _beeperId = beeperId; _sampleRate = blipSampleRate; _clocksPerFrame = clocksPerFrame; _framesPerSecond = framesPerSecond; _blip = new BlipBuffer(blipSampleRate / framesPerSecond); _blip.SetRates(clocksPerFrame * 50, blipSampleRate); } private int clockCounter; /// /// Option to clock the beeper every CPU clock /// public void Clock(int clocksToAdd = 1) { clockCounter += clocksToAdd; } /// /// Option to directly set the current clock position within the frame /// public void SetClock(int currentFrameClock) { clockCounter = currentFrameClock; } private bool lastPulse; /// /// Processes an incoming pulse value /// public void ProcessPulseValue(bool pulse, bool renderSound = true) { if (!renderSound) return; if (lastPulse == pulse) { // no change _blip.AddDelta((uint)clockCounter, 0); } else { if (pulse) _blip.AddDelta((uint)clockCounter, (short)(_volume)); else _blip.AddDelta((uint)clockCounter, -(short)(_volume)); lastVolume = _volume; } lastPulse = pulse; } #region Volume Handling /// /// Beeper volume /// Accepts an int 0-100 value /// public int Volume { get { return VolumeConverterOut(_volume); } set { var newVol = VolumeConverterIn(value); if (newVol != _volume) _blip.Clear(); _volume = VolumeConverterIn(value); } } private int _volume; /// /// The last used volume (used to modify blipbuffer delta values) /// private int lastVolume; /// /// 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 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)_clocksPerFrame); 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]; } clockCounter = 0; } #endregion #region State Serialization public void SyncState(Serializer ser) { ser.BeginSection("Beeper_" + _beeperId); ser.Sync(nameof(_sampleRate), ref _sampleRate); ser.Sync(nameof(_clocksPerFrame), ref _clocksPerFrame); ser.Sync(nameof(_framesPerSecond), ref _framesPerSecond); ser.Sync(nameof(clockCounter), ref clockCounter); ser.Sync(nameof(lastPulse), ref lastPulse); ser.EndSection(); } #endregion } }