BSP Needs A Rewrite (Again) - bryc/code GitHub Wiki

BSP Needs A Rewrite (Again)

I have encountered many countless issues trying to build a synth and sequencer. In December 2016, I made a simple version which played sequences using setInterval. It's main issue was screwing up when tabs were inactive, as well as audio stuttering when browsing, etc. These kinds of audio glitches are unforgivable - the playback has to be butter smooth, because this is music and any flaws in music is easily perceivable. One benefit of this old build is the fact that it uses start/stop on the OscillatorNode, which means changing waveforms is very possible. The only issue is the precise scheduling of these start/stop events, if possible.

This kind of construction (start/stop) will also be required for proper sample playback using AudioBufferSourceNode, which I have just realized is essentially infeasible with my current setup.

Another method I tried used the onended event of an OscillatorNode to control looping. It lessened the likelihood of stuttering within the sequence (because all notes are scheduled in advance), but stuttering was MUCH more likely during the loop period. So this wouldn't fly as well.

I finally found success when using Web Workers to control the scheduling. This provided me with the butter smooth playback and looping I needed. But this came at a cost, I had to schedule the entire song in one big for loop. This meant that if the song was too complex (like a full quality song might be), it might freeze the browser. And because I abandoned the start/stop commands for each note, it meant I couldn't modify parameters like ADSR or waveform. And sample playback was also out of the question since it relied on start/stop. So it was more like fools gold. I was coding myself into a corner.

Still, I managed to take the concept rather far. BSP is wholly based on that method and I got rather far with it, including simple wavetables, pulse-width modulation and some noise buffers.

But I couldn't do drum samples, and certain things I wanted to do just couldn't be realized this way. This demo showed off a fourier waveform generator that could add harmonics dynamically. But since it required the ability to change waveforms, it couldn't be automated. And even if I could, it'd probably have to be done on a per-note basis (like ProTracker).

BSP2

I started working on something that actually sounded like a real synth, with parameters and all. I called it Apollo. It was a simple synth with one oscillator and one LFO, as well as a filter and delay. It also had a (rather buggy) ADSR envelope. But despite this, it could make some nice sounds. So I excitedly went ahead and tried to implement it in my sequencer.

A couple presets I made:

SqueakyLead::5,8810,2.6,21.4,0.05,0.05,0,0,0,0,0,2.37,0,0,4592,0.156,0.65,0.44,3,228
Bladerunner::0,0,4,0,0.25,1.29,0.53,1,0,18927,0,4.21,0,0.54,-9416,0.102,0.87,0.52,3,120
HardLeadSyn::0,16696,8,0,0.16,0.34,0,0,0,-16078,0,24.2,0,2.9,0,0.122,0.69,0.35,3,432
EurphoricTR::0,13531,9.4,0,0.06,0.75,0.04,1,0,-12397,0,11.7,0,6.4,0,0.28,0.48,1,3,82
KickDrumHmm::3,16648,16.5,0,0.05,0.11,0.05,0,-3358,-23743,0,8,0,4,0,0,0,0,0,396
HotButtered::0,12059,9.4,0,0.01,1.27,0,1,0,-11310,0,11.6,0,6.4,0,0.074,0.75,0.2,3,406

I had a lot of issues with the ADSR, it just didn't work properly, and I knew it'd be problematic to implement back in the BSP player. And with the number of parameters, it would be harder to do automations. That's why I made BSP2 really. It was meant to include all features of Apollo aside from the ADSR envelope. Songs would store the patch data like so:

SONG.patches = [
[0.85,1,0,0,0,0,0,0,0,0,8,0,0,0],
[0.95,1,0.85,0.68,0.226,1,983,1.5,31.1,0,8,4,0,0],
[0.84,1,0.08,0.67,0.256,0,993,15,0,0,3.6,1.56,-110,0.04],
[0.84,1,0.08,0.67,0.256,0,993,15,0,0,3.6,1.56,-110,0.04],
[0.84,1,0.08,0.67,0.256,0,993,15,0,0,3.6,1.56,-110,0.04],
]

Then I tried to make all parameters automatable:

    BSP.BSPSynth.prototype = {
        setParams: function(params, time = 0) {
            this.params = this.params || {};
            var keys = Object.keys(params).map(Number);
            for(var i = 0; i < keys.length; i++) {
                this.params[keys[i]] = params[keys[i]];
                switch(keys[i]) {
                case  0: this.OscGain.gain.setValueAtTime(params[keys[i]],time); break;
                case  1: this.Osc.type = wave[params[keys[i]]]; break;
                case  2: this.DelayGain.gain.setValueAtTime(params[keys[i]],time); break;
                case  3: this.DelayFeedback.gain.setValueAtTime(params[keys[i]],time); break;
                case  4: this.Delay.delayTime.setValueAtTime(params[keys[i]],time); break;
                case  5: this.Filter.type = filt[params[keys[i]]]; break;
                case  6: this.Filter.frequency.setValueAtTime(24000-params[keys[i]]*24,time); break;
                case  7: this.Filter.Q.setValueAtTime(params[keys[i]],time); break;
                case  8: this.Filter.gain.setValueAtTime(params[keys[i]],time); break;
                case  9: this.LFO.type = wave[params[keys[i]]]; break;
                case 10: this.LFO.frequency.setValueAtTime(params[keys[i]],time); break;
                case 11: this.LFOGain1.gain.setValueAtTime(params[keys[i]],time); break;
                case 12: this.LFOGain2.gain.setValueAtTime(params[keys[i]],time); break;
                case 13: this.LFOGain3.gain.setValueAtTime(params[keys[i]],time); break;
                }
            }
        }
    }

I think it was at this point I realized that some parameters couldn't be scheduled, and I ultimately abandoned BSP2 to go back to BSP1.

BSP3 (Third time's the charm?)

I still like BSP1, although it definitely has a fundamentally flawed design. So the goal of BSP3 would be to accomplish what BSP1 set out to do without those design flaws. That goal is thus:

A simple but expressive synthesizer, using bandlimited waveforms (square, saw, etc), pulse-width generators, wavetables, fourier synthesis and sample playback (fur drums). Parameters must support automation. Still a prototype design, but it will pack a punch.

How this will be done, I will need to study a lot of potential areas. Perhaps I can combine setInterval and Web Workers? Or just use Web Workers and a buffered scheduler that still uses stop/start without setInterval. A lot of other synths skip OscillatorNode entirely and generate their audio using AudioBufferSourceNode. An interesting area to explore for sure, but it probably wont solve my timing issues. But looking into how others do it could help.