New AY-3-8912 implementation. Better sounding and more performant

This commit is contained in:
Asnivor 2018-03-15 16:32:26 +00:00
parent 8234b2acfa
commit 7a36f913ec
16 changed files with 1020 additions and 973 deletions

View File

@ -263,7 +263,7 @@
<Compile Include="Computers\SinclairSpectrum\Hardware\Input\SinclairJoystick2.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Input\SinclairJoystick1.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Input\NullJoystick.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\SoundOuput\AY38912.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\SoundOuput\AYChip.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\SoundOuput\Buzzer.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Datacorder\DatacorderDevice.cs" />
<Compile Include="Computers\SinclairSpectrum\Hardware\Abstraction\IKeyboard.cs" />

View File

@ -11,7 +11,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// <summary>
/// Represents a PSG device (in this case an AY-3-891x)
/// </summary>
public interface IPSG : ISoundProvider
public interface IPSG : ISoundProvider, IPortIODevice
{
/// <summary>
/// Initlization routine
@ -24,7 +24,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// Activates a register
/// </summary>
int SelectedRegister { get; set; }
/// <summary>
/// Writes to the PSG
/// </summary>
@ -35,6 +35,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// Reads from the PSG
/// </summary>
int PortRead();
/// <summary>
/// Resets the PSG

View File

@ -158,6 +158,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
if (_tapeIsPlaying)
return;
_machine.BuzzerDevice.SetTapeMode(true);
_machine.Spectrum.OSD_TapePlaying();
// update the lastCycle
@ -210,6 +212,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
if (!_tapeIsPlaying)
return;
_machine.BuzzerDevice.SetTapeMode(false);
_machine.Spectrum.OSD_TapeStopped();
// sign that the tape is no longer playing

View File

@ -339,6 +339,9 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
(for instance by XOR A/IN A,(FE)) is one, no key is pressed
*/
if ((port & 0x0001) != 0)
return false;
if ((port & 0x8000) == 0)
{
result &= KeyLine[7];

View File

@ -1,718 +0,0 @@

using BizHawk.Common;
using BizHawk.Emulation.Common;
using System;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
public class AY38912 : IPSG
{
private int _tStatesPerFrame;
private int _sampleRate;
private int _samplesPerFrame;
private int _tStatesPerSample;
private int _sampleCounter;
const int AY_SAMPLE_RATE = 16;
private int _AYCyclesPerFrame;
private int _nsamp;
private int _AYCount;
/// <summary>
/// The final sample buffer
/// </summary>
private short[] _samples = new short[0];
/// <summary>
/// Number of samples in one frame
/// </summary>
public int SamplesPerFrame
{
get { return _samplesPerFrame; }
set { _samplesPerFrame = value; }
}
/// <summary>
/// Number of TStates in each sample
/// </summary>
public int TStatesPerSample
{
get { return _tStatesPerSample; }
set { _tStatesPerSample = value; }
}
#region Construction & Initialisation
public AY38912()
{
Reset();
}
/// <summary>
/// Initialises the AY chip
/// </summary>
public void Init(int sampleRate, int tStatesPerFrame)
{
_sampleRate = sampleRate;
_tStatesPerFrame = tStatesPerFrame;
_tStatesPerSample = 79;
_samplesPerFrame = _tStatesPerFrame / _tStatesPerSample;
_AYCyclesPerFrame = _tStatesPerFrame / AY_SAMPLE_RATE;
_samples = new short[_samplesPerFrame * 2];
_nsamp = _samplesPerFrame;
}
#endregion
public void UpdateSound(int currentFrameCycle)
{
for (int i = 0; i < (currentFrameCycle / AY_SAMPLE_RATE) - _AYCount; i++)
{
Update();
SampleAY();
_AYCount++;
}
// calculate how many samples must be processed
int samplesToGenerate = (currentFrameCycle / _tStatesPerSample) - (_sampleCounter / 2);
// begin generation
if (samplesToGenerate > 0)
{
// ensure the required resolution
while (soundSampleCounter < 4)
{
SampleAY();
}
EndSampleAY();
// generate needed samples
for (int i = 0; i < samplesToGenerate; i++)
{
_samples[_sampleCounter++] = (short)(averagedChannelSamples[0]);
_samples[_sampleCounter++] = (short)(averagedChannelSamples[1]);
samplesToGenerate--;
}
averagedChannelSamples[0] = 0;
averagedChannelSamples[1] = 0;
averagedChannelSamples[2] = 0;
}
}
public void StartFrame()
{
_AYCount = 0;
// the stereo _samples buffer should already have been processed as a part of
// ISoundProvider at the end of the last frame
_sampleCounter = 0;
}
public void EndFrame()
{
}
public void Reset()
{
// reset volumes
for (int i = 0; i < 16; i++)
AY_SpecVolumes[i] = (short)(AY_Volumes[i] * 8191);
soundSampleCounter = 0;
regs[AY_NOISEPER] = 0xFF;
noiseOut = 0x01;
envelopeVolume = 0;
noiseCount = 0;
// reset state of all channels
for (int f = 0; f < 3; f++)
{
channel_count[f] = 0;
channel_mix[f] = 0;
channel_out[f] = 0;
averagedChannelSamples[f] = 0;
}
envelopeCount = 0;
randomSeed = 1;
selectedRegister = 0;
}
#region IStatable
public void SyncState(Serializer ser)
{
ser.BeginSection("AY38912");
ser.Sync("_tStatesPerFrame", ref _tStatesPerFrame);
ser.Sync("_sampleRate", ref _sampleRate);
ser.Sync("_samplesPerFrame", ref _samplesPerFrame);
ser.Sync("_tStatesPerSample", ref _tStatesPerSample);
ser.Sync("_sampleCounter", ref _sampleCounter);
ser.Sync("ChannelLeft", ref ChannelLeft);
ser.Sync("ChannelRight", ref ChannelRight);
ser.Sync("ChannelCenter", ref ChannelCenter);
ser.Sync("Regs", ref regs, false);
ser.Sync("NoiseOut", ref noiseOut);
ser.Sync("envelopeVolume", ref envelopeVolume);
ser.Sync("noiseCount", ref noiseCount);
ser.Sync("envelopeCount", ref envelopeCount);
ser.Sync("randomSeed", ref randomSeed);
ser.Sync("envelopeClock", ref envelopeClock);
ser.Sync("selectedRegister", ref selectedRegister);
ser.Sync("soundSampleCounter", ref soundSampleCounter);
ser.Sync("stereoSound", ref stereoSound);
ser.Sync("sustaining", ref sustaining);
ser.Sync("sustain", ref sustain);
ser.Sync("alternate", ref alternate);
ser.Sync("attack", ref attack);
ser.Sync("envelopeStep", ref envelopeStep);
ser.Sync("channel_out", ref channel_out, false);
ser.Sync("channel_count", ref channel_count, false);
ser.Sync("averagedChannelSamples", ref averagedChannelSamples, false);
ser.Sync("channel_mix", ref channel_mix, false);
ser.Sync("AY_SpecVolumes", ref AY_SpecVolumes, false);
ser.Sync("_samples", ref _samples, false);
ser.Sync("_nsamp", ref _nsamp);
ser.EndSection();
}
#endregion
#region AY Sound Implementation
/*
Based on the AYSound class from ArjunNair's Zero-Emulator
https://github.com/ArjunNair/Zero-Emulator/
*MIT LICENSED*
The MIT License (MIT)
Copyright (c) 2009 Arjun Nair
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/// <summary>
/// Register constants
/// </summary>
private const byte AY_A_FINE = 0;
private const byte AY_A_COARSE = 1;
private const byte AY_B_FINE = 2;
private const byte AY_B_COARSE = 3;
private const byte AY_C_FINE = 4;
private const byte AY_C_COARSE = 5;
private const byte AY_NOISEPER = 6;
private const byte AY_ENABLE = 7;
private const byte AY_A_VOL = 8;
private const byte AY_B_VOL = 9;
private const byte AY_C_VOL = 10;
private const byte AY_E_FINE = 11;
private const byte AY_E_COARSE = 12;
private const byte AY_E_SHAPE = 13;
private const byte AY_PORT_A = 14;
private const byte AY_PORT_B = 15;
/// <summary>
/// Channels
/// </summary>
internal enum Channel
{
A, B, C
}
/// <summary>
/// ACB configuration
/// </summary>
private int ChannelLeft = 0;
private int ChannelRight = 1; //2 if ABC
private int ChannelCenter = 2; //1 if ABC
/// <summary>
/// Register storage
/// </summary>
private int[] regs = new int[16];
/// <summary>
/// State
/// </summary>
private int noiseOut;
private int envelopeVolume;
private int noiseCount;
private int envelopeCount;
private ulong randomSeed;
private byte envelopeClock = 0;
private int selectedRegister;
public ushort soundSampleCounter;
private bool stereoSound = false;
private bool sustaining;
private bool sustain;
private bool alternate;
private int attack;
private int envelopeStep;
/// <summary>
/// Buffer arrays
/// </summary>
private int[] channel_out = new int[3];
private int[] channel_count = new int[3];
private int[] averagedChannelSamples = new int[3];
private short[] channel_mix = new short[3];
/// <summary>
/// Measurements from comp.sys.sinclair (2001 Matthew Westcott)
/// </summary>
private float[] AY_Volumes =
{
0.0000f, 0.0137f, 0.0205f, 0.0291f,
0.0423f, 0.0618f, 0.0847f, 0.1369f,
0.1691f, 0.2647f, 0.3527f, 0.4499f,
0.5704f, 0.6873f, 0.8482f, 1.0000f
};
/// <summary>
/// Volume storage (short)
/// </summary>
private short[] AY_SpecVolumes = new short[16];
/// <summary>
/// Sets the ACB configuration
/// </summary>
/// <param name="val"></param>
public void SetSpeakerACB(bool val)
{
// ACB
if (val)
{
ChannelCenter = 2;
ChannelRight = 1;
}
// ABC
else
{
ChannelCenter = 1;
ChannelRight = 2;
}
}
/// <summary>
/// Utility method to set all registers externally
/// </summary>
/// <param name="_regs"></param>
public void SetRegisters(byte[] _regs)
{
for (int f = 0; f < 16; f++)
regs[f] = _regs[f];
}
/// <summary>
/// Utility method to get all registers externally
/// </summary>
/// <returns></returns>
public byte[] GetRegisters()
{
byte[] newArray = new byte[16];
for (int f = 0; f < 16; f++)
newArray[f] = (byte)(regs[f] & 0xff);
return newArray;
}
/// <summary>
/// Selected Register property
/// </summary>
public int SelectedRegister
{
get { return selectedRegister; }
set { if (value < 16) selectedRegister = value; }
}
/// <summary>
/// Simulates a port write to the AY chip
/// </summary>
/// <param name="val"></param>
public void PortWrite(int val)
{
switch (SelectedRegister)
{
// not implemented / necessary
case AY_A_FINE:
case AY_B_FINE:
case AY_C_FINE:
case AY_E_FINE:
case AY_E_COARSE:
break;
case AY_A_COARSE:
case AY_B_COARSE:
case AY_C_COARSE:
val &= 0x0f;
break;
case AY_NOISEPER:
case AY_A_VOL:
case AY_B_VOL:
case AY_C_VOL:
val &= 0x1f;
break;
case AY_ENABLE:
/*
if ((lastEnable == -1) || ((lastEnable & 0x40) != (regs[AY_ENABLE] & 0x40))) {
SelectedRegister = ((regs[AY_ENABLE] & 0x40) > 0 ? regs[AY_PORT_B] : 0xff);
}
if ((lastEnable == -1) || ((lastEnable & 0x80) != (regs[AY_ENABLE] & 0x80))) {
PortWrite((regs[AY_ENABLE] & 0x80) > 0 ? regs[AY_PORT_B] : 0xff);
}
lastEnable = regs[AY_ENABLE];*/
break;
case AY_E_SHAPE:
val &= 0x0f;
attack = ((val & 0x04) != 0 ? 0x0f : 0x00);
// envelopeCount = 0;
if ((val & 0x08) == 0)
{
/* if Continue = 0, map the shape to the equivalent one which has Continue = 1 */
sustain = true;
alternate = (attack != 0);
}
else
{
sustain = (val & 0x01) != 0;
alternate = (val & 0x02) != 0;
}
envelopeStep = 0x0f;
sustaining = false;
envelopeVolume = (envelopeStep ^ attack);
break;
case AY_PORT_A:
/*
if ((regs[AY_ENABLE] & 0x40) > 0) {
selectedRegister = regs[AY_PORT_A];
}*/
break;
case AY_PORT_B:
/*
if ((regs[AY_ENABLE] & 0x80) > 0) {
PortWrite(regs[AY_PORT_A]);
}*/
break;
}
regs[SelectedRegister] = val;
}
/// <summary>
/// Simulates port reads from the AY chip
/// </summary>
/// <returns></returns>
public int PortRead()
{
if (SelectedRegister == AY_PORT_B)
{
if ((regs[AY_ENABLE] & 0x80) == 0)
return 0xff;
else
return regs[AY_PORT_B];
}
return regs[selectedRegister];
}
private void EndSampleAY()
{
//averagedChannelSamples[0] = (short)((averagedChannelSamples[ChannelLeft] +
// averagedChannelSamples[ChannelCenter] +
// averagedChannelSamples[ChannelRight])
// / soundSampleCounter);
// averagedChannelSamples[1] = (short)((averagedChannelSamples[ChannelLeft] +
// averagedChannelSamples[ChannelCenter] +
// averagedChannelSamples[ChannelRight])
// / soundSampleCounter);
averagedChannelSamples[0] = (short)((averagedChannelSamples[ChannelLeft] + averagedChannelSamples[ChannelCenter] / 1.5) / soundSampleCounter);
averagedChannelSamples[1] = (short)((averagedChannelSamples[ChannelRight] + averagedChannelSamples[ChannelCenter] / 1.5) / soundSampleCounter);
soundSampleCounter = 0;
}
private void SampleAY()
{
int ah;
ah = regs[AY_ENABLE];
channel_mix[(int)Channel.A] = MixChannel(ah, regs[AY_A_VOL], (int)Channel.A);
ah >>= 1;
channel_mix[(int)Channel.B] = MixChannel(ah, regs[AY_B_VOL], (int)Channel.B);
ah >>= 1;
channel_mix[(int)Channel.C] = MixChannel(ah, regs[AY_C_VOL], (int)Channel.C);
averagedChannelSamples[0] += channel_mix[(int)Channel.A];
averagedChannelSamples[1] += channel_mix[(int)Channel.B];
averagedChannelSamples[2] += channel_mix[(int)Channel.C];
soundSampleCounter++;
}
private short MixChannel(int ah, int cl, int chan)
{
int al = channel_out[chan];
int bl, bh;
bl = ah;
bh = ah;
bh &= 0x1;
bl >>= 3;
al |= (bh); //Tone | AY_ENABLE
bl |= (noiseOut); //Noise | AY_ENABLE
al &= bl;
if ((al != 0))
{
if ((cl & 16) != 0)
cl = envelopeVolume;
cl &= 15;
//return (AY_Volumes[cl]);
return (AY_SpecVolumes[cl]);
}
return 0;
}
/// <summary>
/// Gets the tone period for the specified channel
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
private int TonePeriod(int channel)
{
return (regs[(channel) << 1] | ((regs[((channel) << 1) | 1] & 0x0f) << 8));
}
/// <summary>
/// Gets the noise period for the specified channel
/// </summary>
/// <returns></returns>
private int NoisePeriod()
{
return (regs[AY_NOISEPER] & 0x1f);
}
/// <summary>
/// Gets the envelope period for the specified channel
/// </summary>
/// <returns></returns>
private int EnvelopePeriod()
{
return ((regs[AY_E_FINE] | (regs[AY_E_COARSE] << 8)));
}
/// <summary>
/// Gets the noise enable value for the specified channel
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
private int NoiseEnable(int channel)
{
return ((regs[AY_ENABLE] >> (3 + channel)) & 1);
}
/// <summary>
/// Gets the tone enable value for the specified channel
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
private int ToneEnable(int channel)
{
return ((regs[AY_ENABLE] >> (channel)) & 1);
}
/// <summary>
/// Gets the tone envelope value for the specified channel
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
private int ToneEnvelope(int channel)
{
//return ((regs[AY_A_VOL + channel] & 0x10) >> 4);
return ((regs[AY_A_VOL + channel] >> 4) & 0x1);
}
/// <summary>
/// Updates noise
/// </summary>
private void UpdateNoise()
{
noiseCount++;
if (noiseCount >= NoisePeriod() && (noiseCount > 4))
{
/* Is noise output going to change? */
if (((randomSeed + 1) & 2) != 0) /* (bit0^bit1)? */
{
noiseOut ^= 1;
}
/* The Random Number Generator of the 8910 is a 17-bit shift */
/* register. The input to the shift register is bit0 XOR bit3 */
/* (bit0 is the output). This was verified on AY-3-8910 and YM2149 chips. */
/* The following is a fast way to compute bit17 = bit0^bit3. */
/* Instead of doing all the logic operations, we only check */
/* bit0, relying on the fact that after three shifts of the */
/* register, what now is bit3 will become bit0, and will */
/* invert, if necessary, bit14, which previously was bit17. */
if ((randomSeed & 1) != 0)
randomSeed ^= 0x24000; /* This version is called the "Galois configuration". */
randomSeed >>= 1;
noiseCount = 0;
}
}
/// <summary>
/// Updates envelope
/// </summary>
private void UpdateEnvelope()
{
/* update envelope */
if (!sustaining)
{
envelopeCount++;
if ((envelopeCount >= EnvelopePeriod()))
{
envelopeStep--;
/* check envelope current position */
if (envelopeStep < 0)
{
if (sustain)
{
if (alternate)
attack ^= 0x0f;
sustaining = true;
envelopeStep = 0;
}
else
{
/* if CountEnv has looped an odd number of times (usually 1), */
/* invert the output. */
if (alternate && ((envelopeStep & (0x0f + 1)) != 0) && (envelopeCount > 4))
attack ^= 0x0f;
envelopeStep &= 0x0f;
}
}
envelopeCount = 0;
}
}
envelopeVolume = (envelopeStep ^ attack);
}
public void Update()
{
envelopeClock ^= 1;
if (envelopeClock == 1)
{
envelopeCount++;
//if ((((regs[AY_A_VOL + 0] & 0x10) >> 4) & (((regs[AY_A_VOL + 1] & 0x10) >> 4) & ((regs[AY_A_VOL + 2] & 0x10) >> 4))) != 1)
//if ((((regs[AY_A_VOL + 0] >> 4) & 0x1) & (((regs[AY_A_VOL + 1] >> 4) & 0x1) & ((regs[AY_A_VOL + 2] >> 4) & 0x1))) != 0)
if (((regs[AY_A_VOL + 0] & 0x10) & (regs[AY_A_VOL + 1] & 0x10) & (regs[AY_A_VOL + 2] & 0x10)) != 1)
{
// update envelope
if (!sustaining)
UpdateEnvelope();
envelopeVolume = (envelopeStep ^ attack);
}
}
// update noise
if ((regs[AY_ENABLE] & 0x38) != 0x38)
{
UpdateNoise();
}
// update channels
channel_count[0]++;
int regs1 = (regs[1] & 0x0f) << 8;
if (((regs[0] | regs1) > 4) && (channel_count[0] >= (regs[0] | regs1)))
{
channel_out[0] ^= 1;
channel_count[0] = 0;
}
int regs3 = (regs[3] & 0x0f) << 8;
channel_count[1]++;
if (((regs[2] | regs3) > 4) && (channel_count[1] >= (regs[2] | regs3)))
{
channel_out[1] ^= 1;
channel_count[1] = 0;
}
int regs5 = (regs[5] & 0x0f) << 8;
channel_count[2]++;
if (((regs[4] | regs5) > 4) && (channel_count[2] >= (regs[4] | regs5)))
{
channel_out[2] ^= 1;
channel_count[2] = 0;
}
}
#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()
{
}
public void GetSamplesSync(out short[] samples, out int nsamp)
{
samples = _samples;
nsamp = _nsamp;
}
#endregion
}
}

