Since the emulated Z80 runs a lot faster than a real Z80, the instructions for the AY chip will be delivered in short bursts every frame (at 50Hz). Because of this, the generated audio signal will not be timed exactly right. Normally, this won't be a problem, considering that the error will be less than 0.02 seconds. However, when the program uses precise timing to generate digital sound, it will not work.
One way to generate digital sound on an AY chip is to set both the tone and noise signals off in the mix. This will result in a permanent high output, which can then be modulated with the volume control to produce a waveform. This technique is used in this demo: b2gemba.tap. To handle this I implemented a class which worked in the same way as the beeper, using the nAudio BufferedWaveProvider. Activated by setting both tone and noise off in the mix, whenever the volume changed, I would feed the buffer with a number of samples corresponding to the number of T-states that had passed since the previous volume change, and with the previous amplitude. The nAudio player would then play back the samples at the correct rate. This worked in principle, but the sound quality was not great. I also found that some programs use another technique which my solution did not support. One such game is Parsec (an excellent game by the way) which modulates the volume of a high pitch signal (pitch value 0) to generate some speech synthesis. To handle this I had to synchronize the regular AY signal with the Z80. After some failed experimenting with extending the buffered wave provider to handle all aspects of the AY signal I choose instead to implement an instruction queue of sorts in my existing solution. Instead of processing instructions sent to the AY directly, the instructions are now placed in a queue, together with the sample number within the frame where the instruction should be processed (the sample number corresponding to the T-state at which the instruction was received). I then added a routine in the signal generator Read-method to pull instructions from the queue at the correct point in time with regard to the current sample number within the frame being processed. This solution now also handles the case where a constant high signal is modulated.
0 Comments
Recently, the game developer Alessandro Grussu discovered that the soundtrack of his new game Sophia II sounded strange on my emulator - specifically that something was wrong with the mix. He kindly provided me with his original PT3 file, which I could open in Vortex Tracker to analyze. After some head scratching and failed remedies (patiently tested by Alessandro), I finally understood that the problem was related to how my emulator translated the AY volume setting to an audio signal amplitude. The AY signal, which is generated by the AYSignalGenerator class, can have an amplitude between 0 and 1. Since the AY signal is played back by the same audio provider as the beeper, the AY signal is limited to a maximum amplitude of 0.1 to achieve balance between the two audio sources. So basically the AY volume setting 0 to 15 needs to be translated to a signal amplitude of 0 to 0.1. I had implemented this in a linear way, so that each AY volume step increased the signal amplitude by 0.00667 (0.1/15). This actually worked out quite well, at least I thought so (maybe because it never crossed my mind that it would be otherwise). However, the Sophia II soundtrack is very dynamic, and with my linear model the dynamics were largely lost. I needed to increase the perceived difference between low and high volume levels, by increasing the signal amplitude exponentially with higher AY volume levels. The solution, based purely on trial and error, was to relate the signal amplitude to the AY volume setting raised to the power of 3 like this: where: A is the signal amplitude (max value = 0.1) V is the AY volume setting (max value = 15) I'm not certain that the above relation is 100% correct, but the result sounds good to me. Update 2019-04-15: I found the correct function for the amplitude on the CPC Wiki, here. I also found this diagram in a manual for the AY-8910, which matches the function well: Update 2020-08-22:
I have replaced the above function with the actual, measured amplitude values reported here. Finally, a big step towards emulating the Spectrum 128 is finished. In a way it was easier than emulating the beeper, since the AY emulation can run on it's own in parallel with the CPU, whereas the beeper needs to be synced precisely with the processing time of each instruction. The AY emulation consists of three classes:
The figure below illustrates how the components interact. As with the beeper, the NAudio library was used for emulating the AY chip. The audio output is handled by the AYController class, via WasapiOut. A mixer is used to handle input from the three AYChannel objects. For generating the square wave and white noise I initially used the SignalGenerator class included in the NAudio library. I then handled envelopes in the AYChannel class, where I used a timer to adjust the signal volume according to the selected envelope pattern. However, I realized that the envelopes can be very fast (kHz), which would require a precision that would be impossible with a timer. I therefore replaced the SignalGenerator with my own (well, to some extent anyway) class where I included envelopes integrated with the actual signal generation, which worked very well. I also had to modify the white noise algorithm to take into account the possibility to set the frequency of the white noise, which was not possible in the original SignalGenerator class.
History
The AY-3-8910 sound chip was introduced by General Instrument in the late seventies. The chip came in different variants and was used in many home computers during the eighties (Atari ST, Amstrad CPC and MSX among others). In 1985 Sinclair released the ZX Spectrum 128 which included the AY-3-8912 sound chip (a variant of AY-3-8910 with fewer pins) to complement the simple beeper used in previous models. General functions The sound chip has three channels, each with a square tone generator and a white noise generator. There is also a volume envelope function which can be applied to modify the signal's attack and decay (also in repeating patterns). Only one envelope can be applied at any time across all three channels but it can be switched on or off per channel. An important point is that the sound chip works like a state machine, meaning that if you set a parameter to a value, this parameter will stay the same until you change it. So, you can't tell the sound chip to produce a signal for a certain time period - you have to tell it exactly when to turn the signal on and off. Registers The AY-3-8910 sound chip has 14 registers for different parameters: Register Function Range 0 Channel A fine tone period 8-bit(0-255) 1 Channel A coarse tone period 4-bit(0-15) 2 Channel B fine tone period 8-bit(0-255) 3 Channel B coarse tone period 4-bit(0-15) 4 Channel C fine tone period 8-bit(0-255) 5 Channel C coarse tone period 4-bit(0-15) 6 Noise period 5-bit(0-31) 7 Mixer 8-bit 8 Channel A volume 4-bit(0-15) 9 Channel B volume 4-bit(0-15) 10 Channel C volume 4-bit(0-15) 11 Envelope fine period 8-bit(0-255) 12 Envelope coarse period 8-bit(0-255) 13 Envelope shape 4-bit(0-15)
Notes about the registers:
Value Shape 0-3: \__________ (decay - silence) 4-7: /|_________ (attack - silence) 8: \|\|\|\|\|\ (repeated decay) 9: \__________ (decay - silence) 10: \/\/\/\/\/\ (repeated decay/attack) 11: \|^^^^^ (decay - max volume) 12: /|/|/|/|/|/ (repeated attack) 13: /^^^^^ (attack - max volume) 14: /\/\/\/\/\/ (repeated attack/decay) 15: /|_________ (attack - silence)
How the Spectrum interacts with the sound chip
The Spectrum controls the sound chip registers by first placing the register number on port $FFFD and then placing the parameter value on port $BFFD. The AY-3-8912 sound chip emulation is now working, but I haven't fully integrated the code yet. In a coming post I will explain how the emulation is implemented. To illustrate the solution, I have put together a simple C# demo which can be downloaded here (source here). As with the beeper functions, it is based on the NAudio library, which has to be added to the source (available via NuGet).
|
Archives
November 2020
Categories
All
|