Topic: new engine: ulasyn
tldr: 2 pulse channels with lo-pass/hi-pass filters with somewhat variable cutoff.
- 2 pulse wave channels
- variable duty cycle
- duty cycle sweep with variable speed
- noise mode (channel 2 only)
- filters with variable cutoff (6 levels for lo-pass, 5 levels for hi-pass)
- sample rate 9114 Hz
- interrupting PWM sampled drums at 18229 Hz with 7 pitch and 3 volume levels
- player size 3333 bytes (when assembled at a 256b border)
Some points of interest:
- The filter implementation itself is pretty boring, just a lookup into a 14-byte table with values for each pair of (current volume, next raw channel state).
- Noise mode uses a new technique. The engine has one version of the main loop for each volume level, so using self-modifying code to enable/disable the effect was not an option. After just randomly experimenting, I ended up with
; hl is oscillator state
ld a,(MASK)
and h
rlca
xor h
ld h,a
At 29t it's rather expensive, but I really wanted noise mode and could spare the cycles. Depending on MASK, the divider and the duty cycle, this can produce many wildly different sounds. I found that using a mask of 0x76 and a divider of 0x2712 works well to fake standard white noise, with duties from 0x30-0xc0 being somewhat usable for simulating volume levels. Unfortunately the filters are a bit to crude to really work well on noise.
- Using a rather low sample rate for the PWM drums here. Not sure if that's such a great idea, but hey, we've got noise mode to make up for the low upper frequency limit. Originally I wanted to do synth drums instead (using the new noise mode of course), but in the end PWM is much more convenient for the composer.
- Still got some cycles left, so this can be pushed further. Phaser with filters would be really cool, and might be doable. Aside from needing to find some additional registers, the main challenge would be to swap the xor/or/and without smc.
I'm also wondering whether it is possible to increase the cutoff resolution. In theory the number of volume levels can be doubled by going from 16t to 8t per volume level, but that has the problem that we cannot render level 1 properly on ZX beeper. On hardware, this can be done by sacrificing levels 0 and 1 and using level 2 as new level 0, but on emulators this is inherently beepy (that's why zbmod has an "emulator" version). So I don't want to go down that route. In ulasyn, each volume level is generated twice per sample, so one could theoretically jump to the previous or next level half-way. The problem is that if this happens repeatedly at the same volume, we get an audible parasite tone from the mixing. So the filter tables and total volume calculation would need to take that into account. Right now what's being looked up is the answer to the question: Given the current volume 0..6 and the unfiltered next volume (either 0 or 6), what should be the next volume? This needs an added delta, ie: Assuming that the unfiltered next volume does not change,will the volume increase, decrease, or stay the same in the sample after the next one? Based on this, we can select the next volume "core" as follows:
- If both channels want to increase the volume further, jump half-way from volume to volume+1.
- Likewise, if both channels want to decrease the volume further, jump half-way from volume to volume-1.
- Else, do not jump half-way.
This way, repeated jumps from the same initial volume can only occur when the volume lookahead is wrong (because unfiltered state changed from 0 to 6 or vice versa), which should be rare enough to avoid parasite tones. This would give us one additional bit of cutoff precision.