Do ChannelF timings a bit better; some cleanups

This commit is contained in:
CasualPokePlayer 2024-09-05 16:56:12 -07:00
parent ca104aec8a
commit c17930c40f
7 changed files with 170 additions and 154 deletions

View File

@ -12,32 +12,71 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
public bool DeterministicEmulation { get; set; }
private double cpuFreq;
private double refreshRate => region == RegionType.NTSC ? 60.5307257846 : 50.0801282051;
public int ClockPerFrame;
public int FrameClock = 0;
public int FrameClock;
private void CalcClock()
{
// CPU speeds from https://en.wikipedia.org/wiki/Fairchild_Channel_F
// also https://github.com/mamedev/mame/blob/c8192c898ce7f68c0c0b87e44199f0d3e710439b/src/mame/drivers/channelf.cpp
double cpuFreq, pixelClock;
int pixelClocksPerFrame;
if (Region == DisplayType.NTSC)
{
cpuFreq = 1.7897725;
{
// NTSC CPU speed is NTSC Colorburst / 2
const double NTSC_COLORBURST = 4500000 * 227.5 / 286;
cpuFreq = NTSC_COLORBURST / 2;
// NTSC pixel clock is NTSC Colorburst * 8 / 7
pixelClock = NTSC_COLORBURST * 8 / 7;
// NTSC refresh rate is (pixelclock * 8 / 7) / (256 * 264)
pixelClocksPerFrame = 256 * 264;
// (aka (1023750000 * 8) / (256 * 264 * 286 * 7)
// reduced to 234375 / 3872
VsyncNumerator = 234375;
VsyncDenominator = 3872;
}
else
{
if (version == ConsoleVersion.ChannelF)
{
cpuFreq = 2.0;
// PAL CPU speed is 2MHz
cpuFreq = 2000000;
// PAL pixel clock is 4MHz
pixelClock = 4000000;
// PAL refresh rate is pixelclock / (256 * 312)
pixelClocksPerFrame = 256 * 312;
// reduced to 15625 / 312
VsyncNumerator = 15625;
VsyncDenominator = 312;
}
else if (version == ConsoleVersion.ChannelF_II)
{
cpuFreq = 1.9704972;
// PAL CPU speed for gen 2 seems to be contested
// various sources seem to say 1.77MHz (i.e. PAL Colorburst * 2 / 5)
// wikipedia used to have such, before changing it to 1.97MHz (i.e. PAL Colorburst * 4 / 9)
// if we assume the pixel clock is double the cpu freq, then 1.97MHz makes more sense (49.3Hz)
// 1.77MHz * 2 would result in 44.3Hz, complete nonsense for PAL
// however, this kind of relationship between the CPU freq and pixel clock is necessarily the case (see NTSC)
// wikipedia's numbers seem to come from a mame contributor (e5frog) editing the page anyways, so it's probably trustworthy
const double PAL_COLORBURST = 15625 * 283.75 + 25;
cpuFreq = PAL_COLORBURST * 4 / 9;
// not entirely sure what the pixel clock for PAL is here
// presumingly, it's just cpuFreq * 2?
pixelClock = PAL_COLORBURST * 8 / 9;
// PAL refresh rate is pixelclock / (256 * 312)
pixelClocksPerFrame = 256 * 312;
// (aka (4433618.75 * 8) / (256 * 312 * 9)
// reduced to 17734475 / 359424
VsyncNumerator = 17734475;
VsyncDenominator = 359424;
}
else
{
throw new InvalidOperationException();
}
}
var c = ((cpuFreq * 1000000) / refreshRate);
var c = cpuFreq * pixelClocksPerFrame / pixelClock;
ClockPerFrame = (int) c;
SetupAudio();

View File

@ -5,48 +5,26 @@ using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Consoles.ChannelF
{
public partial class ChannelF : ISettable<ChannelF.ChannelFSettings, ChannelF.ChannelFSyncSettings>
public partial class ChannelF : ISettable<object, ChannelF.ChannelFSyncSettings>
{
internal ChannelFSettings Settings = new ChannelFSettings();
internal ChannelFSyncSettings SyncSettings = new ChannelFSyncSettings();
private ChannelFSyncSettings _syncSettings;
public ChannelFSettings GetSettings()
{
return Settings.Clone();
}
public object GetSettings()
=> null;
public ChannelFSyncSettings GetSyncSettings()
{
return SyncSettings.Clone();
}
=> _syncSettings.Clone();
public PutSettingsDirtyBits PutSettings(ChannelFSettings o)
{
Settings = o;
return PutSettingsDirtyBits.None;
}
public PutSettingsDirtyBits PutSettings(object o)
=> PutSettingsDirtyBits.None;
public PutSettingsDirtyBits PutSyncSettings(ChannelFSyncSettings o)
{
bool ret = ChannelFSyncSettings.NeedsReboot(SyncSettings, o);
SyncSettings = o;
var ret = ChannelFSyncSettings.NeedsReboot(_syncSettings, o);
_syncSettings = o;
return ret ? PutSettingsDirtyBits.RebootCore : PutSettingsDirtyBits.None;
}
[CoreSettings]
public class ChannelFSettings
{
public ChannelFSettings Clone()
{
return (ChannelFSettings)MemberwiseClone();
}
public ChannelFSettings()
{
SettingsUtil.SetDefaultValues(this);
}
}
[CoreSettings]
public class ChannelFSyncSettings
{

View File

@ -6,40 +6,48 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
/// Sound related functions
/// </summary>
public partial class ChannelF : ISoundProvider
{
private int tone = 0;
private short[] sampleBuffer;
private int[] toneBuffer;
private readonly double sampleRate = 44100;
private readonly double decay = 0.998;
private readonly int rampUpTime = 10;
private int samplesPerFrame;
private double cyclesPerSample;
private double amplitude = 0;
private int rampCounter = 0;
{
private const double SAMPLE_RATE = 44100;
private const double DECAY = 0.998;
private const int RAMP_UP_TIME = 10;
private int _tone;
private short[] _sampleBuffer;
private double[] _filteredSampleBuffer;
private int[] _toneBuffer;
private int _samplesPerFrame;
private double _cyclesPerSample;
private double _amplitude;
private int _rampCounter;
private int _currTone;
private int _samplePosition;
private short[] _finalSampleBuffer = [ ];
private void SetupAudio()
{
samplesPerFrame = (int)(sampleRate / refreshRate);
cyclesPerSample = ClockPerFrame / (double)samplesPerFrame;
sampleBuffer = new short[samplesPerFrame];
toneBuffer = new int[samplesPerFrame];
_samplesPerFrame = (int)(SAMPLE_RATE * VsyncDenominator / VsyncNumerator);
// TODO: more precise audio clocking
_cyclesPerSample = ClockPerFrame / (double)_samplesPerFrame;
_sampleBuffer = new short[_samplesPerFrame];
_filteredSampleBuffer = new double[_samplesPerFrame];
_toneBuffer = new int[_samplesPerFrame];
}
private void AudioChange()
{
var currSample = (int)(FrameClock / cyclesPerSample);
var currSample = (int)(FrameClock / _cyclesPerSample);
while (currSample < samplesPerFrame)
while (currSample < _samplesPerFrame)
{
toneBuffer[currSample++] = tone;
_toneBuffer[currSample++] = _tone;
}
}
private int GetWaveSample(double time, int tone)
private static int GetWaveSample(double time, int tone)
{
int t = 0;
var t = 0;
switch (tone)
{
case 0:
@ -63,79 +71,71 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
return t;
}
private int GetSineWaveSample(double time, double freq)
#if false
private static int GetSineWaveSample(double time, double freq)
{
double sTime = time / sampleRate;
double sAngle = 2.0 * Math.PI * sTime * freq;
var sTime = time / SAMPLE_RATE;
var sAngle = 2.0 * Math.PI * sTime * freq;
return (int)(Math.Sin(sAngle) * 0x8000);
}
private int GetSquareWaveSample(double time, double freq)
}
#endif
private static int GetSquareWaveSample(double time, double freq)
{
double sTime = time / sampleRate;
double period = 1.0 / freq;
double halfPeriod = period / 2.0;
double currentTimeInPeriod = sTime % period;
var sTime = time / SAMPLE_RATE;
var period = 1.0 / freq;
var halfPeriod = period / 2.0;
var currentTimeInPeriod = sTime % period;
return currentTimeInPeriod < halfPeriod ? 32766 : -32766;
}
private void ApplyLowPassFilter(short[] samples, double cutoffFrequency)
private static void ApplyLowPassFilter(short[] samples, double cutoffFrequency)
{
if (samples == null || samples.Length == 0)
return;
double rc = 1.0 / (cutoffFrequency * 2 * Math.PI);
double dt = 1.0 / sampleRate;
double alpha = dt / (rc + dt);
var rc = 1.0 / (cutoffFrequency * 2 * Math.PI);
const double dt = 1.0 / SAMPLE_RATE;
var alpha = dt / (rc + dt);
short previousSample = samples[0];
for (int i = 1; i < samples.Length; i++)
var previousSample = samples[0];
for (var i = 1; i < samples.Length; i++)
{
samples[i] = (short)(previousSample + alpha * (samples[i] - previousSample));
previousSample = samples[i];
}
}
private void ApplyBassBoostFilter(short[] samples, double boostAmount, double cutoffFrequency)
private static void ApplyBassBoostFilter(short[] samples, double[] filteredSamples, double boostAmount, double cutoffFrequency)
{
if (samples == null || samples.Length == 0)
return;
double A = Math.Pow(10, boostAmount / 40);
double omega = 2 * Math.PI * cutoffFrequency / sampleRate;
double sn = Math.Sin(omega);
double cs = Math.Cos(omega);
double alpha = sn / 2 * Math.Sqrt((A + 1 / A) * (1 / 0.707 - 1) + 2);
double beta = 2 * Math.Sqrt(A) * alpha;
var A = Math.Pow(10, boostAmount / 40);
var omega = 2 * Math.PI * cutoffFrequency / SAMPLE_RATE;
var sn = Math.Sin(omega);
var cs = Math.Cos(omega);
var alpha = sn / 2 * Math.Sqrt((A + 1 / A) * (1 / 0.707 - 1) + 2);
var beta = 2 * Math.Sqrt(A) * alpha;
double b0 = A * ((A + 1) - (A - 1) * cs + beta);
double b1 = 2 * A * ((A - 1) - (A + 1) * cs);
double b2 = A * ((A + 1) - (A - 1) * cs - beta);
double a0 = (A + 1) + (A - 1) * cs + beta;
double a1 = -2 * ((A - 1) + (A + 1) * cs);
double a2 = (A + 1) + (A - 1) * cs - beta;
var b0 = A * ((A + 1) - (A - 1) * cs + beta);
var b1 = 2 * A * ((A - 1) - (A + 1) * cs);
var b2 = A * ((A + 1) - (A - 1) * cs - beta);
var a0 = (A + 1) + (A - 1) * cs + beta;
var a1 = -2 * ((A - 1) + (A + 1) * cs);
var a2 = (A + 1) + (A - 1) * cs - beta;
double[] filteredSamples = new double[samples.Length];
filteredSamples[0] = samples[0];
filteredSamples[1] = samples[1];
for (int i = 2; i < samples.Length; i++)
for (var i = 2; i < samples.Length; i++)
{
filteredSamples[i] = (b0 / a0) * samples[i] + (b1 / a0) * samples[i - 1] + (b2 / a0) * samples[i - 2]
- (a1 / a0) * filteredSamples[i - 1] - (a2 / a0) * filteredSamples[i - 2];
}
for (int i = 0; i < samples.Length; i++)
for (var i = 0; i < samples.Length; i++)
{
samples[i] = Clamp((short)filteredSamples[i], short.MinValue, short.MaxValue);
}
short Clamp(short value, short min, short max)
{
if (value < min) return min;
if (value > max) return max;
return value;
samples[i] = (short)Math.Min(Math.Max(filteredSamples[i], short.MinValue), short.MaxValue);
}
}
@ -156,76 +156,77 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
public void DiscardSamples()
{
sampleBuffer = new short[samplesPerFrame];
Array.Clear(_sampleBuffer, 0, _sampleBuffer.Length);
// pre-populate the tonebuffer with the last active tone from the frame (will be overwritten if and when a tone change happens)
int lastTone = toneBuffer[^1];
for (int i = 0; i < toneBuffer.Length; i++)
toneBuffer[i] = lastTone;
var lastTone = _toneBuffer[^1];
for (var i = 0; i < _toneBuffer.Length; i++)
_toneBuffer[i] = lastTone;
}
int currTone = 0;
int samplePosition = 0;
public void GetSamplesSync(out short[] samples, out int nsamp)
{
// process tone buffer
for (int t = 0; t < toneBuffer.Length; t++)
for (var t = 0; t < _toneBuffer.Length; t++)
{
var tValue = toneBuffer[t];
var tValue = _toneBuffer[t];
if (currTone != tValue)
if (_currTone != tValue)
{
// tone is changing
if (tValue == 0)
{
// immediate silence
currTone = tValue;
sampleBuffer[t] = 0;
_currTone = tValue;
_sampleBuffer[t] = 0;
}
else
{
// tone change
amplitude = 1;
samplePosition = 0;
rampCounter = rampUpTime;
currTone = tValue;
_amplitude = 1;
_samplePosition = 0;
_rampCounter = RAMP_UP_TIME;
_currTone = tValue;
if (rampCounter <= 0)
sampleBuffer[t] = (short)((GetWaveSample(samplePosition++, currTone) * amplitude) / 30);
if (_rampCounter <= 0)
_sampleBuffer[t] = (short)((GetWaveSample(_samplePosition++, _currTone) * _amplitude) / 30);
}
}
else if (currTone > 0)
else if (_currTone > 0)
{
// tone is continuing
if (rampCounter <= 0)
if (_rampCounter <= 0)
{
amplitude *= decay;
samplePosition %= samplesPerFrame;
sampleBuffer[t] = (short)((GetWaveSample(samplePosition++, currTone) * amplitude) / 30);
_amplitude *= DECAY;
_samplePosition %= _samplesPerFrame;
_sampleBuffer[t] = (short)((GetWaveSample(_samplePosition++, _currTone) * _amplitude) / 30);
}
else
{
rampCounter--;
_rampCounter--;
}
}
else
{
// explicit silence
sampleBuffer[t] = 0;
_sampleBuffer[t] = 0;
}
}
ApplyBassBoostFilter(sampleBuffer, 4.0, 600);
ApplyLowPassFilter(sampleBuffer, 10000);
ApplyBassBoostFilter(_sampleBuffer, _filteredSampleBuffer, 4.0, 600);
ApplyLowPassFilter(_sampleBuffer, 10000);
nsamp = sampleBuffer.Length;
samples = new short[nsamp * 2];
nsamp = _sampleBuffer.Length;
if (_finalSampleBuffer.Length < nsamp * 2)
{
_finalSampleBuffer = new short[nsamp * 2];
}
samples = _finalSampleBuffer;
for (int i = 0, s = 0; i < nsamp; i++, s += 2)
{
samples[s] = (short)(sampleBuffer[i] * 10);
samples[s + 1] = (short)(sampleBuffer[i] * 10);
samples[s] = (short)(_sampleBuffer[i] * 10);
samples[s + 1] = (short)(_sampleBuffer[i] * 10);
}
DiscardSamples();

View File

@ -18,15 +18,16 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
ser.Sync(nameof(_isLag), ref _isLag);
ser.Sync(nameof(_lagCount), ref _lagCount);
ser.Sync(nameof(tone), ref tone);
ser.Sync(nameof(sampleBuffer), ref sampleBuffer, false);
ser.Sync(nameof(toneBuffer), ref toneBuffer, false);
ser.Sync(nameof(samplesPerFrame), ref samplesPerFrame);
ser.Sync(nameof(cyclesPerSample), ref cyclesPerSample);
ser.Sync(nameof(amplitude), ref amplitude);
ser.Sync(nameof(rampCounter), ref rampCounter);
ser.Sync(nameof(currTone), ref currTone);
ser.Sync(nameof(samplePosition), ref samplePosition);
ser.Sync(nameof(_tone), ref _tone);
ser.Sync(nameof(_sampleBuffer), ref _sampleBuffer, false);
ser.Sync(nameof(_filteredSampleBuffer), ref _filteredSampleBuffer, false);
ser.Sync(nameof(_toneBuffer), ref _toneBuffer, false);
ser.Sync(nameof(_samplesPerFrame), ref _samplesPerFrame);
ser.Sync(nameof(_cyclesPerSample), ref _cyclesPerSample);
ser.Sync(nameof(_amplitude), ref _amplitude);
ser.Sync(nameof(_rampCounter), ref _rampCounter);
ser.Sync(nameof(_currTone), ref _currTone);
ser.Sync(nameof(_samplePosition), ref _samplePosition);
ser.Sync(nameof(StateConsole), ref StateConsole, false);
ser.Sync(nameof(StateRight), ref StateRight, false);

View File

@ -99,8 +99,8 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
public int BufferWidth => 256 - lBorder - rBorder;
public int BufferHeight => (64 * scanlineRepeats) - tBorder - bBorder;
public int BackgroundColor => Colors.ARGB(0xFF, 0xFF, 0xFF);
public int VsyncNumerator => (int)refreshRate;
public int VsyncDenominator => 1;
public int VsyncNumerator { get; private set; }
public int VsyncDenominator { get; private set; }
public int[] TrimOutputBuffer(int[] buff, int leftTrim, int topTrim, int rightTrim, int bottomTrim)
{

View File

@ -10,7 +10,7 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
public partial class ChannelF : IDriveLight
{
[CoreConstructor(VSystemID.Raw.ChannelF)]
public ChannelF(CoreLoadParameters<ChannelFSettings, ChannelFSyncSettings> lp)
public ChannelF(CoreLoadParameters<object, ChannelFSyncSettings> lp)
{
var ser = new BasicServiceProvider(this);
ServiceProvider = ser;
@ -18,13 +18,11 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
_gameInfo = lp.Roms.Select(r => r.Game).ToList();
_files = lp.Roms.Select(r => r.RomData).ToList();
Settings = lp.Settings ?? new ChannelFSettings();
SyncSettings = lp.SyncSettings ?? new ChannelFSyncSettings();
_syncSettings = lp.SyncSettings ?? new ChannelFSyncSettings();
region = _syncSettings.Region;
version = _syncSettings.Version;
region = SyncSettings.Region;
version = SyncSettings.Version;
MemoryCallbacks = new MemoryCallbackSystem(new[] { "System Bus" });
MemoryCallbacks = new MemoryCallbackSystem([ "System Bus" ]);
ControllerDefinition = ChannelFControllerDefinition;
@ -40,7 +38,6 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
}
Cartridge = VesCartBase.Configure(_gameInfo[0], _files[0]);
CPU = new F3850
{

View File

@ -126,9 +126,9 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
OutputLatch[addr] = value;
latch_y = (value | 0xC0) ^ 0xFF;
var audio = ((value) >> 6) & 0x03;
if (audio != tone)
if (audio != _tone)
{
tone = audio;
_tone = audio;
AudioChange();
}