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.
0 Comments
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. When I started this project I had absolutely no idea about how to implement the Spectrum beeper. It did indeed turn out to be quite a challenge, partly because I didn't know anything about coding audio at all and partly because of the way the Spectrum beeper works.
How does the beeper work? In Sinclair BASIC, you can activate the beeper with the Beep command. For example "Beep 2,8" will produce a 2 second tone in G#. Sound is generated by a small speaker (beeper) which is controlled by on/off signals produced by the Spectrum. These on/off signals produce a square wave (in theory at least) with a tone corresponding to the frequency of the on/off signals - a 1-bit sound generator. There is a special routine in ROM for controlling the beeper, and it is precisely timed, based on the Z80 clock frequency. Of course, it is possible to control the beeper directly via machine code, which many games do, often producing multi channel sounds. It should also be noted that the ROM routine halts the computer during audio playback to ensure correct timing. In games this is impractical, so programmers had to create sound routines that could produce consistent sounds even during gameplay. How to emulate the beeper? As mentioned above timing is essential. The problem is that an emulator would be challenged to time every Z80 instruction exactly - at least as far as I know. The SoftSpectrum Z80 emulation timing is based on the CPU interrupt period, which is 1/50.88 second. The emulator processes the same number of instructions as a real Spectrum during an interrupt period. In effect the emulator processes a batch of Z80 instructions once every 50th second, but at much higher speed than a real Z80 CPU, so the instructions are processed in bursts. During the rest of the time it does other things like update the screen, handle keyboard input etc. Plugging the emulator directly to a 1-bit audio generator would of course produce very strange sounds due to the emulator processing speed, so something more needs to be done. The solution I found was to send the signals from the Z80 meant for the beeper to a buffered array, where the length of each pulse corresponds to a number of bytes with a value for on or off in the array. The array is processed by an audio generator (NAudio) to create a sound. While this should work in theory I put down many hours trying to get a decent sound from the emulator. No matter what I did I couldn't get rid of intermittent "stutters" and other artifacts. Finally I stumbled upon a discussion somewhere on the web about the lack of precision in the .NET timer class. Apparently it is well known that it lacks the precision or priority for this kind of application, causing irregular interrupt periods, which led to gaps in the audio feed. Luckily I found the Multimedia Timer class at Codeproject.com, which saved my day! Actually I still have to de-tune the beeper somewhat to avoid stutters, but this should not be noticeable. Update 29 December 2016: The de-tuning mentioned above is no longer needed, after I have improved the beeper functions. |
Archives
November 2020
Categories
All
|