-
Notifications
You must be signed in to change notification settings - Fork 8
Developer journey: Implementing the Head On sound PCB
This page contains a description of how we implemented the sound PCB of the game Head On from SEGA/Gremlin.
We analyzed the schematics of the Head On sound PCB board. Identifying circuits for individual sounds, and repeated parts of the circuit. We talked about what we thought is what, and decided to start with the car sound.
Searching the web for video's with the sounds, we found a couple, but the quality was bad:
JimmyStones contacted the maker of the sound board, to see if he had some tips. He seemed willing to help, but not familiar enough with the fpga domain to join the project. Then JimmyStones found someone who owns a cabinet and was able to obtain some good sound recordings.
This gave us a way to validate our results.
We started by implementing the car sound in the falstad simulator. It took some tweaking, mostly getting things like the diode type and transistor beta values right. We managed to get a result that sounded pretty much like the real deal. This is the final circuit: car_circuit.txt it can be loaded into the falstad simulator From there it is possible to record a wav file, which we used to analyze the sound.
It turns out that the car sound consists of a repeating waveform that looks like this:
The frequency of this waveform is modulated. It starts inaudibly low. Then when the game starts, it follows an upward curve until a limit. When the "high speed" button is pressed it momentarily drops in frequency, then follows a quick upward curve, and follows a downward curve when it is released, like so:
Based on this information we were able to come up wih an implementation strategy:
- Turn the waveform into an array
- Turn the frequency response curve into trhee lookup tables:
- One for the start of the game
- One for when the "high speed" button is pressed
- One for when the "high speed button is not pressed
- Follow the frequnce curve as needed, according to the current state.
For the first version of the sound we simplified the frequency curve to just straight lines.
The bonus sound seemed simple at first, but turned out to be more complicated than we thought. When the bonus input goes high shortly, and then low. The sound is quite simple, like so: We came up with a algorithm to describe this sound, and implemented it in a SystemVerilog module.
Bonus is a pulse generator, that goes to 100% amplitude immediately when the bonus pin goes high.
The pulse is always running, just multiplied by an amplitude.
When the bonus pin goes low, the sound decreases in volume following a an exponential curve.
When the bonus pin is low, the pulse period is 3/4 of the length, resulting in a perfect fourth
The amplitude halves every 28 ms so it's something like:
Amplitude = MaxAmplitude-((0.976^time_in_miliseconds)*MaxAmplitude)
The pulse period when the bonus pin is high has length 0.002746s, and it's high 75% of the time
so equivalent to a loop of:
{1,1,1,0}
The final result looks like it goes through a very slight low pass filter.
at 48khz, results in a loop of:
{97{2}, 1, 33{0}, 1}
MaxAmplitude should be set to the highest number that fits, to keep precision. Normally I use fixed point math with 32 of precision, for multiplications like this. Later we will convert to 16 bits precision, for the output.
We implemented in SystemVerilog this like so:
module bonus(
input clk,
input clk_48KHz_en,
input bonus_en,
output reg[15:0] audio_out = 0
);
localparam MAX_AMPLITUDE = 1 << 18 << 14;
reg[1:0] WAVEFORM_SLOW[131:0];
reg[1:0] WAVEFORM_FAST[98:0];
assign WAVEFORM_SLOW[97] = 1;
assign WAVEFORM_SLOW[131] = 1;
assign WAVEFORM_FAST[73] = 1;
assign WAVEFORM_FAST[98] = 1;
genvar i;
generate
for (i = 0; i <= 96; i = i + 1) begin
assign WAVEFORM_SLOW[i] = 2;
end
for (i = 98; i <= 130; i = i + 1) begin
assign WAVEFORM_SLOW[i] = 0;
end
endgenerate
genvar j;
generate
for (j = 0; j <= 72; j = j + 1) begin
assign WAVEFORM_FAST[j] = 2;
end
for (j = 74; j <= 97; j = j + 1) begin
assign WAVEFORM_FAST[j] = 0;
end
endgenerate
localparam AMPLITUDE_RATIO_PER_TIMESTEP_18 = 262008; // z^(28ms*48khz) = 0.5, so z = 0.99948, 0.99948 * 2^18 = 262008
reg[9:0] current_sample = 0;
reg[68:0] amplitude = 0;
reg last_bonus_en = 0;
localparam SLOW_TO_FAST_RATIO_16 = 87381; // 4/3 * 2^16 = 87381.3333333
reg[15:0] map_slow_to_fast = ((current_sample * SLOW_TO_FAST_RATIO_16) >> 16);
always_ff @(posedge clk) begin
if(clk_48KHz_en)begin
last_bonus_en <= bonus_en;
if(bonus_en)begin
amplitude <= MAX_AMPLITUDE;
if(current_sample == 131)begin
current_sample <= 0;
end else begin
current_sample <= current_sample + 1;
end
audio_out <= (amplitude * WAVEFORM_SLOW[current_sample]) >> 18;
end else begin
if(last_bonus_en)begin
current_sample <= map_slow_to_fast;
audio_out <= (amplitude * WAVEFORM_SLOW[current_sample]) >> 18;
end else begin
if(current_sample >= 98)begin
current_sample <= 0;
end else begin
current_sample <= current_sample + 1;
end
audio_out <= (amplitude * WAVEFORM_FAST[current_sample]) >> 18;
end
amplitude <= (AMPLITUDE_RATIO_PER_TIMESTEP_18 * amplitude) >> 18;
end
end
end
endmodule
But it turned out that this was not the whole story. At the end of the game, the bonus pin goes high for a longer time, which triggers a different effect, where the frequency of the tone gets modulated in a more complex way. We had to go back to simulation, where it turned out that the NOT gated at the bottom had the wrong voltage threshold assigned to them.
While this did not change the short bonus sound, which the machine makes when a jewel is picked up, it does change the sound when the bonus pin goes high for a longer period. It seemed like the Falstad simulator wasn't fast enough to do the simulation, but after tuning the initial voltages of the capacitors, the system got into a simulatable state.
This was what came out of the simulation, which gave some insight about which frequencies to use. But listening to the recordings from teh real cabinet there were still some differences:
- the real sound goes up and down faster
- the real sound stays low longer and high shorter
- the real sound has a slide between the fequencies
The simulation also has a slide going from high to low, but it is more prominent/slower the real recording.
Also with this improved simulation, the blip sound when coins are picked up turned out to be slightly different.
The amplitude goes down, before the frequency goes up.
Aslo the frequency of the high tone is higher than we originally thought, we checked in the recordings from the real cabinet, where it was also higher.
Based on these new findings we improved our implementation of the bonus sound:
module bonus(
input clk,
input clk_48KHz_en,
input bonus_en,
output reg[15:0] audio_out = 0
);
localparam MAX_AMPLITUDE = 1 << 18 << 14;
reg[1:0] WAVEFORM_SLOW[131:0];
reg[1:0] WAVEFORM_FAST[98:0];
assign WAVEFORM_SLOW[97] = 1;
assign WAVEFORM_SLOW[131] = 1;
assign WAVEFORM_FAST[57] = 1;
assign WAVEFORM_FAST[78] = 1;
genvar i;
generate
for (i = 0; i <= 96; i = i + 1) begin
assign WAVEFORM_SLOW[i] = 2;
end
for (i = 98; i <= 130; i = i + 1) begin
assign WAVEFORM_SLOW[i] = 0;
end
endgenerate
genvar j;
generate
for (j = 0; j <= 56; j = j + 1) begin
assign WAVEFORM_FAST[j] = 2;
end
for (j = 58; j <= 77; j = j + 1) begin
assign WAVEFORM_FAST[j] = 0;
end
endgenerate
localparam AMPLITUDE_RATIO_PER_TIMESTEP_18 = 262008; // z^(28ms*48khz) = 0.5, so z = 0.99948, 0.99948 * 2^18 = 262008
reg[32:0] current_sample = 0;
reg[68:0] amplitude = 0;
reg last_bonus_en = 0;
reg[15:0] bonus_en_ago = 0;
localparam SLOW_TO_FAST_RATIO_16 = 39222; // 79/132 * 2 ^ 16 = 39222.3030303
reg[32:0] map_slow_to_fast = ((current_sample * SLOW_TO_FAST_RATIO_16) >> 16);
reg[32:0] bonus_en_length = 0;
always_ff @(posedge clk) begin
if(clk_48KHz_en)begin
if(bonus_en || bonus_en_ago < (11 * 132))begin
bonus_en_length <= bonus_en_length + 1;
if(current_sample == 131)begin
current_sample <= 0;
end else begin
current_sample <= current_sample + 1;
end
if(bonus_en_length >= 55 * 132)begin
if(current_sample >= 78)begin
current_sample <= 0;
audio_out <= (amplitude * WAVEFORM_FAST[0]) >> 18;
end else begin
current_sample <= current_sample + 1;
audio_out <= (amplitude * WAVEFORM_FAST[current_sample]) >> 18;
end
if(bonus_en_length == 75 * 132) begin
bonus_en_length <= 0;
end
end else begin
audio_out <= (amplitude * WAVEFORM_SLOW[current_sample]) >> 18;
end
if(~bonus_en)begin
bonus_en_ago <= bonus_en_ago + 1;
last_bonus_en <= 1;
amplitude <= (AMPLITUDE_RATIO_PER_TIMESTEP_18 * amplitude) >> 18;
end else begin
amplitude <= MAX_AMPLITUDE;
bonus_en_ago <= 0;
end
end else begin
if(last_bonus_en)begin
bonus_en_length <= 0;
last_bonus_en <= 0;
current_sample <= map_slow_to_fast;
audio_out <= (amplitude * WAVEFORM_FAST[map_slow_to_fast]) >> 18;
end else begin
if(current_sample >= 78)begin
current_sample <= 0;
end else begin
current_sample <= current_sample + 1;
end
audio_out <= (amplitude * WAVEFORM_FAST[current_sample]) >> 18;
end
amplitude <= (AMPLITUDE_RATIO_PER_TIMESTEP_18 * amplitude) >> 18;
end
end
end
endmodule
The only thing that is missing now, is the slide between the frequencies. It is left for later improvement.
The crash sound consists of a noise source that switches two oscilators on and off. The oscilators are based on 555 timers, which are out of tune, that go through their own signal path.
An in-depth analysis:
We simulated the circuit:
The real noise source is a 18 bit lsfr, in the simulation we used a built-in noise source of the simulator, but it worked out nicely. It took some tweaking and improving to get the simulation to work, but it sounded like the real thing in the end.
It's a difficult sound to convert into an algorithm. It would probably involve some kind of sine continuation like found in the bang sound of red baron, but it's not exactly clear to us how the opamp circuits here work.
The bang sound from red baron has a similar, but not idenitcal opamp circuit:
We decided to take a long sample of the resulting noise, which will fade in quickly , with a random offset, when the crash pin goes high, and it will fade out when the crash pin goes low.
The result was the follow implementation: crash_sample.sv
module CrashSound
(
input clk,
input clk_48KHz_en,
input play,
output reg[15:0] audio
);
reg[15:0] crash_sample[26144];
int sample_counter = 0;
always @(posedge clk) begin
if (~play) begin
sample_counter <= 0;
audio <= 0;
end else if (clk_48KHz_en && sample_counter < 26144) begin
audio <= crash_sample[sample_counter];
sample_counter <= sample_counter + 1;
end
end
initial begin
`include "crash_sample.sv"
end
endmodule
We used the tool from arcade battlezone to conver the wav into a reg-assignment.
MiSTer FPGA Offical Home Page - Open Source GPL 3.0 - project discussion forum
- Tutorials and Reference
- Welcome to MiSTer
- Inputs
- Network Communications
- Extra Features
- Addons overview
- How to get boards?
- SDRAM Board
- IO Board
- Direct Video
- Analog video output compatibility
- Using CRT TVs & monitors
- RTC board
- USB Hub
- ADC-in (Audio/Tape input)
- Case
- Unofficial Addons