├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── clap.ogg ├── demo.html ├── litsynth.js └── litsynth.js.md /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padenot/litsynth/0b8f983864d9f331b4fc74f75b044089c922910f/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Paul Adenot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: js html 2 | @echo "All done" 3 | 4 | js: 5 | grep "^ " litsynth.js.md | sed 's/^ //' > litsynth.js 6 | 7 | html: 8 | docco litsynth.js.md 9 | 10 | gh-page: html 11 | cp ./clap.ogg ./docs 12 | cp ./demo.html ./docs 13 | cp ./litsynth.js ./docs 14 | git checkout gh-pages 15 | rm -f litsynth.js.md LICENSE README.md Makefile 16 | cp -r ./docs/* . && rm -R ./docs/* 17 | rmdir docs 18 | mv litsynth.js.html index.html 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | litsynth 2 | ======== 3 | 4 | `litsynth` is a minimal synth for demoscene purposes, written in literate 5 | javascript, using the Web Audio API. 6 | 7 | Links 8 | ===== 9 | 10 | - [Source Code](https://github.com/padenot/litsynth) 11 | - [Documentation](http://padenot.github.io/litsynth/index.html) 12 | - [Demonstration](http://padenot.github.io/litsynth/demo.html) 13 | 14 | License 15 | ======= 16 | 17 | MIT 18 | -------------------------------------------------------------------------------- /clap.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padenot/litsynth/0b8f983864d9f331b4fc74f75b044089c922910f/clap.ogg -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /litsynth.js: -------------------------------------------------------------------------------- 1 | function note2freq(note) { 2 | return Math.pow(2, (note - 69) / 12) * 440; 3 | } 4 | function S(ac, clap, track) { 5 | this.ac = ac; 6 | this.clap = clap; 7 | this.track = track; 8 | this.rev = ac.createConvolver(); 9 | this.rev.buffer = this.ReverbBuffer(); 10 | this.sink = ac.createGain(); 11 | this.sink.connect(this.rev); 12 | this.rev.connect(ac.destination); 13 | this.sink.connect(ac.destination); 14 | } 15 | S.prototype.NoiseBuffer = function() { 16 | if (!S._NoiseBuffer) { 17 | S._NoiseBuffer = this.ac.createBuffer(1, this.ac.sampleRate / 10, this.ac.sampleRate); 18 | var cd = S._NoiseBuffer.getChannelData(0); 19 | for (var i = 0; i < cd.length; i++) { 20 | cd[i] = Math.random() * 2 - 1; 21 | } 22 | } 23 | return S._NoiseBuffer; 24 | } 25 | S.prototype.ReverbBuffer = function() { 26 | var len = 0.5 * this.ac.sampleRate, 27 | decay = 0.5; 28 | var buf = this.ac.createBuffer(2, len, this.ac.sampleRate); 29 | for (var c = 0; c < 2; c++) { 30 | var channelData = buf.getChannelData(c); 31 | for (var i = 0; i < channelData.length; i++) { 32 | channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay); 33 | } 34 | } 35 | return buf; 36 | } 37 | S.prototype.Kick = function(t) { 38 | var o = this.ac.createOscillator(); 39 | var g = this.ac.createGain(); 40 | o.connect(g); 41 | g.connect(this.sink); 42 | g.gain.setValueAtTime(1.0, t); 43 | g.gain.setTargetAtTime(0.0, t, 0.1); 44 | o.frequency.value = 100; 45 | o.frequency.setTargetAtTime(30, t, 0.15); 46 | o.start(t); 47 | o.stop(t + 1); 48 | var osc2 = this.ac.createOscillator(); 49 | var gain2 = this.ac.createGain(); 50 | osc2.frequency.value = 40; 51 | osc2.type = "square"; 52 | osc2.connect(gain2); 53 | gain2.connect(this.sink); 54 | gain2.gain.setValueAtTime(0.5, t); 55 | gain2.gain.setTargetAtTime(0.0, t, 0.01); 56 | osc2.start(t); 57 | osc2.stop(t + 1); 58 | } 59 | S.prototype.Hats = function(t) { 60 | var s = this.ac.createBufferSource(); 61 | s.buffer = this.NoiseBuffer(); 62 | var g = this.ac.createGain(); 63 | var hpf = this.ac.createBiquadFilter(); 64 | hpf.type = "highpass"; 65 | hpf.frequency.value = 5000; 66 | g.gain.setValueAtTime(1.0, t); 67 | g.gain.setTargetAtTime(0.0, t, 0.02); 68 | s.connect(g); 69 | g.connect(hpf); 70 | hpf.connect(this.sink); 71 | s.start(t); 72 | } 73 | S.prototype.Clap = function(t) { 74 | var s = this.ac.createBufferSource(); 75 | var g = this.ac.createGain(); 76 | s.buffer = this.clap; 77 | s.connect(g); 78 | g.connect(this.sink); 79 | g.gain.value = 0.5; 80 | s.start(t); 81 | } 82 | S.prototype.Bass = function(t, note) { 83 | var o = this.ac.createOscillator(); 84 | var o2 = this.ac.createOscillator(); 85 | var g = this.ac.createGain(); 86 | var g2 = this.ac.createGain(); 87 | o.frequency.value = o2.frequency.value = note2freq(note); 88 | o.type = o2.type = "sawtooth"; 89 | g.gain.setValueAtTime(1.0, t); 90 | g.gain.setTargetAtTime(0.0, t, 0.1); 91 | g2.gain.value = 0.5; 92 | var lp = this.ac.createBiquadFilter(); 93 | lp.Q.value = 25; 94 | lp.frequency.setValueAtTime(300, t); 95 | lp.frequency.setTargetAtTime(3000, t, 0.05); 96 | o.connect(g); 97 | o2.connect(g); 98 | g.connect(lp); 99 | lp.connect(g2); 100 | g2.connect(this.sink); 101 | o.start(t); 102 | o.stop(t + 1); 103 | } 104 | S.prototype.clock = function() { 105 | var beatLen = 60 / this.track.tempo; 106 | return (this.ac.currentTime - this.startTime) / beatLen; 107 | } 108 | S.prototype.start = function() { 109 | this.startTime = this.ac.currentTime; 110 | this.nextScheduling = 0; 111 | this.scheduler(); 112 | } 113 | S.prototype.scheduler = function() { 114 | var beatLen = 60 / this.track.tempo; 115 | var current = this.clock(); 116 | var lookahead = 0.5; 117 | if (current + lookahead > this.nextScheduling) { 118 | var steps = []; 119 | for (var i = 0; i < 4; i++) { 120 | steps.push(this.nextScheduling + i * beatLen / 4); 121 | } 122 | for (var i in this.track.tracks) { 123 | for (var j = 0; j < steps.length; j++) { 124 | var idx = Math.round(steps[j] / ((beatLen / 4))); 125 | var note = this.track.tracks[i][idx % this.track.tracks[i].length]; 126 | if (note != 0) { 127 | this[i](steps[j], note); 128 | } 129 | } 130 | } 131 | this.nextScheduling += (60 / this.track.tempo); 132 | } 133 | setTimeout(this.scheduler.bind(this), 100); 134 | } 135 | var track = { 136 | tempo: 135, 137 | tracks: { 138 | Kick: [ 1, 0, 0, 0, 1, 0, 0, 0, 139 | 1, 0, 0, 0, 1, 0, 0, 0, 140 | 1, 0, 0, 0, 1, 0, 0, 0, 141 | 1, 0, 0, 0, 1, 0, 0, 0], 142 | Hats: [ 0, 0, 1, 0, 0, 0, 1, 0, 143 | 0, 0, 1, 0, 0, 0, 1, 1, 144 | 0, 0, 1, 0, 0, 0, 1, 0, 145 | 0, 0, 1, 0, 0, 0, 1, 0 ], 146 | Clap: [ 0, 0, 0, 0, 1, 0, 0, 0, 147 | 0, 0, 0, 0, 1, 0, 0, 0, 148 | 0, 0, 0, 0, 1, 0, 0, 0, 149 | 0, 0, 0, 0, 1, 0, 0, 0], 150 | Bass: [36, 0,38,36,36,38,41, 0, 151 | 36,60,36, 0,39, 0,48, 0, 152 | 36, 0,24,60,40,40,24,24, 153 | 36,60,36, 0,39, 0,48, 0 ] 154 | } 155 | }; 156 | fetch('clap.ogg').then((response) => { 157 | response.arrayBuffer().then((arraybuffer) => { 158 | var ac = new AudioContext(); 159 | ac.decodeAudioData(arraybuffer).then((clap) => { 160 | var s = new S(ac, clap, track); 161 | s.start(); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /litsynth.js.md: -------------------------------------------------------------------------------- 1 | litsynth 2 | ========= 3 | 4 | - [Source Code](https://github.com/padenot/litsynth) 5 | - [Documentation](http://padenot.github.io/litsynth/index.html) 6 | - [Demonstration](http://padenot.github.io/litsynth/demo.html) 7 | 8 | Introduction and scope 9 | ---------------------- 10 | 11 | *litsynth* is the simplest Web Audio API synth module that can be used for 12 | demoscene purposes. It simply plays back a tune using *instruments*, according 13 | to a *score*, using a *scheduler*. 14 | 15 | The important parts of the code are therefore: 16 | - The instruments. Here, a kick drum, a hi-hat and a acid bass synth are 17 | included, perfect for a simple techno tune ; 18 | - The scheduler. It is responsible to schedule the notes, and is the core of the 19 | synth ; 20 | - The score. It tells the scheduler which instrument should be played and when ; 21 | - Effect sends, like reverb and delays. 22 | 23 | A real synth will have much more features. In no particular order: 24 | - Effect automation and LFOs (Low Frequency Oscillators), to be able to modulate 25 | effects and parameters over time ; 26 | - Patterns and playlist instead of this `simplistic` track structure ; 27 | - More instruments, maybe using more advanced synthesis techniques ; 28 | - Being able to decide on the note length, velocity, and to change instrument 29 | parameters for each note ; 30 | 31 | Utility functions 32 | ----------------- 33 | 34 | This utility functions allows us to convert a MIDI note number to a frequency 35 | value. It'll be useful when we'll try to write melodies. 36 | 37 | function note2freq(note) { 38 | return Math.pow(2, (note - 69) / 12) * 440; 39 | } 40 | 41 | The synth 42 | --------- 43 | 44 | This is our main object. It takes an 45 | [AudioContext](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext) 46 | (`ac`) and a `track`. A `track` is a JavaScript object that contains a tempo, a 47 | list of instruments, and the notes they have to play. 48 | 49 | `this.sink` will be the 50 | [AudioNode](https://developer.mozilla.org/en-US/docs/Web/API/AudioNode) to which 51 | all instruments will be connected. This level of indirection will allow us to 52 | easily add global effects, like a reverb, later on if we feel the tune need it. 53 | 54 | `this.clap` is an `AudioBuffer` containing a 808 clap sample. 55 | 56 | function S(ac, clap, track) { 57 | this.ac = ac; 58 | this.clap = clap; 59 | this.track = track; 60 | 61 | We get a convolver to have a global reverb, and we set the sink to a gain node, 62 | that is just here to make a junction: 63 | 64 | this.rev = ac.createConvolver(); 65 | this.rev.buffer = this.ReverbBuffer(); 66 | this.sink = ac.createGain(); 67 | this.sink.connect(this.rev); 68 | this.rev.connect(ac.destination); 69 | 70 | this.sink.connect(ac.destination); 71 | } 72 | 73 | Noises (here, Gaussian noise), are very useful when doing audio synthesis. Here, 74 | we will use noise to create a hi-hat cymbal. 75 | 76 | S.prototype.NoiseBuffer = function() { 77 | 78 | It would be wasteful to recompute a noise buffer each time the hi-hat hits, so 79 | we compute it once and store it. 80 | 81 | if (!S._NoiseBuffer) { 82 | 83 | [AudioBuffer](https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer) hold 84 | samples, associated with a number of channels and a sample rate. They are to be 85 | used with a variety of nodes (they can't simply take, say, a `Float32Array`, but 86 | it is easy to set a `Float32Array` to be the content of an `AudioBuffer` like 87 | so: `AudioBuffer.getChannelData(0).set(float32array)`. 88 | 89 | S._NoiseBuffer = this.ac.createBuffer(1, this.ac.sampleRate / 10, this.ac.sampleRate); 90 | 91 | To be able to write into an `AudioBuffer`, you need to use the 92 | `getChannelData(channel)` method. This gives you a `Float32Array` that you can 93 | modify at will. 94 | 95 | var cd = S._NoiseBuffer.getChannelData(0); 96 | for (var i = 0; i < cd.length; i++) { 97 | 98 | `Math.random()` is in the [0.0; 1.0] interval. Audio samples, in the Web Audio 99 | API, are in the [-1.0; 1.0] interval, so we need to rescale our random noise. 100 | 101 | cd[i] = Math.random() * 2 - 1; 102 | } 103 | } 104 | return S._NoiseBuffer; 105 | } 106 | 107 | 108 | Then, we'll just get a simple decreasing exponential curve, with some noise, to 109 | simulate a reverb, still in an `AudioBuffer`: 110 | 111 | S.prototype.ReverbBuffer = function() { 112 | var len = 0.5 * this.ac.sampleRate, 113 | decay = 0.5; 114 | var buf = this.ac.createBuffer(2, len, this.ac.sampleRate); 115 | for (var c = 0; c < 2; c++) { 116 | var channelData = buf.getChannelData(c); 117 | for (var i = 0; i < channelData.length; i++) { 118 | channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay); 119 | } 120 | } 121 | return buf; 122 | } 123 | 124 | The instruments 125 | --------------- 126 | 127 | Here is our kick drum. Here, we will simply use a pure sine wave, with a 128 | decaying frequency and volume. The `t` parameters tells us when the kick is 129 | supposed to be triggered. 130 | 131 | S.prototype.Kick = function(t) { 132 | 133 | First, we need an 134 | [OscillatorNode](https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode) 135 | to create the sine wave (it defaults to a sine wave shape, so we don't have to 136 | set anything), and a 137 | [GainNode](https://developer.mozilla.org/en-US/docs/Web/API/GainNode) to be able 138 | to alter the volume of sine wave over time. 139 | In the Web Audio API, you always create new `AudioNode` using the `AudioContext`. 140 | `AudioNode`s can't be used in multiple context, but `AudioBuffer` can. 141 | Here, we connect the oscillator to the gain node, and the gain node to the 142 | destination of the synth, in order for the oscillator to be processed by the 143 | gain node. 144 | 145 | var o = this.ac.createOscillator(); 146 | var g = this.ac.createGain(); 147 | o.connect(g); 148 | g.connect(this.sink); 149 | 150 | We start by setting the gain (volume) of the `GainNode` to 1.0, which is the 151 | default. A `GainNode` can be used to amplify or reduce the volume: a value 152 | greater than 1.0 will amplify the volume of the input, whereas a value lesser 153 | than 1.0 will reduce it. 154 | 155 | g.gain.setValueAtTime(1.0, t); 156 | 157 | Percussions sound better when the decay curve is exponential, and not linear. We 158 | use the `setTargetAtTime` method on the `gain` 159 | [AudioParam](https://developer.mozilla.org/en-US/docs/Web/API/AudioParam) to do 160 | so. Here, we reduce the volume to 0.0 (silence), starting a time `t`, with a 161 | linear constant of `0.1`. This third parameter can be thought of as the number 162 | of seconds it takes to reach `1 - 1/Math.E` (around 63.2%). 163 | 164 | g.gain.setTargetAtTime(0.0, t, 0.1); 165 | 166 | Then the pitch. Kick drums are sometimes called bass drum, so they have to have 167 | a low frequency. Let's start at 100Hz, and decay to 30Hz using a time constant 168 | of 0.15. 169 | 170 | o.frequency.value = 100; 171 | o.frequency.setTargetAtTime(30, t, 0.15); 172 | 173 | Finally, we want to start the `OscillatorNode` at `t`, and we want to stop it 174 | sometimes later. Here, we just want to stop it later than the decay envelope, 175 | one second will do. 176 | 177 | o.start(t); 178 | o.stop(t + 1); 179 | 180 | To add a bit more "bite" to the sound, we repeat the same technique, but with a 181 | very short 40Hz square wave. This will add some attack to the sound. 182 | 183 | var osc2 = this.ac.createOscillator(); 184 | var gain2 = this.ac.createGain(); 185 | 186 | osc2.frequency.value = 40; 187 | osc2.type = "square"; 188 | 189 | osc2.connect(gain2); 190 | gain2.connect(this.sink); 191 | 192 | 193 | gain2.gain.setValueAtTime(0.5, t); 194 | gain2.gain.setTargetAtTime(0.0, t, 0.01); 195 | 196 | osc2.start(t); 197 | osc2.stop(t + 1); 198 | } 199 | 200 | Now, onto the hi-hats. This also demonstrate how to play samples with the Web 201 | Audio API. 202 | 203 | S.prototype.Hats = function(t) { 204 | 205 | First, we need to get an 206 | [AudioBufferSourceNode](https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode). 207 | It can be though of as an `AudioBuffer` player. We set its `buffer` property to 208 | the noise buffer we prepared earlier. 209 | 210 | var s = this.ac.createBufferSource(); 211 | s.buffer = this.NoiseBuffer(); 212 | 213 | Next, a `GainNode` to do an envelope, like for the kick drum. 214 | 215 | var g = this.ac.createGain(); 216 | 217 | Finally, we need to get rid of all the low frequency that are present in our 218 | noise buffer. To do so, we will use a 219 | [BiquadFilterNode](https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode), 220 | set to be a high-pass (that lets all the high frequency though, but cuts the low 221 | frequencies), with a cutoff frequency of 5000Hz. The cutoff frequency can be 222 | tuned, different music genre call for different hi-hats. 223 | 224 | var hpf = this.ac.createBiquadFilter(); 225 | hpf.type = "highpass"; 226 | hpf.frequency.value = 5000; 227 | 228 | Once again, we do an exponential envelope, with a time constant chosen by ear. 229 | 230 | g.gain.setValueAtTime(1.0, t); 231 | g.gain.setTargetAtTime(0.0, t, 0.02); 232 | 233 | Then we connect all the nodes, and we start to play the buffer at time `t`. It 234 | will stop playing automatically when the end is reached. 235 | 236 | s.connect(g); 237 | g.connect(hpf); 238 | hpf.connect(this.sink); 239 | 240 | s.start(t); 241 | } 242 | 243 | Onto the clap sound. This is just playing back a buffer: 244 | 245 | S.prototype.Clap = function(t) { 246 | var s = this.ac.createBufferSource(); 247 | var g = this.ac.createGain(); 248 | 249 | s.buffer = this.clap; 250 | 251 | s.connect(g); 252 | g.connect(this.sink); 253 | 254 | g.gain.value = 0.5; 255 | 256 | s.start(t); 257 | } 258 | 259 | Finally, a simple acid bass synth completes our trio of instruments. 260 | 261 | S.prototype.Bass = function(t, note) { 262 | 263 | We need two `OscillatorNode`, a `GainNode` for the envelope, and another gain 264 | node for the volume: 265 | 266 | var o = this.ac.createOscillator(); 267 | var o2 = this.ac.createOscillator(); 268 | var g = this.ac.createGain(); 269 | var g2 = this.ac.createGain(); 270 | 271 | We set the frequency of the oscillators to the current note, and the shape of the 272 | waveform to be a sawtooth. 273 | 274 | o.frequency.value = o2.frequency.value = note2freq(note); 275 | o.type = o2.type = "sawtooth"; 276 | 277 | Once again, a simple envelope. The time constant here is longer than for 278 | percussions. 279 | 280 | g.gain.setValueAtTime(1.0, t); 281 | g.gain.setTargetAtTime(0.0, t, 0.1); 282 | 283 | We set the volume a bit lower: we have two sawtooth wave: 284 | 285 | g2.gain.value = 0.5; 286 | 287 | Now we need a low-pass filter so it sounds good. We add a bit of resonnance to 288 | the filter so that we have an harsh overtone: 289 | 290 | var lp = this.ac.createBiquadFilter(); 291 | lp.Q.value = 25; 292 | 293 | We automate the filter so it has an attack portion: it takes some time to open 294 | up, the beginning of the sound being a bit more soft: 295 | 296 | lp.frequency.setValueAtTime(300, t); 297 | lp.frequency.setTargetAtTime(3000, t, 0.05); 298 | 299 | Connect all the nodes, and start and stop the `OscillatorNode` as previously 300 | explained. 301 | 302 | o.connect(g); 303 | o2.connect(g); 304 | g.connect(lp); 305 | lp.connect(g2); 306 | g2.connect(this.sink); 307 | 308 | o.start(t); 309 | o.stop(t + 1); 310 | } 311 | 312 | The scheduler 313 | ------------- 314 | 315 | This function allows the caller to know when in the tune the synth is, and 316 | possibly to schedule scenes, and the like. It is very important to schedule the 317 | graphics from the music, and not the inverse. 318 | 319 | S.prototype.clock = function() { 320 | var beatLen = 60 / this.track.tempo; 321 | return (this.ac.currentTime - this.startTime) / beatLen; 322 | } 323 | 324 | This function allows to start the synth. It calls the `scheduler` function for 325 | the first time, that will do all the work. 326 | 327 | S.prototype.start = function() { 328 | this.startTime = this.ac.currentTime; 329 | this.nextScheduling = 0; 330 | this.scheduler(); 331 | } 332 | 333 | Now the more complicated part. This function, called repeatedly, will schedule 334 | the instruments to play the right notes at the right time. 335 | 336 | S.prototype.scheduler = function() { 337 | 338 | We need the current time, and we like to have it in beats, and not in seconds, 339 | as it's easier to deal with. `beatLen` tells us how long a beat 340 | is, and allows to convert between beats and seconds. 341 | 342 | var beatLen = 60 / this.track.tempo; 343 | var current = this.clock(); 344 | 345 | Since, when doing demoscene stuff, the main thread is often busy with other 346 | things (graphics, physics, procedural generation of various textures and 347 | geometry, etc.), we want to schedule a little bit ahead, in case the main thread 348 | locks up for some time. We don't really care about latency, here, so let's say 349 | one second is good. 350 | 351 | var lookahead = 0.5; 352 | 353 | If it's time to schedule more sounds, do so, otherwise, just do nothing apart 354 | scheduling this function to be called again soon. 355 | 356 | if (current + lookahead > this.nextScheduling) { 357 | 358 | This synth can schedule notes in quarter beats. We schedule a beat at a time. We 359 | start by storing the time values for quarter beats in an array 360 | 361 | var steps = []; 362 | for (var i = 0; i < 4; i++) { 363 | steps.push(this.nextScheduling + i * beatLen / 4); 364 | } 365 | 366 | Then, for each tracks, and for each quarter beats, we find where in the score we 367 | are, and if there is a note (if the value is not zero), we play it. 368 | 369 | for (var i in this.track.tracks) { 370 | for (var j = 0; j < steps.length; j++) { 371 | var idx = Math.round(steps[j] / ((beatLen / 4))); 372 | 373 | Here, we loop the tune forever. 374 | 375 | var note = this.track.tracks[i][idx % this.track.tracks[i].length]; 376 | if (note != 0) { 377 | this[i](steps[j], note); 378 | } 379 | } 380 | } 381 | 382 | Since we just scheduled some notes, we change the time of the next scheduling to 383 | be a beat further in time. 384 | 385 | this.nextScheduling += (60 / this.track.tempo); 386 | } 387 | 388 | Then, we call this function again soon. In a real demoscene synth, it can be 389 | interesting to simply put the `scheduler` function in the main rendering loop, 390 | as it is supposed to be called once every 16 milliseconds. 391 | 392 | setTimeout(this.scheduler.bind(this), 100); 393 | } 394 | 395 | The score 396 | --------- 397 | 398 | Then, we need to decide on a tempo, and write a score. Here, we are doing 399 | techno, so 140 bpm is good. Kick goes on the beat, and the hi-hat is off-beat. 400 | Clap is every two beats. 401 | 402 | Then, we have a little bass theme. It can be interesting to have patterns and 403 | a playlist instead of directly the notes, so that patterns can be reused. 404 | 405 | 406 | var track = { 407 | tempo: 135, 408 | tracks: { 409 | Kick: [ 1, 0, 0, 0, 1, 0, 0, 0, 410 | 1, 0, 0, 0, 1, 0, 0, 0, 411 | 1, 0, 0, 0, 1, 0, 0, 0, 412 | 1, 0, 0, 0, 1, 0, 0, 0], 413 | Hats: [ 0, 0, 1, 0, 0, 0, 1, 0, 414 | 0, 0, 1, 0, 0, 0, 1, 1, 415 | 0, 0, 1, 0, 0, 0, 1, 0, 416 | 0, 0, 1, 0, 0, 0, 1, 0 ], 417 | Clap: [ 0, 0, 0, 0, 1, 0, 0, 0, 418 | 0, 0, 0, 0, 1, 0, 0, 0, 419 | 0, 0, 0, 0, 1, 0, 0, 0, 420 | 0, 0, 0, 0, 1, 0, 0, 0], 421 | Bass: [36, 0,38,36,36,38,41, 0, 422 | 36,60,36, 0,39, 0,48, 0, 423 | 36, 0,24,60,40,40,24,24, 424 | 36,60,36, 0,39, 0,48, 0 ] 425 | } 426 | }; 427 | 428 | Finally, we get an AudioContext, pass it to the synth along with a track, and 429 | start the tune. 430 | 431 | 432 | fetch('clap.ogg').then((response) => { 433 | response.arrayBuffer().then((arraybuffer) => { 434 | var ac = new AudioContext(); 435 | ac.decodeAudioData(arraybuffer).then((clap) => { 436 | var s = new S(ac, clap, track); 437 | s.start(); 438 | }); 439 | }); 440 | }); 441 | --------------------------------------------------------------------------------