I’m in the process of adding sound support to my ZX81 iOS emulator app that will be compatible with programs like Kelly Murta's 2005 ZX81 Music Interpreter. The emulator has already had AY support for about 2 years now. To be frank, there are too many holes in my understanding of the mechanics of turning a simple ZX81 cassette out high or low to the end result of musical pitches.
So the purpose of this post is to organise my thoughts and see if you can fill in the gaps in my knowledge. The two other emulators I have been reviewing, EightOne and SZ81, both are based on variants of the sound code source file by Ian Collier. From the comments in the file it looks like the code was originally for the Spectrum Beeper.
Basics:
cassette out low = IN FEh
cassette out high = any OUT
I see that the EightyOne emulator has the condition:
IN if (!(Address&1)) THEN sound_beeper(0);
Whereas SZX81:
if (!NMI_generator && VSYNC_state == 0) && IN FE THEN sound_beeper(1);
Related questions:
1. It looks like the SZ81 implementation has the sound_beeper(0); / sound_beeper(1); conditions are incorrectly inverted. But I don’t think this would have much impact on creating musical tones so long as the wavelength frequency (pitch) is correct. Would you concur?
2. For the cassette out low trigger would it be more correct to say that the NMI also needs to be off? In addition to the SZ81 emulator, I have seen other documentation that suggests the NMI would need to be off.
Spectrum Beeper / ZX81 Cassette out:
From looking at code and comments in source file by Ian Collier it looks like when the low or high level is set the wave amplitude returns back to a silent level. In most emulators of 8 bit systems the sound silent state is 0x80 (128) and anything below this is the low state and above this is the high state. And then as emulated cycle time passes (so long as there are no low/high triggering) the amplitude is incremented / decremented to return to the 0x80 silent position.
Related questions:
3. If a ZX81 user program does not alternate between low and high triggers and say instead does a series of consecutive IN FEh (cassette low) operations without cassette high triggers, will a real ZX81 computer keep setting the amplitude to a low displacement? The reason I ask is that Ian Collier’s code seems to ignore consecutive low or high triggers and instead lets the amplitude return back to a silent level based on when it was last flipped.
4. I assume that the cycles between low and high triggering are critical for determining the pitch. But since the rate that the wave amplitude naturally returns back to a silent level cannot be controlled, wouldn’t the wave form look more like my attached figure ZX81 figure if the high / low are tightly pinched to control the pitch?
Ian Collier’s code:
There’s a few things that don’t make sense in the beeper code. I found the AY code far easier to follow. So I was wondering if you could shed some light on the code. My own code comments are included the the '//' prefix and not the existing '/*' enclosed comments. The code that I do not understand is in labelled with /* START/END ????? */ blocks (cannot use text colour in code comments to make it clearer).
Code: Select all
void sound_beeper(int on)
{
unsigned char *ptr;
int newpos,subpos;
int val,subval;
int f;
// straight forward - just set the amplitude to the volume level with 128/0x80 being
// the halfway point silence level emulator_beeper_volume could be in a range of 0 to 31.
val = (on ? 128 + emulator_beeper_volume : 128 - emulator_beeper_volume);
// see question three above - ignore any beeper triggering
if (val == sound_oldval_orig) {
return;
}
// frametstates = the number of emulated cycles executed in the current emulated
// video display frame.
// sound_framesiz = sound_freq / 50, where sound_freq is the out frequency of the
// emulated device. It’s only 22050 for eightOne but in modern iOS devices for my
// emulator this would be 48000, i.e. 48.0Khz
// 65000 = the number of clock cycles that you would expect per video frame on a 50Hz
// PAL display system, i.e.
// 3.25 MHz / 50 = 3250000 / 50 = 65000
// The following is to locate the position in the data buffer for the current sound frame
// based on the number of cycles that have passed. It is worth noting that in a tight loop
// with closely spaced IN FEh or OUT triggers the same newpos value may be calculated
// because once position in the sound buffer could cover a number of instructions being
// executed by the CPU, e.g.:
// 3.25 MHz = 3250000 so 3250000 / 48000 = 67.7 Tstates can be executed for every
// sound data point
newpos = (frametstates * sound_framesiz) / 65000;
/* START ????? */
// this I really don’t understand - especially the significance of the beeper volume
subpos = (frametstates * sound_framesiz * emulator_beeper_volume) / 65000 - emulator_beeper_volume * newpos;
/* if we already wrote here, adjust the level. */
if (newpos == sound_oldpos) {
/* adjust it as if the rest of the sample period were all in
* the new state. (Often it will be, but if not, we'll fix
* it later by doing this again.)
*/
if (on) {
beeper_last_subpos += emulator_beeper_volume - subpos;
} else {
beeper_last_subpos -= emulator_beeper_volume - subpos;
}
} else {
beeper_last_subpos = (on ? emulator_beeper_volume - subpos:subpos);
}
// AMPL_BEEPER = 31
subval = 128 - AMPL_BEEPER + beeper_last_subpos;
/* END ????? */
if (newpos >= 0) {
// fill gap from previous position - this is easy enough to follow
ptr = sound_buf + sound_fillpos;
for (f=sound_fillpos; f<newpos && f <sound_framesiz; f++) {
// BEEPER_OLDVAL_ADJUST is a simple macro that gradually returns sound_oldval back to
// the silent level (0x80) as per the Spectrum Beeper / ZX81 Cassette out section above
BEEPER_OLDVAL_ADJUST;
*ptr++ = sound_oldval;
}
/* START ????? */
// this again I really don’t get - surely all the above code to arrive at the subval
// would only be applied to a single data position in the sound buffer and it importance
// would be negligible in the long stream of thousands of sound data points that would
// follow. I could understand if its value updated the sound_oldval because it would
// impact all the subsequent sound points.
if (newpos < sound_framesiz) {
/* newpos may be less than sound_fillpos, so... */
ptr = sound_buf + newpos;
/* limit subval in case of faded beeper level,
* to avoid slight spikes on ordinary tones.
*/
if ((sound_oldval < 128 && subval < sound_oldval) || (sound_oldval >= 128 && subval > sound_oldval)) {
subval=sound_oldval;
}
/* write subsample value */
*ptr = subval;
}
/* END ????? */
}
// subval is not passed into the next phase of processing
sound_oldpos = newpos;
sound_fillpos = newpos + 1;
sound_oldval = sound_oldval_orig = val;
}
5. What is the purpose of the code between /* START ????? */ and /* END ????? */?
Thanks in advance for any response to any of the above points.
Kevin