I'm going off the official manual, plus notes from MAME's implementation:
https://github.com/mamedev/mame/blob/ma ... ay8910.cpp
It says the counters increment instead of decrement, easy enough:
Code: Select all
auto PSG::Tone::clock() -> void {
if(++counter < period) return;
counter = 0;
phase ^= 1;
}
//note: lfsr.bit(n) == (lfsr >> n & 1)
auto PSG::Noise::clock() -> void {
if(++counter < period) return;
counter = 0;
if(phase ^= 1) lfsr = (lfsr.bit(0) ^ lfsr.bit(3)) << 16 | lfsr >> 1;
}
https://github.com/mamedev/mame/blob/ma ... .cpp#L1129
Too evil. So I just went off of the official PDF instead and came up with this:
Code: Select all
auto PSG::Envelope::clock() -> void {
if(holding) return;
if(++counter < period) return;
counter = 0;
if(!attacking) {
if(phase != 0) return (void)phase--;
} else {
if(phase != 15) return (void)phase++;
}
if(!repeat) {
phase = 0;
holding = 1;
} else if(hold) {
if(alternate) phase ^= 15;
holding = 1;
} else if(alternate) {
attacking ^= 1;
if(!attacking) {
phase--;
} else {
phase++;
}
} else {
phase = !attacking ? 15 : 0;
}
}
//called when writing to the envelope mode register
auto PSG::Envelope::reload() -> void {
holding = 0;
attacking = attack;
phase = !attacking ? 15 : 0;
}
If we're not in continue mode (I call it repeat because continue is a C++ keyword), the PDF says the output drops to 0. So that'd give us:
0,1,2,3,...14,15,0,0,0,0,... or
15,14,13,...,2,1,0,0,0,0
Next, if it's hold mode, we freeze the envelope output (phase) indefinitely. And this also allows to alternate/invert the final output:
0,1,2,...,14,15,0,0,0 or
15,14,13,...,2,1,0,15,15,15...
Next, if it's just alternating, then we flip the attack value (which I've cached to attacking since you can read back the written register values), and so that it looks like a proper triangle wave instead of repeating, we force an increment/decrement after alternating between attack/decay, so:
0,1,2,...,13,14,15,14,13,12,...2,1,0,1,2,...
Whereas without the extra inc/dec we'd end up with:
0,1,2,...,13,14,15,15,14,13,12,...,2,1,0,0,1,2,...
Finally, the last case are the sawtooth style waves where it just keeps repeating:
0,1,2,...13,14,15,0,1,2,...,13,14,15,0,1,2,...
I think this is all correct. Not sure about I/O register writes:
Code: Select all
auto PSG::write(uint8 data) -> void {
switch(io.register) {
case 0:
toneA.period.bits(0, 7) = data.bits(0,7);
break;
case 1:
toneA.period.bits(8,11) = data.bits(0,3);
toneA.unused.bits(0, 3) = data.bits(4,7);
break;
case 2:
toneB.period.bits(0, 7) = data.bits(0,7);
break;
case 3:
toneB.period.bits(8,11) = data.bits(0,3);
toneB.unused.bits(0, 3) = data.bits(4,7);
break;
case 4:
toneC.period.bits(0, 7) = data.bits(0,7);
break;
case 5:
toneC.period.bits(8,11) = data.bits(0,3);
toneC.unused.bits(0, 3) = data.bits(4,7);
break;
case 6:
noise.period = data.bits(0,4);
noise.unused = data.bits(5,7);
break;
case 7:
channelA.tone = data.bit(0);
channelB.tone = data.bit(1);
channelC.tone = data.bit(2);
channelA.noise = data.bit(3);
channelB.noise = data.bit(4);
channelC.noise = data.bit(5);
io.portA = data.bit(6);
io.portB = data.bit(7);
break;
case 8:
channelA.amplitude = data.bits(0,3);
channelA.envelope = data.bit (4);
channelA.unused = data.bits(5,7);
break;
case 9:
channelB.amplitude = data.bits(0,3);
channelB.envelope = data.bit (4);
channelB.unused = data.bits(5,7);
break;
case 10:
channelC.amplitude = data.bits(0,3);
channelC.envelope = data.bit (4);
channelC.unused = data.bits(5,7);
break;
case 11:
envelope.period.bits(0, 7) = data.bits(0,7);
break;
case 12:
envelope.period.bits(8,15) = data.bits(0,7);
break;
case 13:
envelope.hold = data.bit (0);
envelope.alternate = data.bit (1);
envelope.attack = data.bit (2);
envelope.repeat = data.bit (3);
envelope.unused = data.bits(4,7);
envelope.reload();
break;
} //14,15, I/O ports not implemented yet
}
Next up is the mixing. The manual says you can enable tone and/or noise on each channel, so I came up with this:
Code: Select all
auto PSG::mix() -> double {
double output = 0.0;
if((toneA.phase && !channelA.tone) || (noise.lfsr.bit(0) && !channelA.noise)) {
output += amplitudes[channelA.envelope ? envelope.phase : channelA.amplitude];
}
if((toneB.phase && !channelB.tone) || (noise.lfsr.bit(0) && !channelB.noise)) {
output += amplitudes[channelB.envelope ? envelope.phase : channelB.amplitude];
}
if((toneC.phase && !channelC.tone) || (noise.lfsr.bit(0) && !channelC.noise)) {
output += amplitudes[channelC.envelope ? envelope.phase : channelC.amplitude];
}
return output / 3.0;
}
Code: Select all
m_vol_enabled[chan] = (m_output[chan] | TONE_ENABLEQ(chan)) & (NOISE_OUTPUT() | NOISE_ENABLEQ(chan));
Code: Select all
(toneN.phase | channelN.tone) & (noise.lfsr.bit(0) | channelN.noise);
Finally, the last issue is that every last AY-3-8910 implementation has different logarithm values.
MAME has a hardware measurement table here:
https://github.com/mamedev/mame/blob/ma ... 10.cpp#L37
Then later it says:
vol(i) = exp(i/2-7.5)
Then later we have these measurements:
https://github.com/mamedev/mame/blob/ma ... 0.cpp#L722
Other AY-3-8910 cores have different volume scales as well. None of them match. Which values should I be using for the MSX1?
Now getting into MAME notes more:
I ... don't understand any of that.The main difference between the AY-3-8910 and the YM2149 is, that the
AY-3-8910 datasheet mentions, that fixed volume level 0, which is set by
registers 8 to 10 is "channel off". The YM2149 mentions, that the generated
signal has a 2V DC component. This is confirmed by measurements. The approach
taken here is to assume the 2V DC offset for all outputs for the YM2149.
For the AY-3-8910, an offset is used if envelope is active for a channel.
This is backed by oscilloscope pictures from the datasheet. If a fixed volume
is set, i.e. envelope is disabled, the output voltage is set to 0V. Recordings
I found on the web for gyruss indicate, that the AY-3-8910 offset should
be around 0.2V. This will also make sound levels more compatible with
user observations for scramble.
So it's saying that whenever the fixed volume for the channels is set to 0, it means the channel is off.
What does that mean? The counters won't run? But they do on the YM2149?
MAME is only actually using this via zero_is_off in build_3D_table which looks ... insanely complicated.
Reminds me a bit of the NES where the volume of one channel biases the volume of other channels. Is that what this is?
And then the YM2149 has a 2V DC offset ... so a fixed amount to add/subtract by ... only for a fixed volume of zero, or for any volume?
How do I convert a 2VDC offset to an amount to add/subtract by? Is it positive or negative?
And then it's saying ... the AY-3-8910 only gets the offset if the envelope is enabled, and it's 0.2VDC here?
Should I even bother with this? My code runs a 20hz highpass filter on the final output to remove any DC bias anyway.
What is the actual frequency of this chip? The PDF says the counters run on a clock divider of 16, so I've just been doing 3.58MHz/16, and each step at that frequency, clocking the tones/noise/envelope by one.
Then there's this note:
As far as I see, MAME isn't doing anything different between the tone/noise/envelope periods, all three are:Also, note that period = 0 is the same as period = 1. This is mentioned
in the YM2203 data sheets. However, this does NOT apply to the Envelope
period. In that case, period = 0 is half as period = 1.
Code: Select all
counter++;
if(counter >= period) ...
https://github.com/mamedev/mame/blob/ma ... .cpp#L1117
What am I missing there?
... finally, I feel it's best to separate the AY-3-8910 and YM2149 implementations, so for now mine is just strictly AY-3-8910.
It's only a hundred lines of code or so to duplicate, which seems more sensible to me than having m_step, m_env_step_mask, etc.
The big question I'll have for YM2149 is ... if the envelopes now have 32 states, then that means we need a 32-state logarithmic volume table. So, would anyone happen to have actual values for such a table?
Thanks in advance if anyone is able to answer any of these questions ^-^