View File

@ -0,0 +1,802 @@

using BizHawk.Common;
using BizHawk.Emulation.Common;
using System;
using System.Collections.Generic;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// AY-3-8912 Emulated Device
///
/// Based heavily on the YM-2149F / AY-3-8910 emulator used in Unreal Speccy
/// (Originally created under Public Domain license by SMT jan.2006)
///
/// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.cpp
/// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.h
/// </summary>
public class AYChip : IPSG
{
#region Device Fields
/// <summary>
/// The emulated machine (passed in via constructor)
/// </summary>
private SpectrumBase _machine;
private int _tStatesPerFrame;
private int _sampleRate;
private int _samplesPerFrame;
private int _tStatesPerSample;
private short[] _audioBuffer;
private int _audioBufferIndex;
private int _lastStateRendered;
#endregion
#region Construction & Initialization
/// <summary>
/// Main constructor
/// </summary>
public AYChip(SpectrumBase machine)
{
_machine = machine;
}
/// <summary>
/// Initialises the AY chip
/// </summary>
public void Init(int sampleRate, int tStatesPerFrame)
{
InitTiming(sampleRate, tStatesPerFrame);
UpdateVolume();
Reset();
}
#endregion
#region IPortIODevice
public bool ReadPort(ushort port, ref int value)
{
if (port != 0xfffd)
{
// port read is not addressing this device
return false;
}
value = PortRead();
return true;
}
public bool WritePort(ushort port, int value)
{
if (port == 0xfffd)
{
// register select
SelectedRegister = value & 0x0f;
return true;
}
else if (port == 0xbffd)
{
// Update the audiobuffer based on the current CPU cycle
// (this process the previous data BEFORE writing to the currently selected register)
int d = (int)(_machine.CurrentFrameCycle);
BufferUpdate(d);
// write to register
PortWrite(value);
return true;
}
return false;
}
#endregion
#region AY Implementation
#region Public Properties
/// <summary>
/// AY mixer panning configuration
/// </summary>
[Flags]
public enum AYPanConfig
{
MONO = 0,
ABC = 1,
ACB = 2,
BAC = 3,
BCA = 4,
CAB = 5,
CBA = 6,
}
/// <summary>
/// The AY panning configuration
/// </summary>
public AYPanConfig PanningConfiguration
{
get
{
return _currentPanTab;
}
set
{
if (value != _currentPanTab)
{
_currentPanTab = value;
UpdateVolume();
}
}
}
/// <summary>
/// The AY chip output volume
/// (0 - 100)
/// </summary>
public int Volume
{
get
{
return _volume;
}
set
{
value = Math.Max(0, value);
value = Math.Max(100, value);
if (_volume == value)
{
return;
}
_volume = value;
UpdateVolume();
}
}
/// <summary>
/// The currently selected register
/// </summary>
public int SelectedRegister
{
get { return _activeRegister; }
set
{
_activeRegister = (byte)value;
}
}
#endregion
#region Public Methods
/// <summary>
/// Resets the PSG
/// </summary>
public void Reset()
{
/*
_noiseVal = 0x0FFFF;
_outABC = 0;
_outNoiseABC = 0;
_counterNoise = 0;
_counterA = 0;
_counterB = 0;
_counterC = 0;
_EnvelopeCounterBend = 0;
// clear all the registers
for (int i = 0; i < 14; i++)
{
SelectedRegister = i;
PortWrite(0);
}
randomSeed = 1;
// number of frames to update
var fr = (_audioBufferIndex * _tStatesPerFrame) / _audioBuffer.Length;
// update the audio buffer
BufferUpdate(fr);
*/
}
/// <summary>
/// Reads the value from the currently selected register
/// </summary>
/// <returns></returns>
public int PortRead()
{
return _registers[_activeRegister];
}
/// <summary>
/// Writes to the currently selected register
/// </summary>
/// <param name="value"></param>
public void PortWrite(int value)
{
if (_activeRegister >= 0x10)
return;
byte val = (byte)value;
if (((1 << _activeRegister) & ((1 << 1) | (1 << 3) | (1 << 5) | (1 << 13))) != 0)
val &= 0x0F;
if (((1 << _activeRegister) & ((1 << 6) | (1 << 8) | (1 << 9) | (1 << 10))) != 0)
val &= 0x1F;
if (_activeRegister != 13 && _registers[_activeRegister] == val)
return;
_registers[_activeRegister] = val;
switch (_activeRegister)
{
// Channel A (Combined Pitch)
// (not written to directly)
case 0:
case 1:
_dividerA = _registers[AY_A_FINE] | (_registers[AY_A_COARSE] << 8);
break;
// Channel B (Combined Pitch)
// (not written to directly)
case 2:
case 3:
_dividerB = _registers[AY_B_FINE] | (_registers[AY_B_COARSE] << 8);
break;
// Channel C (Combined Pitch)
// (not written to directly)
case 4:
case 5:
_dividerC = _registers[AY_C_FINE] | (_registers[AY_C_COARSE] << 8);
break;
// Noise Pitch
case 6:
_dividerN = val * 2;
break;
// Mixer
case 7:
_bit0 = 0 - ((val >> 0) & 1);
_bit1 = 0 - ((val >> 1) & 1);
_bit2 = 0 - ((val >> 2) & 1);
_bit3 = 0 - ((val >> 3) & 1);
_bit4 = 0 - ((val >> 4) & 1);
_bit5 = 0 - ((val >> 5) & 1);
break;
// Channel Volumes
case 8:
_eMaskA = (val & 0x10) != 0 ? -1 : 0;
_vA = ((val & 0x0F) * 2 + 1) & ~_eMaskA;
break;
case 9:
_eMaskB = (val & 0x10) != 0 ? -1 : 0;
_vB = ((val & 0x0F) * 2 + 1) & ~_eMaskB;
break;
case 10:
_eMaskC = (val & 0x10) != 0 ? -1 : 0;
_vC = ((val & 0x0F) * 2 + 1) & ~_eMaskC;
break;
// Envelope (Combined Duration)
// (not written to directly)
case 11:
case 12:
_dividerE = _registers[AY_E_FINE] | (_registers[AY_E_COARSE] << 8);
break;
// Envelope Shape
case 13:
// reset the envelope counter
_countE = 0;
if ((_registers[AY_E_SHAPE] & 4) != 0)
{
// attack
_eState = 0;
_eDirection = 1;
}
else
{
// decay
_eState = 31;
_eDirection = -1;
}
break;
case 14:
// IO Port - not implemented
break;
}
}
/// <summary>
/// Start of frame
/// </summary>
public void StartFrame()
{
_audioBufferIndex = 0;
BufferUpdate(0);
}
/// <summary>
/// End of frame
/// </summary>
public void EndFrame()
{
BufferUpdate(_tStatesPerFrame);
}
/// <summary>
/// Updates the audiobuffer based on the current frame t-state
/// </summary>
/// <param name="frameCycle"></param>
public void UpdateSound(int frameCycle)
{
BufferUpdate(frameCycle);
}
#endregion
#region Private Fields
/// <summary>
/// Register indicies
/// </summary>
private const int AY_A_FINE = 0;
private const int AY_A_COARSE = 1;
private const int AY_B_FINE = 2;
private const int AY_B_COARSE = 3;
private const int AY_C_FINE = 4;
private const int AY_C_COARSE = 5;
private const int AY_NOISEPITCH = 6;
private const int AY_MIXER = 7;
private const int AY_A_VOL = 8;
private const int AY_B_VOL = 9;
private const int AY_C_VOL = 10;
private const int AY_E_FINE = 11;
private const int AY_E_COARSE = 12;
private const int AY_E_SHAPE = 13;
private const int AY_PORT_A = 14;
private const int AY_PORT_B = 15;
/// <summary>
/// The register array
/*
The AY-3-8910/8912 contains 16 internal registers as follows:
Register Function Range
0 Channel A fine pitch 8-bit (0-255)
1 Channel A course pitch 4-bit (0-15)
2 Channel B fine pitch 8-bit (0-255)
3 Channel B course pitch 4-bit (0-15)
4 Channel C fine pitch 8-bit (0-255)
5 Channel C course pitch 4-bit (0-15)
6 Noise pitch 5-bit (0-31)
7 Mixer 8-bit (see below)
8 Channel A volume 4-bit (0-15, see below)
9 Channel B volume 4-bit (0-15, see below)
10 Channel C volume 4-bit (0-15, see below)
11 Envelope fine duration 8-bit (0-255)
12 Envelope course duration 8-bit (0-255)
13 Envelope shape 4-bit (0-15)
14 I/O port A 8-bit (0-255)
15 I/O port B 8-bit (0-255) (Not present on the AY-3-8912)
* The volume registers (8, 9 and 10) contain a 4-bit setting but if bit 5 is set then that channel uses the
envelope defined by register 13 and ignores its volume setting.
* The mixer (register 7) is made up of the following bits (low=enabled):
Bit: 7 6 5 4 3 2 1 0
Register: I/O I/O Noise Noise Noise Tone Tone Tone
Channel: B A C B A C B A
The AY-3-8912 ignores bit 7 of this register.
*/
/// </summary>
private int[] _registers = new int[16];
/// <summary>
/// The currently selected register
/// </summary>
private byte _activeRegister;
/// <summary>
/// The frequency of the AY chip
/// </summary>
private static int _chipFrequency = 1773400;
/// <summary>
/// The rendering resolution of the chip
/// </summary>
private double _resolution = 50D * 8D / _chipFrequency;
/// <summary>
/// Channel generator state
/// </summary>
private int _bitA;
private int _bitB;
private int _bitC;
/// <summary>
/// Envelope state
/// </summary>
private int _eState;
/// <summary>
/// Envelope direction
/// </summary>
private int _eDirection;
/// <summary>
/// Noise seed
/// </summary>
private int _noiseSeed;
/// <summary>
/// Mixer state
/// </summary>
private int _bit0;
private int _bit1;
private int _bit2;
private int _bit3;
private int _bit4;
private int _bit5;
/// <summary>
/// Noise generator state
/// </summary>
private int _bitN;
/// <summary>
/// Envelope masks
/// </summary>
private int _eMaskA;
private int _eMaskB;
private int _eMaskC;
/// <summary>
/// Amplitudes
/// </summary>
private int _vA;
private int _vB;
private int _vC;
/// <summary>
/// Channel gen counters
/// </summary>
private int _countA;
private int _countB;
private int _countC;
/// <summary>
/// Envelope gen counter
/// </summary>
private int _countE;
/// <summary>
/// Noise gen counter
/// </summary>
private int _countN;
/// <summary>
/// Channel gen dividers
/// </summary>
private int _dividerA;
private int _dividerB;
private int _dividerC;
/// <summary>
/// Envelope gen divider
/// </summary>
private int _dividerE;
/// <summary>
/// Noise gen divider
/// </summary>
private int _dividerN;
/// <summary>
/// Panning table list
/// </summary>
private static List<uint[]> PanTabs = new List<uint[]>
{
// MONO
new uint[] { 100,100, 100,100, 100,100 },
// ABC
new uint[] { 100,10, 66,66, 10,100 },
// ACB
new uint[] { 100,10, 10,100, 66,66 },
// BAC
new uint[] { 66,66, 100,10, 10,100 },
// BCA
new uint[] { 10,100, 100,10, 66,66 },
// CAB
new uint[] { 66,66, 10,100, 100,10 },
// CBA
new uint[] { 10,100, 66,66, 100,10 }
};
/// <summary>
/// The currently selected panning configuration
/// </summary>
private AYPanConfig _currentPanTab = AYPanConfig.ABC;
/// <summary>
/// The current volume
/// </summary>
private int _volume = 50;
/// <summary>
/// Volume tables state
/// </summary>
private uint[][] _volumeTables;
/// <summary>
/// Volume table to be used
/// </summary>
private static uint[] AYVolumes = new uint[]
{
0x0000,0x0000,0x0340,0x0340,0x04C0,0x04C0,0x06F2,0x06F2,
0x0A44,0x0A44,0x0F13,0x0F13,0x1510,0x1510,0x227E,0x227E,
0x289F,0x289F,0x414E,0x414E,0x5B21,0x5B21,0x7258,0x7258,
0x905E,0x905E,0xB550,0xB550,0xD7A0,0xD7A0,0xFFFF,0xFFFF,
};
#endregion
#region Private Methods
/// <summary>
/// Forces an update of the volume tables
/// </summary>
private void UpdateVolume()
{
var vol = (ulong)0xFFFF * (ulong)_volume / 100UL;
_volumeTables = new uint[6][];
// parent array
for (int j = 0; j < _volumeTables.Length; j++)
{
_volumeTables[j] = new uint[32];
// child array
for (int i = 0; i < _volumeTables[j].Length; i++)
{
_volumeTables[j][i] = (uint)(
(PanTabs[(int)_currentPanTab][j] * AYVolumes[i] * vol) /
(3 * 65535 * 100));
}
}
}
private int mult_const;
/// <summary>
/// Initializes timing information for the frame
/// </summary>
/// <param name="sampleRate"></param>
/// <param name="frameTactCount"></param>
private void InitTiming(int sampleRate, int frameTactCount)
{
_sampleRate = sampleRate;
_tStatesPerFrame = frameTactCount;
_tStatesPerSample = 79; //(int)Math.Round(((double)_tStatesPerFrame * 50D) /
//(16D * (double)_sampleRate),
//MidpointRounding.AwayFromZero);
_samplesPerFrame = _tStatesPerFrame / _tStatesPerSample;
_audioBuffer = new short[_samplesPerFrame * 2]; //[_sampleRate / 50];
_audioBufferIndex = 0;
mult_const = ((_chipFrequency / 8) << 14) / _machine.ULADevice.ClockSpeed;
var aytickspercputick = (double)_machine.ULADevice.ClockSpeed / (double)_chipFrequency;
int ayCyclesPerSample = (int)((double)_tStatesPerSample * (double)aytickspercputick);
}
/// <summary>
/// Updates the audiobuffer based on the current frame t-state
/// </summary>
/// <param name="cycle"></param>
private void BufferUpdate(int cycle)
{
if (cycle > _tStatesPerFrame)
{
// we are outside of the frame - just process the last value
cycle = _tStatesPerFrame;
}
// get the current length of the audiobuffer
int bufferLength = _samplesPerFrame; // _audioBuffer.Length;
int toEnd = ((bufferLength * cycle) / _tStatesPerFrame);
// loop through the number of samples we need to render
while(_audioBufferIndex < toEnd)
{
// run the AY chip processing at the correct resolution
for (int i = 0; i < _tStatesPerSample / 14; i++)
{
if (++_countA >= _dividerA)
{
_countA = 0;
_bitA ^= -1;
}
if (++_countB >= _dividerB)
{
_countB = 0;
_bitB ^= -1;
}
if (++_countC >= _dividerC)
{
_countC = 0;
_bitC ^= -1;
}
if (++_countN >= _dividerN)
{
_countN = 0;
_noiseSeed = (_noiseSeed * 2 + 1) ^ (((_noiseSeed >> 16) ^ (_noiseSeed >> 13)) & 1);
_bitN = 0 - ((_noiseSeed >> 16) & 1);
}
if (++_countE >= _dividerE)
{
_countE = 0;
_eState += +_eDirection;
var mask = (1 << _registers[AY_E_SHAPE]);
if ((_eState & ~31) != 0)
{
if ((mask & ((1 << 0) | (1 << 1) | (1 << 2) |
(1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) |
(1 << 7) | (1 << 9) | (1 << 15))) != 0)
{
_eState = _eDirection = 0;
}
}
else if ((mask & ((1 << 8) | (1 << 12))) != 0)
{
_eState &= 31;
}
else if ((mask & ((1 << 10) | (1 << 14))) != 0)
{
_eDirection = -_eDirection;
_eState += _eDirection;
}
else
{
// 11,13
_eState = 31;
_eDirection = 0;
}
}
}
// mix the sample
var mixA = ((_eMaskA & _eState) | _vA) & ((_bitA | _bit0) & (_bitN | _bit3));
var mixB = ((_eMaskB & _eState) | _vB) & ((_bitB | _bit1) & (_bitN | _bit4));
var mixC = ((_eMaskC & _eState) | _vC) & ((_bitC | _bit2) & (_bitN | _bit5));
var l = _volumeTables[0][mixA];
var r = _volumeTables[1][mixA];
l += _volumeTables[2][mixB];
r += _volumeTables[3][mixB];
l += _volumeTables[4][mixC];
r += _volumeTables[5][mixC];
_audioBuffer[_audioBufferIndex * 2] = (short)l;
_audioBuffer[(_audioBufferIndex * 2) + 1] = (short)r;
_audioBufferIndex++;
}
_lastStateRendered = cycle;
}
#endregion
#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()
{
_audioBuffer = new short[_samplesPerFrame * 2];
}
public void GetSamplesSync(out short[] samples, out int nsamp)
{
nsamp = _samplesPerFrame;
samples = _audioBuffer;
DiscardSamples();
}
#endregion
#region State Serialization
public int nullDump = 0;
/// <summary>
/// State serialization
/// </summary>
/// <param name="ser"></param>
public void SyncState(Serializer ser)
{
ser.BeginSection("PSG-AY");
ser.Sync("_tStatesPerFrame", ref _tStatesPerFrame);
ser.Sync("_sampleRate", ref _sampleRate);
ser.Sync("_samplesPerFrame", ref _samplesPerFrame);
ser.Sync("_tStatesPerSample", ref _tStatesPerSample);
ser.Sync("_audioBufferIndex", ref _audioBufferIndex);
ser.Sync("_audioBuffer", ref _audioBuffer, false);
ser.Sync("_registers", ref _registers, false);
ser.Sync("_activeRegister", ref _activeRegister);
ser.Sync("_bitA", ref _bitA);
ser.Sync("_bitB", ref _bitB);
ser.Sync("_bitC", ref _bitC);
ser.Sync("_eState", ref _eState);
ser.Sync("_eDirection", ref _eDirection);
ser.Sync("_noiseSeed", ref _noiseSeed);
ser.Sync("_bit0", ref _bit0);
ser.Sync("_bit1", ref _bit1);
ser.Sync("_bit2", ref _bit2);
ser.Sync("_bit3", ref _bit3);
ser.Sync("_bit4", ref _bit4);
ser.Sync("_bit5", ref _bit5);
ser.Sync("_bitN", ref _bitN);
ser.Sync("_eMaskA", ref _eMaskA);
ser.Sync("_eMaskB", ref _eMaskB);
ser.Sync("_eMaskC", ref _eMaskC);
ser.Sync("_vA", ref _vA);
ser.Sync("_vB", ref _vB);
ser.Sync("_vC", ref _vC);
ser.Sync("_countA", ref _countA);
ser.Sync("_countB", ref _countB);
ser.Sync("_countC", ref _countC);
ser.Sync("_countE", ref _countE);
ser.Sync("_countN", ref _countN);
ser.Sync("_dividerA", ref _dividerA);
ser.Sync("_dividerB", ref _dividerB);
ser.Sync("_dividerC", ref _dividerC);
ser.Sync("_dividerE", ref _dividerE);
ser.Sync("_dividerN", ref _dividerN);
ser.SyncEnum("_currentPanTab", ref _currentPanTab);
ser.Sync("_volume", ref nullDump);
for (int i = 0; i < 6; i++)
{
ser.Sync("volTable" + i, ref _volumeTables[i], false);
}
ser.EndSection();
}
#endregion
}
}

