From f82b1b83362d68b5ba81788da3e48679c8548afc Mon Sep 17 00:00:00 2001 From: Asnivor Date: Tue, 5 Dec 2017 13:08:47 +0000 Subject: [PATCH] Custom SoundProviderMixer implementation --- .../BizHawk.Emulation.Cores.csproj | 3 + .../SinclairSpectrum/Hardware/AYSound.cs | 407 ++++++++++++++++++ .../SinclairSpectrum/Machine/SpectrumBase.cs | 10 +- .../Machine/ZXSpectrum128K/ZX128.cs | 2 + .../SinclairSpectrum/SoundProviderMixer.cs | 196 +++++++++ .../ZXSpectrum.ISoundProvider.cs | 16 + .../Computers/SinclairSpectrum/ZXSpectrum.cs | 7 +- 7 files changed, 639 insertions(+), 2 deletions(-) create mode 100644 BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/AYSound.cs create mode 100644 BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs create mode 100644 BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISoundProvider.cs diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index 439b004828..8db1b196d4 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -256,6 +256,7 @@ + @@ -270,6 +271,7 @@ + @@ -307,6 +309,7 @@ + diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/AYSound.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/AYSound.cs new file mode 100644 index 0000000000..3cd2c513dd --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/AYSound.cs @@ -0,0 +1,407 @@ +using System; + +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public class AYSound : ISoundProvider + { + private readonly BlipBuffer _blip = new BlipBuffer(4096); + private short[] _sampleBuffer = new short[0]; + + public AYSound() + { + _blip.SetRates(894866 / 4.0, 44100); + } + + public ushort[] Register = new ushort[16]; + + public int total_clock; // TODO: what is this used for? + + public void Reset() + { + clock_A = clock_B = clock_C = 0x1000; + noise_clock = 0x20; + + for (int i = 0; i < 16; i++) + { + Register[i] = 0x0000; + } + sync_psg_state(); + DiscardSamples(); + } + + public void DiscardSamples() + { + _blip.Clear(); + _sampleClock = 0; + } + + public void GetSamplesAsync(short[] samples) + { + throw new NotSupportedException("Async is not available"); + } + + 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 GetSamplesSync(out short[] samples, out int nsamp) + { + _blip.EndFrame((uint)_sampleClock); + _sampleClock = 0; + + nsamp = _blip.SamplesAvailable(); + int targetLength = nsamp * 2; + if (_sampleBuffer.Length != targetLength) + { + _sampleBuffer = new short[targetLength]; + } + + _blip.ReadSamplesLeft(_sampleBuffer, nsamp); + for (int i = 0; i < _sampleBuffer.Length; i += 2) + { + _sampleBuffer[i + 1] = _sampleBuffer[i]; + } + + samples = _sampleBuffer; + } + + public void GetSamples(short[] samples) + { + throw new Exception(); + } + + private static readonly int[] VolumeTable = + { + 0x0000, 0x0055, 0x0079, 0x00AB, 0x00F1, 0x0155, 0x01E3, 0x02AA, + 0x03C5, 0x0555, 0x078B, 0x0AAB, 0x0F16, 0x1555, 0x1E2B, 0x2AAA + }; + + private int _sampleClock; + private int _latchedSample; + + private int TotalExecutedCycles; + private int PendingCycles; + private int psg_clock; + private int sq_per_A, sq_per_B, sq_per_C; + private int clock_A, clock_B, clock_C; + private int vol_A, vol_B, vol_C; + private bool A_on, B_on, C_on; + private bool A_up, B_up, C_up; + private bool A_noise, B_noise, C_noise; + + private int env_per; + private int env_clock; + private int env_shape; + private int env_E; + private int E_up_down; + private int env_vol_A, env_vol_B, env_vol_C; + + private int noise_clock; + private int noise_per; + private int noise = 0x1; + + public Func ReadMemory; + public Func WriteMemory; + + public void SyncState(Serializer ser) + { + ser.BeginSection("PSG"); + + ser.Sync("Register", ref Register, false); + ser.Sync("Toal_executed_cycles", ref TotalExecutedCycles); + ser.Sync("Pending_Cycles", ref PendingCycles); + + ser.Sync("psg_clock", ref psg_clock); + ser.Sync("clock_A", ref clock_A); + ser.Sync("clock_B", ref clock_B); + ser.Sync("clock_C", ref clock_C); + ser.Sync("noise_clock", ref noise_clock); + ser.Sync("env_clock", ref env_clock); + ser.Sync("A_up", ref A_up); + ser.Sync("B_up", ref B_up); + ser.Sync("C_up", ref C_up); + ser.Sync("noise", ref noise); + ser.Sync("env_E", ref env_E); + ser.Sync("E_up_down", ref E_up_down); + + sync_psg_state(); + + ser.EndSection(); + } + + public ushort? ReadPSG(ushort addr, bool peek) + { + if (addr >= 0x01F0 && addr <= 0x01FF) + { + return (ushort)(Register[addr - 0x01F0]); + } + + return null; + } + + private void sync_psg_state() + { + sq_per_A = (Register[0] & 0xFF) | (((Register[4] & 0xF) << 8)); + if (sq_per_A == 0) + { + sq_per_A = 0x1000; + } + + sq_per_B = (Register[1] & 0xFF) | (((Register[5] & 0xF) << 8)); + if (sq_per_B == 0) + { + sq_per_B = 0x1000; + } + + sq_per_C = (Register[2] & 0xFF) | (((Register[6] & 0xF) << 8)); + if (sq_per_C == 0) + { + sq_per_C = 0x1000; + } + + env_per = (Register[3] & 0xFF) | (((Register[7] & 0xFF) << 8)); + if (env_per == 0) + { + env_per = 0x10000; + } + + env_per *= 2; + + A_on = Register[8].Bit(0); + B_on = Register[8].Bit(1); + C_on = Register[8].Bit(2); + A_noise = Register[8].Bit(3); + B_noise = Register[8].Bit(4); + C_noise = Register[8].Bit(5); + + noise_per = Register[9] & 0x1F; + if (noise_per == 0) + { + noise_per = 0x20; + } + + var shape_select = Register[10] & 0xF; + + if (shape_select < 4) + env_shape = 0; + else if (shape_select < 8) + env_shape = 1; + else + env_shape = 2 + (shape_select - 8); + + vol_A = Register[11] & 0xF; + env_vol_A = (Register[11] >> 4) & 0x3; + + vol_B = Register[12] & 0xF; + env_vol_B = (Register[12] >> 4) & 0x3; + + vol_C = Register[13] & 0xF; + env_vol_C = (Register[13] >> 4) & 0x3; + } + + public bool WritePSG(ushort addr, ushort value, bool poke) + { + if (addr >= 0x01F0 && addr <= 0x01FF) + { + var reg = addr - 0x01F0; + + value &= 0xFF; + + if (reg == 4 || reg == 5 || reg == 6 || reg == 10) + value &= 0xF; + + if (reg == 9) + value &= 0x1F; + + if (reg == 11 || reg == 12 || reg == 13) + value &= 0x3F; + + Register[addr - 0x01F0] = value; + + sync_psg_state(); + + if (reg == 10) + { + env_clock = env_per; + + if (env_shape == 0 || env_shape == 2 || env_shape == 3 || env_shape == 4 || env_shape == 5) + { + env_E = 15; + E_up_down = -1; + } + else + { + env_E = 0; + E_up_down = 1; + } + } + + return true; + } + + return false; + } + + public void generate_sound(int cycles_to_do) + { + // there are 4 cpu cycles for every psg cycle + bool sound_out_A; + bool sound_out_B; + bool sound_out_C; + + for (int i = 0; i < cycles_to_do; i++) + { + psg_clock++; + + if (psg_clock == 4) + { + psg_clock = 0; + + total_clock++; + + clock_A--; + clock_B--; + clock_C--; + + noise_clock--; + env_clock--; + + // clock noise + if (noise_clock == 0) + { + noise = (noise >> 1) ^ (noise.Bit(0) ? 0x10004 : 0); + noise_clock = noise_per; + } + + if (env_clock == 0) + { + env_clock = env_per; + + env_E += E_up_down; + + if (env_E == 16 || env_E == -1) + { + + // we just completed a period of the envelope, determine what to do now based on the envelope shape + if (env_shape == 0 || env_shape == 1 || env_shape == 3 || env_shape == 9) + { + E_up_down = 0; + env_E = 0; + } + else if (env_shape == 5 || env_shape == 7) + { + E_up_down = 0; + env_E = 15; + } + else if (env_shape == 4 || env_shape == 8) + { + if (env_E == 16) + { + env_E = 15; + E_up_down = -1; + } + else + { + env_E = 0; + E_up_down = 1; + } + } + else if (env_shape == 2) + { + env_E = 15; + } + else + { + env_E = 0; + } + } + } + + if (clock_A == 0) + { + A_up = !A_up; + clock_A = sq_per_A; + } + + if (clock_B == 0) + { + B_up = !B_up; + clock_B = sq_per_B; + } + + if (clock_C == 0) + { + C_up = !C_up; + clock_C = sq_per_C; + } + + + sound_out_A = (noise.Bit(0) | A_noise) & (A_on | A_up); + sound_out_B = (noise.Bit(0) | B_noise) & (B_on | B_up); + sound_out_C = (noise.Bit(0) | C_noise) & (C_on | C_up); + + // now calculate the volume of each channel and add them together + int v; + + if (env_vol_A == 0) + { + v = (short)(sound_out_A ? VolumeTable[vol_A] : 0); + } + else + { + int shift_A = 3 - env_vol_A; + if (shift_A < 0) + shift_A = 0; + v = (short)(sound_out_A ? (VolumeTable[env_E] >> shift_A) : 0); + } + + if (env_vol_B == 0) + { + v += (short)(sound_out_B ? VolumeTable[vol_B] : 0); + + } + else + { + int shift_B = 3 - env_vol_B; + if (shift_B < 0) + shift_B = 0; + v += (short)(sound_out_B ? (VolumeTable[env_E] >> shift_B) : 0); + } + + if (env_vol_C == 0) + { + v += (short)(sound_out_C ? VolumeTable[vol_C] : 0); + } + else + { + int shift_C = 3 - env_vol_C; + if (shift_C < 0) + shift_C = 0; + v += (short)(sound_out_C ? (VolumeTable[env_E] >> shift_C) : 0); + } + + if (v != _latchedSample) + { + _blip.AddDelta((uint)_sampleClock, v - _latchedSample); + _latchedSample = v; + } + + _sampleClock++; + } + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs index cb1c9064f9..f23173a259 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs @@ -37,6 +37,11 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// public Buzzer BuzzerDevice { get; set; } + /// + /// Device representing the AY-3-8912 chip found in the 128k and up spectrums + /// + public AYSound AYDevice { get; set; } + /// /// The spectrum keyboard /// @@ -231,9 +236,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum BuzzerDevice.SyncState(ser); TapeDevice.SyncState(ser); + if (AYDevice != null) + AYDevice.SyncState(ser); + ser.EndSection(); - ReInitMemory(); + //ReInitMemory(); } } } diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs index 93b7b58f35..fe3f08d651 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs @@ -42,6 +42,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum BuzzerDevice = new Buzzer(this); BuzzerDevice.Init(44100, UlaFrameCycleCount); + AYDevice = new AYSound(); + KeyboardDevice = new Keyboard48(this); KempstonDevice = new KempstonJoystick(this); diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs new file mode 100644 index 0000000000..57e37f3d77 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs @@ -0,0 +1,196 @@ +using BizHawk.Emulation.Common; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// My attempt at mixing multiple ISoundProvider sources together and outputting another ISoundProvider + /// Currently only supports SyncSoundMode.Sync + /// Attached ISoundProvider sources must already be stereo 44.1khz + /// + internal sealed class SoundProviderMixer : ISoundProvider + { + private class Provider + { + public ISoundProvider SoundProvider { get; set; } + public int MaxVolume { get; set; } + public short[] Buffer { get; set; } + public int NSamp { get; set; } + } + + private readonly List SoundProviders; + + private short[] _buffer; + private int _nSamp; + + public SoundProviderMixer(params ISoundProvider[] soundProviders) + { + SoundProviders = new List(); + + foreach (var s in soundProviders) + { + SoundProviders.Add(new Provider + { + SoundProvider = s, + MaxVolume = short.MaxValue, + }); + } + + EqualizeVolumes(); + } + + public void AddSource(ISoundProvider source) + { + SoundProviders.Add(new Provider + { + SoundProvider = source, + MaxVolume = short.MaxValue + }); + + EqualizeVolumes(); + } + + public void DisableSource(ISoundProvider source) + { + var sp = SoundProviders.Where(a => a.SoundProvider == source); + if (sp.Count() == 1) + SoundProviders.Remove(sp.First()); + else if (sp.Count() > 1) + foreach (var s in sp) + SoundProviders.Remove(s); + } + + public void EqualizeVolumes() + { + if (SoundProviders.Count < 1) + return; + + int eachVolume = short.MaxValue / SoundProviders.Count; + foreach (var source in SoundProviders) + { + source.MaxVolume = eachVolume; + } + } + + #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() + { + foreach (var soundSource in SoundProviders) + { + soundSource.SoundProvider.DiscardSamples(); + } + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + samples = null; + nsamp = 0; + + // get samples from all the providers + foreach (var sp in SoundProviders) + { + int sampCount; + short[] samp; + sp.SoundProvider.GetSamplesSync(out samp, out sampCount); + sp.NSamp = sampCount; + sp.Buffer = samp; + } + + // are all the sample lengths the same? + var firstEntry = SoundProviders.First(); + bool sameCount = SoundProviders.All(s => s.NSamp == firstEntry.NSamp); + + if (!sameCount) + { + int divisor = 1; + int highestCount = 0; + + // get the lowest divisor of all the soundprovider nsamps + for (int d = 2; d < 999; d++) + { + bool divFound = false; + foreach (var sp in SoundProviders) + { + if (sp.NSamp > highestCount) + highestCount = sp.NSamp; + + if (sp.NSamp % d == 0) + divFound = true; + else + divFound = false; + } + + if (divFound) + { + divisor = d; + break; + } + } + + // now we have the largest current number of samples among the providers + // along with a common divisor for all of them + nsamp = highestCount * divisor; + samples = new short[nsamp * 2]; + + // take a pass at populating the samples array for each provider + foreach (var sp in SoundProviders) + { + short sectorVal = 0; + int pos = 0; + for (int i = 0; i < sp.Buffer.Length; i++) + { + if (sp.Buffer[i] > sp.MaxVolume) + sectorVal = (short)sp.MaxVolume; + else + sectorVal = sp.Buffer[i]; + + for (int s = 0; s < divisor; s++) + { + samples[pos++] += sectorVal; + } + } + } + } + else + { + nsamp = firstEntry.NSamp; + samples = new short[nsamp * 2]; + + for (int i = 0; i < samples.Length; i++) + { + short sectorVal = 0; + foreach (var sp in SoundProviders) + { + if (sp.Buffer[i] > sp.MaxVolume) + sectorVal += (short)sp.MaxVolume; + else + sectorVal += sp.Buffer[i]; + } + + samples[i] = sectorVal; + } + } + } + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISoundProvider.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISoundProvider.cs new file mode 100644 index 0000000000..02f144350c --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISoundProvider.cs @@ -0,0 +1,16 @@ +using BizHawk.Emulation.Cores.Components; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum + { + private FakeSyncSound _fakeSyncSound; + private IAsyncSoundProvider ActiveSoundProvider; + private SoundProviderMixer SoundMixer; + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs index 64ddcde13a..15052083da 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs @@ -69,7 +69,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum ser.Register(_tracer); ser.Register(_cpu); ser.Register(_machine); - ser.Register(_machine.BuzzerDevice); + + SoundMixer = new SoundProviderMixer(_machine.BuzzerDevice); + if (_machine.AYDevice != null) + SoundMixer.AddSource(_machine.AYDevice); + + ser.Register(SoundMixer); HardReset();