In the end I decided to have 8 bytes to describe the complete sound of a single voice output. I gave the Kwak-8 eight voices sharing the same output ports with a voice select register. So, having decided to have 8 bytes for each voice, the question then becomes how do I allocate those 64 bits to features.
The easy bits
- Frequency - 16 bits This is an important one so gets a lot of bits. I went for the same scaling as an NTSC SID at register_value*0.0596Hz
- Volume - 8 bits 256 levels should be plenty for fading in and out of sound effects
The WaveForm
Four commonly used simple waveforms turn up a lot in sound synthesis, Sine, Square, Triangle and SawTooth. I decided to provide the first three of these
initially I did a straight linear interpolation between thw wave outputs.
Here's how it looks with scaling.
To fix this I needed something that transitions to a square wave by increasing the slope
instead of adding sharp transitions of increaing size. A sigmoid function looked like a good
candidate. using a function of
y = 1 / ( 1 + Math.exp(x * slopeFactor))
gives a controllable slope.
Tweaking The Wave
To make a wider variety of sounds (wikipedia tells me I'm talking about timbre here, I really have no idea what I'm doing). I provided a way to adjust the shape of the wave. Applying a kind of gamma curve to the time domain can make the waveform front heavy or back heavy.
I tried applying a function that weighted the tips of the gamma curve towards linear. gamma curve linear. This produced a rather unexpected (to me anyway) result of making the curve bend back down in places, given this is in the time domain that means it ends up playing part of the waveform backwards. Nevertheless it does sound interesting and stands a chance of staying in the mix.
If you take away just one thing from this post, making a function like this is a good choice.
/**
* Genrate a sigmoid function for a given weight
* param (number) weight - in the range -1 to +1 controls the steepness of the bend,
* weight=0 is linear.
* less than zero pulls the center towards vertical
* greater than zero pulls the center towards horizontal
*/
function makeSigmoidFunction (weight=0.5, low=0, high=1) {
let range = high-low;
if (weight==0) return a=>a*range+low;
let w = 0.4999/weight-0.5; //just a tad below |1| at the ends
let mid = (high+low)/2;
let xlate = a=>Math.min(1,Math.max(-1,(a*2-1)));
return a => (xlate(a) * w / (w - Math.abs(xlate(a)) +1) ) / 2 * range + mid;
}
Applying the sigmoid to the time domain makes soemthing that looks interesting, Listening to the changes in the sound, it seems like not too bad as a pick,
I'm not entirely sure whether or not a symetrical waveform tilted to the left sounds different to one tilted to the right.
If it turns out the top bit is redundant I might appropriate it for something else at a later date.
- Wave Type - 4 bits Square, sine and triangle with interpolations
- Wave Shift - 4 Bits Apply sigmoid to time domain
Pitch bend
This part was much easier to decide upon. The bend value is just a sine wave with phase and frequency control. The result of the bend output changes the frequency of the playing voice. A low frequency bend can cause a subtle change in pitch and usually will only cover a part of the sine wave. The phase lets you choose whether to start by increasing in pitch or lowering in pitch. At high frequencies it causes a siren and higher still a vibrato.phase
frequency
- bend phase - 3 bits indicating the position in the sine wave to start the bend
- bend Frequency - 5 bits setting the frequency to Math.pos((value+1)/33,2)*30; ranging from a minimum frequency of 0.02Hz to a maximum of 28Hz
- bend amplitude - 8 bits How much bend to apply. Zero is no bend at all. This gets a full 8 bits so that you can fade the bend effect in and out.
The Envelope (and a little noise)
I decided against the classic Attack/Decay/Sustain/Release envelope which is more designed for musical keyboards. The Sustain level assumes an active participant holding the note, If a CPU has to keep paying attention to the note to hold it, it can dynamically decide on any envelope by adjusting the voice volume directly.For an easier fire-and-forget model I went for a simple Attack/Hold/Release envelope. Three durations are specified for fade-in, hold, fade-out. Instead of a Decay phase I elected to use the remaining bits to apply variable levels of noise to the voice.
Thus the last two bytes of the voice registers are:- Attack - 4 bits Duration of fade up to full volume. (Value / 8)² Seconds. Max: 3.5 seconds
- Release - 4 bits Duration of fade out after hold duration ends. (Value / 8)² Seconds. Max: 3.5 seconds
- Hold - 4 bits How long to hold the sound. (Value / 8)² Seconds. Max: 3.5 seconds
- Noise - 4 bits random noise mixed into the sample 0=no noise. 15=all noise
Putting it together
Finally came the task of placing it into the Kwak-8 emulator. This was a surprisingly easy task. During the design phase I created a very simple tester page. Have a play around with it if you like This let me see and hear the result as I tweaked things. By the time I needed to add it to the emulator I had a working template. The most awkward part was having to convert arrow functions to long form. The Emulator is written in haxe, arrow functions are coming to Haxe ( 4.0.0 preview builds have them ), they just aren't in the release I'm using right now.And finally, finally. I had to make a program for the Kwak-8 that actually played sound.
The Emulator is on https://lerc.github.io/kwak-8/ It has an Audio Test program where you can adjust the voice registers in-emulator, and play on a little on-screen keyboard.
Go and have a play with it, It's kinda fun.