View File

@ -140,7 +140,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
if (_renderSound)
{
BuzzerDevice.StartFrame();
if (AYDevice != null && !PagingDisabled)
if (AYDevice != null)
AYDevice.StartFrame();
}
@ -155,11 +155,17 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
CPU.ExecuteOne();
// update AY
int ayCnt = 0;
if (_renderSound)
{
if (AYDevice != null && !PagingDisabled)
if (AYDevice != null)
{
AYDevice.UpdateSound(CurrentFrameCycle);
//AYDevice.UpdateSound(CurrentFrameCycle);
if (ayCnt > 10)
{
//AYDevice.UpdateSound(CurrentFrameCycle);
ayCnt = 0;
}
}
}
}
@ -174,6 +180,9 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
if (_renderSound)
BuzzerDevice.EndFrame();
if (AYDevice != null)
AYDevice.EndFrame();
FrameCount++;
// setup for next frame

View File

@ -16,11 +16,69 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// <returns></returns>
public override byte ReadPort(ushort port)
{
bool deviceAddressed = true;
// process IO contention
ContendPortAddress(port);
int result = 0xFF;
// check AY
if (AYDevice.ReadPort(port, ref result))
return (byte)result;
// Kempston joystick input takes priority over all other input
// if this is detected just return the kempston byte
if ((port & 0xe0) == 0 || (port & 0x20) == 0)
{
if (LocateUniqueJoystick(JoystickType.Kempston) != null)
return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine;
InputRead = true;
}
else
{
if (KeyboardDevice.ReadPort(port, ref result))
{
// not a lagframe
InputRead = true;
// tape loading monitor cycle
TapeDevice.MonitorRead();
// process tape INs
TapeDevice.ReadPort(port, ref result);
}
else
deviceAddressed = false;
}
if (!deviceAddressed)
{
// If this is an unused port the floating memory bus should be returned
// Floating bus is read on the previous cycle
int _tStates = CurrentFrameCycle - 1;
// if we are on the top or bottom border return 0xff
if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod))
{
result = 0xff;
}
else
{
if (ULADevice.floatingBusTable[_tStates] < 0)
{
result = 0xff;
}
else
{
result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]);
}
}
}
/*
// Check whether the low bit is reset
// Technically the ULA should respond to every even I/O address
bool lowBitReset = (port & 0x0001) == 0;
@ -81,7 +139,9 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
}
}
}
*/
return (byte)result;
}
@ -102,6 +162,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
int currT = CPU.TotalExecutedCycles;
AYDevice.WritePort(port, value);
// paging
if (port == 0x7ffd)
{
@ -164,7 +226,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
//TapeDevice.ProcessMicBit((value & MIC_BIT) != 0);
}
/*
// Active AY Register
if ((port & 0xc002) == 0xc000)
{
@ -178,7 +240,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
AYDevice.PortWrite(value);
CPU.TotalExecutedCycles += 3;
}
}
*/
}
}
}

