Recently implemented this chip for an MSX core, but for some reason something else is wrong so I can't test it. Figured I'd ask here in case anyone was familiar with it.
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:
The envelopes get tricky ... MAME is doing all this weird stuff to reduce cases:
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:
Idea was, if we are attacking, and we count from 0 to 15, we should allow the value to actually be 15 for a full cycle, rather than immediately load in a new value once incrementing to 15. I don't see how/if MAME is doing this, but it seems logical.
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:
Do any of these register writes reset the counters to zero, eg for tone, noise, or envelope?
Next up is the mixing. The manual says you can enable tone and/or noise on each channel, so I came up with this:
However, MAME selects channels like this:
In my code it'd be:
The enable bits are inverted, so I'm almost certain these two are equivalent, but I think my way is easier to understand, though I may rename the variables, so it's && !channelC.toneDisable or whatnot.
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.
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:
As a result, period 0 is the same as period 1 even for envelopes:
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 ^-^
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:
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;
}
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;
}
The envelopes get tricky ... MAME is doing all this weird stuff to reduce cases:
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:
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(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;
}
Idea was, if we are attacking, and we count from 0 to 15, we should allow the value to actually be 15 for a full cycle, rather than immediately load in a new value once incrementing to 15. I don't see how/if MAME is doing this, but it seems logical.
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:
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
}
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
}
Do any of these register writes reset the counters to zero, eg for tone, noise, or envelope?
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:
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;
}
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;
}
However, MAME selects channels like this:
Code:
m_vol_enabled[chan] = (m_output[chan] | TONE_ENABLEQ(chan)) & (NOISE_OUTPUT() | NOISE_ENABLEQ(chan));
In my code it'd be:
Code:
(toneN.phase | channelN.tone) & (noise.lfsr.bit(0) | channelN.noise);
The enable bits are inverted, so I'm almost certain these two are equivalent, but I think my way is easier to understand, though I may rename the variables, so it's && !channelC.toneDisable or whatnot.
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:
Quote:
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.
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.
I ... don't understand any of that.
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:
Quote:
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.
in the YM2203 data sheets. However, this does NOT apply to the Envelope
period. In that case, period = 0 is half as period = 1.
As far as I see, MAME isn't doing anything different between the tone/noise/envelope periods, all three are:
Code:
counter++;
if(counter >= period) ...
if(counter >= period) ...
As a result, period 0 is the same as period 1 even for envelopes:
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 ^-^