Foreword
In the previous post we finished off implementing sprites into our C64 emulator.This enabled us to fully play the game Dan Dare within our C64 emulator.
In this post we will start to implement a nice-to-have: Sound emulation.
To implement SID emulation from scratch can be quite a daunting task. So, I did some searching on the Internet to see if I could find an existing SID-implementation, written in Verilog.
As part of this post, I will also show how to create a test bench for evaluating such an implementation.
The chosen SID implementation
After some searching on the Internet, I am game across a nice SID implementation on Github coded in SystemVerilog, by Thomas Kindler. Here is the link to the project:https://github.com/thomask77/verilog-sid-mos6581
I don't have hand-on experience with SystemVerilog, but according to many resources, Vivado does in fact support SystemVerilog. So my SystemVerilog disadvantage shouldn't be much of a setback :-)
Thete was one thing I did experience when using this code with Vivado. For output ports feeding off sequential elements, you need to declare with the 'reg' keyword.
Let us do an overview on how Thomas Kindler's SID module works. Thomas Kindler based much of the inner workings on the Interview with Bob Yannes, the designer of the original SID chip. A copy of this interview can be found on a couple of places on the Internet, including here.
At the heart of each voice on the SID is a 24-bit phase accumulator, clocked at 1MHz.
The phase accumulator is the work horse for generating one complete cycle for the desired waveform at the desired frequency.
Each Voice on the SID have a phase accumulator and gets incremented at each clock cycle by the value stored in its 16 bit frequency register. That is registers 54272+54273 for Voice 1, 54279+54280 for Voice 2, and 54286+54287 for Voice 3.
If a Voice circuit have a frequency value of 1, we will therefore be producing waveforms with a period of 16 seconds, which is a frequency less than 1Hz. On the other hand, a frequency value of 65535 will yield a waveform with a frequency of about 4KHz.
Let us have a look at how the different waveforms gets created.
The triangle waveform is generated as follows:
... out_triangle = acc[22:12] << 1; ... if (acc[23]) out_triangle ^= '1; ...
We are using the lower 23 bits of the phase accumulator. Our waveform starts off increasing till bit 23 of the phase accumulator gets set, after which we do a XOR on the resulting values, giving us a mirrored image of the previous time period.
Generating sawtooth is much simpler:
out_saw = acc[23:12];
And also pulse:
out_pulse = acc[23:12] < pw ? '1 : '0;
Noise gets generated with a Fibonacci sequence, which will not be going into detail here.
I also not be going into the specifics of Envelope generation. I will only mention here that Envelope generation applies ADSR (Attack, Decay, Sustain, Release) to the resulting waveform, and is dealt with in the file sid_env.sv.
Creating the Testbed
The simple part of creating a Testbed for the SID module is wiring up all the ports, supplying a clock signal, and applying some reset logic.The complex part comes to get hold of a sequence of SID register writes that will generate a sound that we can evaluate by ear.
The most obvious way to get hold of such sequences would be to intercept writes to SID registers when executing the applicable program within a C64 emulator.
Some time ago a write an emulator in JavaScript, here, which I am going to use for this purpose. The full source of this emulator can be found here. We should now briefly put our JavaScript thinking caps on :-)
I will try though to keep the discussion on this JavaScript emulator short, trying to convey just the basic idea. Should some of you would like to have a more detailed blog post on this, please drop me a comment.
The place where we will be doing the interception of SID writes, will be within the file memory.js, within the following method:
function IOWrite(address, value) { if ((address >= 0xdc00) & (address <= 0xdcff)) { return ciaWrite(address, value); } else if ((address >= 0xd000) & (address <= 0xd02e)) { return myVideo.writeReg(address - 0xd000, value); } else if ((address >= 0xd800) & (address <= 0xdbe8)) { return myVideo.writeColorRAM (address - 0xd800, value); } else { IOUnclaimed[address - 0xd000] = value; if ((address & 0xff00) == 54272) mysid.log(address & 31, value); return; } }
mysid is the instance of a class that we still need to define, so let us start with the outline of the class:
function sid() { var mycpu; var lasttime = 0; this.setCpu = function(cpu) { mycpu = cpu; } this.log = function(addr, val) { diff = mycpu.getCycleCount() - lasttime; lasttime = mycpu.getCycleCount(); } }
One important thing when recording the writes, is to also record the exact time instance when the write happened. For this purpose we need a handle to the CPU instance to get the current Cycle Count.
With these pieces of information at hand, we can create a series of Verilog statement for the register writes that we can place in an initial-begin..end block. We will write these statements to a text area defined within our HTML page. With these changes are log function looks as follows:
... this.log = function(addr, val) { diff = mycpu.getCycleCount() - lasttime; lasttime = mycpu.getCycleCount(); var ins = document.getElementById("diss"); var temp = ins.value+ "\n#"+diff*10+";\n@(negedge clk)\n"+ "rw = 0; n_cs = 0; addr = 5'd"+addr+ "; data = "+val+";\n@(negedge clk)\n"+"rw=1; n_cs=1;"; ins.value = temp; } ...
A typical sequence of this generated Verilog code looks as follows:
#580; @(negedge clk) rw = 0; n_cs = 0; addr = 5'd11; data = 32; @(negedge clk) rw=1; n_cs=1; #712860; @(negedge clk) rw = 0; n_cs = 0; addr = 5'd0; data = 162; @(negedge clk) rw=1; n_cs=1; #80; @(negedge clk) rw = 0; n_cs = 0; addr = 5'd1; data = 37; @(negedge clk) rw=1; n_cs=1; #200; @(negedge clk) rw = 0; n_cs = 0; addr = 5'd4; data = 32; @(negedge clk) rw=1; n_cs=1;
So, we delay each set of assignments by a certain period of time as captured by the log function.
Interesting, JavaScript generating Verilog code!
Next, let us write some code for capturing the produced sound samples to a file so that we can listen to the produced sound later on:
... integer f = 0; integer i = 0; ... initial begin f = $fopen("sound.raw","wb"); #100; for (i = 0; i < 90000000; i = i + 1) begin @(negedge clk) if ((i% 20) == 1) begin $fwrite(f,"%c",audio[7:0]); $fwrite(f,"%c",audio[15:8]); end end $fclose(f); end
The sound gets produced at a rate of 1MHz. I am reducing the sample rate to 48KHz by catching only every 20th sample. In this way most sound player would be able to keep up.
Audacity is a Opensource program that allows you to import and play these raw samples.
Test Results
Let us listen to the resulting sounds.Our first attempt is kind of successful, but there is a bit of distortion:
One can clearly see the waveform goes off the screen in a couple of places.
Looking at the source file sid_filter.sv of the SID implementation, one kind of get a feeling of where things go wrong:
out_next = (out_next * reg_vol) >> 2;
Here we multiply the final sample with the master volume and divide the result by 4. When inspecting the waveform during a Vivado simulation, multiplying by a master volume of 15 sometimes yield a number that is way past the range of a 16-bit number, and dividing by 4 simply isn't enough. I fix this by dividing by 8 instead of four:
out_next = (out_next * reg_vol) >> 3;
The result is much better, although not taking advantage of the full volume range:
In Summary
In this post we started implementing sound within our emulator.We evaluated Thomas Kindler's SID implementation and found it work very well.
Many thanks for Thomas Kindler for making the source of this implementation available on Github.
In the next post we will continue to integrate this SID core to our C64 core.
Till next time!
No comments:
Post a Comment