View File

@ -31,7 +31,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
BuzzerDevice = new Buzzer(this);
BuzzerDevice.Init(44100, ULADevice.FrameLength);
AYDevice = new AY38912();
AYDevice = new AYChip(this);
AYDevice.Init(44100, ULADevice.FrameLength);
KeyboardDevice = new StandardKeyboard(this);

View File

@ -16,11 +16,67 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// <returns></returns>
public override byte ReadPort(ushort port)
{
bool deviceAddressed = true;
// process IO contention
ContendPortAddress(port);
int result = 0xFF;
// check AY
if (AYDevice.ReadPort(port, ref result))
return (byte)result;
// Kempston joystick input takes priority over all other input
// if this is detected just return the kempston byte
if ((port & 0xe0) == 0 || (port & 0x20) == 0)
{
if (LocateUniqueJoystick(JoystickType.Kempston) != null)
return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine;
InputRead = true;
}
else
{
if (KeyboardDevice.ReadPort(port, ref result))
{
// not a lagframe
InputRead = true;
// tape loading monitor cycle
TapeDevice.MonitorRead();
// process tape INs
TapeDevice.ReadPort(port, ref result);
}
else
deviceAddressed = false;
}
if (!deviceAddressed)
{
// If this is an unused port the floating memory bus should be returned
// Floating bus is read on the previous cycle
int _tStates = CurrentFrameCycle - 1;
// if we are on the top or bottom border return 0xff
if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod))
{
result = 0xff;
}
else
{
if (ULADevice.floatingBusTable[_tStates] < 0)
{
result = 0xff;
}
else
{
result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]);
}
}
}
/*
// Check whether the low bit is reset
// Technically the ULA should respond to every even I/O address
bool lowBitReset = (port & 0x0001) == 0;
@ -88,10 +144,11 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
else
result = 0xff;
}
*/
*//*
// if unused port the floating memory bus should be returned (still todo)
}
*/
return (byte)result;
}
@ -103,6 +160,9 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// <param name="value"></param>
public override void WritePort(ushort port, byte value)
{
// process IO contention
ContendPortAddress(port);
// get a BitArray of the port
BitArray portBits = new BitArray(BitConverter.GetBytes(port));
// get a BitArray of the value byte
@ -111,7 +171,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
// Check whether the low bit is reset
bool lowBitReset = !portBits[0]; // (port & 0x01) == 0;
ULADevice.Contend(port);
AYDevice.WritePort(port, value);
// port 0x7ffd - hardware should only respond when bits 1 & 15 are reset and bit 14 is set
if (port == 0x7ffd)
@ -131,10 +191,10 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
// portbit 4 is the LOW BIT of the ROM selection
ROMlow = bits[4];
}
}
}
// port 0x1ffd - hardware should only respond when bits 1, 13, 14 & 15 are reset and bit 12 is set
else if (port == 0x1ffd)
if (port == 0x1ffd)
{
if (!PagingDisabled)
{
@ -172,75 +232,6 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
// bit 4 is the printer port strobe
PrinterPortStrobe = bits[4];
}
/*
// port 0x7ffd - hardware should only respond when bits 1 & 15 are reset and bit 14 is set
if (!portBits[1] && !portBits[15] && portBits[14])
{
// paging (skip if paging has been disabled - paging can then only happen after a machine hard reset)
if (!PagingDisabled)
{
// bit 0 specifies the paging mode
SpecialPagingMode = bits[0];
if (!SpecialPagingMode)
{
// we are in normal mode
// portbit 4 is the LOW BIT of the ROM selection
BitArray romHalfNibble = new BitArray(2);
romHalfNibble[0] = portBits[4];
// value bit 2 is the high bit of the ROM selection
romHalfNibble[1] = bits[2];
// value bit 1 is ignored in normal paging mode
// set the ROMPage
ROMPaged = ZXSpectrum.GetIntFromBitArray(romHalfNibble);
// bit 3 controls shadow screen
SHADOWPaged = bits[3];
// Bit 5 set signifies that paging is disabled until next reboot
PagingDisabled = bits[5];
}
}
}
// port 0x1ffd - special paging mode
// hardware should only respond when bits 1, 13, 14 & 15 are reset and bit 12 is set
if (!portBits[1] && portBits[12] && !portBits[13] && !portBits[14] && !portBits[15])
{
if (!PagingDisabled && SpecialPagingMode)
{
// process special paging
// this is decided based on combinations of bits 1 & 2
// Config 0 = Bit1-0 Bit2-0
// Config 1 = Bit1-1 Bit2-0
// Config 2 = Bit1-0 Bit2-1
// Config 3 = Bit1-1 Bit2-1
BitArray confHalfNibble = new BitArray(2);
confHalfNibble[0] = bits[1];
confHalfNibble[1] = bits[2];
// set special paging configuration
PagingConfiguration = ZXSpectrum.GetIntFromBitArray(confHalfNibble);
// last value should be saved at 0x5b67 (23399) - not sure if this is actually needed
WriteBus(0x5b67, value);
}
// bit 3 controls the disk motor (1=on, 0=off)
DiskMotorState = bits[3];
// bit 4 is the printer port strobe
PrinterPortStrobe = bits[4];
}
*/
// Only even addresses address the ULA
if (lowBitReset)
@ -268,100 +259,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
//TapeDevice.ProcessMicBit((value & MIC_BIT) != 0);
}
else
{
// AY Register activation
if ((port & 0xc002) == 0xc000)
{
var reg = value & 0x0f;
AYDevice.SelectedRegister = reg;
CPU.TotalExecutedCycles += 3;
}
else
{
if ((port & 0xC002) == 0x8000)
{
AYDevice.PortWrite(value);
CPU.TotalExecutedCycles += 3;
}
/*
else
{
if ((port & 0xC002) == 0x4000) //Are bits 1 and 15 reset and bit 14 set?
{
// memory paging activate
if (PagingDisabled)
return;
// bit 5 handles paging disable (48k mode, persistent until next reboot)
if ((value & 0x20) != 0)
{
PagingDisabled = true;
}
// shadow screen
if ((value & 0x08) != 0)
{
SHADOWPaged = true;
}
else
{
SHADOWPaged = false;
}
}
else
{
//Extra Memory Paging feature activate
if ((port & 0xF002) == 0x1000) //Is bit 12 set and bits 13,14,15 and 1 reset?
{
if (PagingDisabled)
return;
// set disk motor state
//todo
if ((value & 0x08) != 0)
{
//diskDriveState |= (1 << 4);
}
else
{
//diskDriveState &= ~(1 << 4);
}
if ((value & 0x1) != 0)
{
// activate special paging mode
SpecialPagingMode = true;
PagingConfiguration = (value & 0x6 >> 1);
}
else
{
// normal paging mode
SpecialPagingMode = false;
}
}
else
{
// disk write port
if ((port & 0xF002) == 0x3000) //Is bit 12 set and bits 13,14,15 and 1 reset?
{
//udpDrive.DiskWriteByte((byte)(val & 0xff));
}
}
}
}
*/
}
}
LastULAOutByte = value;
}
/// <summary>

View File

@ -31,7 +31,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
BuzzerDevice = new Buzzer(this);
BuzzerDevice.Init(44100, ULADevice.FrameLength);
AYDevice = new AY38912();
AYDevice = new AYChip(this);
AYDevice.Init(44100, ULADevice.FrameLength);
KeyboardDevice = new StandardKeyboard(this);

View File

@ -16,14 +16,16 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
/// <returns></returns>
public override byte ReadPort(ushort port)
{
bool deviceAddressed = true;
// process IO contention
ContendPortAddress(port);
int result = 0xFF;
// Check whether the low bit is reset
// Technically the ULA should respond to every even I/O address
bool lowBitReset = (port & 0x0001) == 0;
// check AY
if (AYDevice.ReadPort(port, ref result))
return (byte)result;
// Kempston joystick input takes priority over all other input
// if this is detected just return the kempston byte
@ -34,62 +36,45 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
InputRead = true;
}
else if (lowBitReset)
{
// Even I/O address so get input from keyboard
KeyboardDevice.ReadPort(port, ref result);
TapeDevice.MonitorRead();
// not a lagframe
InputRead = true;
// tape loading monitor cycle
TapeDevice.MonitorRead();
// process tape INs
TapeDevice.ReadPort(port, ref result);
}
else
{
// devices other than the ULA will respond here
// (e.g. the AY sound chip in a 128k spectrum
// AY register activate - on +3/2a both FFFD and BFFD active AY
if ((port & 0xc002) == 0xc000)
if (KeyboardDevice.ReadPort(port, ref result))
{
result = (int)AYDevice.PortRead();
// not a lagframe
InputRead = true;
// tape loading monitor cycle
TapeDevice.MonitorRead();
// process tape INs
TapeDevice.ReadPort(port, ref result);
}
else if ((port & 0xc002) == 0x8000)
else
deviceAddressed = false;
}
if (!deviceAddressed)
{
// If this is an unused port the floating memory bus should be returned
// Floating bus is read on the previous cycle
int _tStates = CurrentFrameCycle - 1;
// if we are on the top or bottom border return 0xff
if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod))
{
result = (int)AYDevice.PortRead();
result = 0xff;
}
// Kempston Mouse (not implemented yet)
else if ((port & 0xF002) == 0x2000) //Is bit 12 set and bits 13,14,15 and 1 reset?
else
{
//result = udpDrive.DiskStatusRead();
// disk drive is not yet implemented - return a max status byte for the menu to load
result = 255;
}
else if ((port & 0xF002) == 0x3000)
{
//result = udpDrive.DiskReadByte();
result = 0;
}
else if ((port & 0xF002) == 0x0)
{
if (PagingDisabled)
result = 0x1;
else
if (ULADevice.floatingBusTable[_tStates] < 0)
{
result = 0xff;
}
else
{
result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]);
}
}
// if unused port the floating memory bus should be returned (still todo)
}
return (byte)result;
@ -113,6 +98,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
// Check whether the low bit is reset
bool lowBitReset = !portBits[0]; // (port & 0x01) == 0;
AYDevice.WritePort(port, value);
// port 0x7ffd - hardware should only respond when bits 1 & 15 are reset and bit 14 is set
if (port == 0x7ffd)
{
@ -134,7 +121,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
}
}
// port 0x1ffd - hardware should only respond when bits 1, 13, 14 & 15 are reset and bit 12 is set
else if (port == 0x1ffd)
if (port == 0x1ffd)
{
if (!PagingDisabled)
{
@ -199,25 +186,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
//TapeDevice.ProcessMicBit((value & MIC_BIT) != 0);
}
else
{
// AY Register activation
if ((port & 0xc002) == 0xc000)
{
var reg = value & 0x0f;
AYDevice.SelectedRegister = reg;
CPU.TotalExecutedCycles += 3;
}
else
{
if ((port & 0xC002) == 0x8000)
{
AYDevice.PortWrite(value);
CPU.TotalExecutedCycles += 3;
}
}
}
LastULAOutByte = value;
}

View File

@ -31,7 +31,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
BuzzerDevice = new Buzzer(this);
BuzzerDevice.Init(44100, ULADevice.FrameLength);
AYDevice = new AY38912();
AYDevice = new AYChip(this);
AYDevice.Init(44100, ULADevice.FrameLength);
KeyboardDevice = new StandardKeyboard(this);

View File

@ -24,8 +24,13 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
public bool PutSettings(ZXSpectrumSettings o)
{
if (SoundMixer != null)
SoundMixer.Stereo = o.StereoSound;
//if (SoundMixer != null)
//SoundMixer.Stereo = o.StereoSound;
if (_machine != null && _machine.AYDevice != null && _machine.AYDevice.GetType() == typeof(AYChip))
{
((AYChip)_machine.AYDevice as AYChip).PanningConfiguration = o.AYPanConfig;
}
Settings = o;
@ -48,10 +53,17 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
[DefaultValue(true)]
public bool AutoLoadTape { get; set; }
/*
[DisplayName("Stereo Sound")]
[Description("Turn stereo sound on or off")]
[DefaultValue(true)]
public bool StereoSound { get; set; }
*/
[DisplayName("AY-3-8912 Panning Config")]
[Description("Set the PSG panning configuration.\nThe chip has 3 audio channels that can be outputed in different configurations")]
[DefaultValue(AYChip.AYPanConfig.ABC)]
public AYChip.AYPanConfig AYPanConfig { get; set; }
[DisplayName("Core OSD Message Verbosity")]
[Description("Full: Display all GUI messages\nMedium: Display only emulator/device generated messages\nNone: Show no messages")]

View File

@ -51,8 +51,6 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
core = ms.ToArray();
}
if (ser.IsWriter)
{
ser.SyncEnum("_machineType", ref _machineType);
@ -70,12 +68,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
var tmpM = _machineType;
ser.SyncEnum("_machineType", ref _machineType);
if (tmpM != _machineType)
if (tmpM != _machineType && _machineType.ToString() != "72")
{
string msg = "SAVESTATE FAILED TO LOAD!!\n\n";
msg += "Current Configuration: " + _machineType.ToString();
msg += "Current Configuration: " + tmpM.ToString();
msg += "\n";
msg += "Saved Configuration: " + tmpM.ToString();
msg += "Saved Configuration: " + _machineType.ToString();
msg += "\n\n";
msg += "If you wish to load this SaveState ensure that you have the correct machine configuration selected, reboot the core, then try again.";
CoreComm.ShowMessage(msg);

View File

@ -101,7 +101,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
SoundMixer = new SoundProviderMixer((int)(32767 / 10), (ISoundProvider)_machine.BuzzerDevice);
if (_machine.AYDevice != null)
SoundMixer.AddSource(_machine.AYDevice);
SoundMixer.Stereo = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).StereoSound;
//SoundMixer.Stereo = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).StereoSound;
if (_machine.AYDevice != null && _machine.AYDevice.GetType() == typeof(AYChip))
{
((AYChip)_machine.AYDevice as AYChip).PanningConfiguration = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).AYPanConfig;
}
ser.Register<ISoundProvider>(SoundMixer);