Random Beat Sequencer (AudioContext Oscillators FTW) ('-' * 52) Takes composition data, and data relative to each instrument and generates a random beat sequence.
A Pen by Jake Albaugh on CodePen.
| <h1>Random Beat Sequencer</h1> | |
| <div id="composition"> | |
| <div class="header"> | |
| <div class="pattern"> | |
| <div class="bar"> | |
| <div class="beats"></div> | |
| </div> | |
| </div> | |
| <div class="label"></div> | |
| </div> | |
| <div class="rhythms"> | |
| </div> | |
| <span id="indicator"></span> | |
| </div> | |
| <div class="triggers"> | |
| <a id="play"> | |
| <span class="play">Play</span> | |
| <span class="stop">Stop</span> | |
| </a> | |
| <a id="new">New</a> | |
| </div> | |
| <article> | |
| <main> | |
| <h1>Why this exists</h1> | |
| <p>I have become more and more intrigued by the idea of creating order out of chaos. I recently made a <a href="http://codepen.io/jakealbaugh/pen/WbwWag" target="blank">Random "Word" Generator</a>. I decided that I would like to try and do a similar thing with music, and rhythms are a great starting point. Maybe I was late to the game, but I just recently learned that you can create and play oscillators in webkit browsers which is pretty incredible, so I had to try it out.</p> | |
| <h1>How it works</h1> | |
| <h2>The Composition Object</h2> | |
| <p>The composition object consists of composition-level parameters.</p> | |
| <ul> | |
| <li><strong>bars:</strong> the number of bars to generate</li> | |
| <li><strong>beats:</strong> how many beats per bar</li> | |
| <li><strong>resolution:</strong> the beat resolution (grid size)</li> | |
| <li><strong>bpm:</strong> beats per minute</li> | |
| </ul> | |
| <pre>composition = { | |
| bars: 2, | |
| beats: 4, | |
| resolution: 16, | |
| bpm: 90 | |
| }</pre> | |
| <h2>Instrument Objects</h2> | |
| <p>Instrument objects consist of pattern and oscillator parameters.</p> | |
| <ul> | |
| <li><strong>name:</strong> the name of the instrument</li> | |
| <li><strong>min:</strong> the minimum beat (can happen on every ___ note)</li> | |
| <li><strong>tendency:</strong> {object} | |
| <ul> | |
| <li>Tends to be on a ___ note starting on beat ___. </li> | |
| <li><strong>every:</strong> the first blank</li> | |
| <li><strong>on:</strong> the second blank</li> | |
| </ul> | |
| </li> | |
| <li><strong>presence:</strong> the chance of appearing on every minimum beat (is increased on tendency)</li> | |
| <li><strong>tone:</strong> {object} | |
| <ul> | |
| <li><strong>freq:</strong> oscillator frequency</li> | |
| <li><strong>sustain:</strong> note sustain</li> | |
| <li><strong>wave:</strong> oscillator waveform</li> | |
| </ul> | |
| </li> | |
| </ul> | |
| <pre>kick = { | |
| name: 'kick', | |
| min: 1 / 8, | |
| tendency: {every: 1/2, on: 1}, | |
| presence: 0.3, | |
| tone: {frequency: 60, sustain: 0.2, wave: 'square'} | |
| }</pre> | |
| <h2>Putting it together</h2> | |
| <p>Each instrument gets fed along with the composition into a rhythm generator that uses the parameters to generate a random rhythm for that instrument. The rhythm's beats output is nothing more than an array of zeroes (misses), ones (hits), and falses (below minimum beats).</p> | |
| <p>When a new rhythm is created, it gets an `osc` function to create a new AudioContext oscillator. The rhythm's tone parameters get fed in to that oscillator.</p> | |
| <pre>Rhythm = (composition, params) -> | |
| <strong># unpacking params</strong> | |
| {name, min, tendency, presence, tone} = params | |
| <strong># next common beat</strong> | |
| if tendency then commonbeat = tendency.on | |
| beats = [] | |
| <strong># for each bar</strong> | |
| for bar in [1..composition.bars] | |
| <strong># next beat</strong> | |
| nextbeat = 1 | |
| <strong># for each beat</strong> | |
| for beat in [1..(composition.beats * (composition.resolution / 4))] | |
| <strong># if next beat on grid is our minimum length</strong> | |
| if nextbeat % beat == 0 | |
| nextbeat += composition.resolution * min | |
| <strong># if common, increase potential hit</strong> | |
| if commonbeat && (beat == commonbeat) | |
| <strong># increase potential hit by 20% for tendency beats</strong> | |
| freq = presence * 1.2 | |
| <strong># set up next common beat</strong> | |
| commonbeat += tendency.every * composition.resolution | |
| else | |
| freq = presence | |
| <strong># get our random chance</strong> | |
| chance = Math.random() | |
| <strong># push pos or neg value</strong> | |
| if chance < freq then beats.push 1 else beats.push 0 | |
| else | |
| beats.push false | |
| <strong># return the rhythm</strong> | |
| this.name = name | |
| this.beats = beats | |
| this.tone = tone | |
| this.osc = () -> | |
| return new Oscillator(tone) | |
| return this</pre> | |
| <p>Rhythm output:</p> | |
| <ul> | |
| <li><strong>name:</strong> the name of the rhythm instrument</li> | |
| <li><strong>beats:</strong> array of zeroes (misses), ones (hits), and falses (below minimum beats).</li> | |
| <li><strong>tone:</strong> orginial tone object</li> | |
| <li><strong>osc: </strong> method() | |
| <ul> | |
| <li>Generates a new tone-specific Oscillator.</li> | |
| <li>According to the Oscillator spec, you need to create a new oscillator for every note played.</li> | |
| <li>Rhythm.osc() can be called to generate a new oscillator using its unique tone parameters.</li> | |
| <li>Inside of the new Oscillator (below), the play() method plays the oscillator using the tone params</li> | |
| <li>In the loop, when we want to play the rhythm's oscillator, we create a new Oscillator with the .osc() method, then call that new Oscillator's play() method.</li> | |
| </ul> | |
| </li> | |
| </ul> | |
| <pre>Oscillator = (tone) -> | |
| this.tone = tone | |
| this.play = () -> | |
| <strong># capturing current time for play start and stop</strong> | |
| current_time = audio_context.currentTime | |
| <strong># create oscillator </strong> | |
| osc = audio_context.createOscillator() | |
| <strong># set frequency </strong> | |
| osc.frequency.value = this.tone.frequency | |
| <strong># set waveform </strong> | |
| osc.type = this.tone.wave | |
| <strong># connect to context </strong> | |
| osc.connect audio_context.destination | |
| <strong># play it </strong> | |
| osc.start(current_time) | |
| <strong># stop after sustain </strong> | |
| osc.stop(current_time + this.tone.sustain) | |
| return this</pre> | |
| <h2>The Loop</h2> | |
| <p>To play the beat, there is a looping function that steps through each rhythm array and plays the rhythms oscillator if there is a value.</p> | |
| <pre>loop_beats = (composition, rhythms) -> | |
| index = 0 | |
| <strong># total beat count</strong> | |
| beats = composition.bars * (composition.beats * (composition.resolution / 4)) | |
| <strong># css width of beat </strong> | |
| beat_width = 100 / beats | |
| <strong># indicator element </strong> | |
| $indicator = $('#indicator') | |
| <strong># single beat instance</strong> | |
| next_beat = () -> | |
| for rhythm in rhythms | |
| <strong># if beat in any rhythm array has value </strong> | |
| if rhythm.beats[index] == 1 | |
| <strong># new oscillator </strong> | |
| o = rhythm.osc() | |
| <strong># play oscillator</strong> | |
| o.play() | |
| <strong># remove active class from all beats in previous bar</strong> | |
| $('.beat.active').removeClass 'active' | |
| <strong># add active class to all beats in this bar</strong> | |
| $('.beat:nth-child(' + (index + 1) + ')').addClass 'active' | |
| <strong># position indicator </strong> | |
| $indicator.css({'left': beat_width * index + '%'}) | |
| <strong># update index </strong> | |
| index = (index + 1) % beats | |
| <strong># first call of next beat</strong> | |
| next_beat() | |
| <strong># bpm to ms </strong> | |
| bpm_time = 60000 / composition.bpm | |
| <strong># ms to relative speed (based on resolution) </strong> | |
| time = bpm_time / (composition.resolution / 4) | |
| <strong># set interval for next beat to occur at approriate time</strong> | |
| beat_interval = window.setInterval(next_beat, time)</pre> | |
| <h2>What Ive Learned</h2> | |
| <p>AudioContext and oscillators are really fun. There is so much potential for "auralizing" data.</p> | |
| </main> | |
| </article> |
Random Beat Sequencer (AudioContext Oscillators FTW) ('-' * 52) Takes composition data, and data relative to each instrument and generates a random beat sequence.
A Pen by Jake Albaugh on CodePen.
| # initiate audio context | |
| audio_context = undefined | |
| (init = (g) -> | |
| try | |
| # "crossbrowser" audio context. | |
| audio_context = new (g.AudioContext or g.webkitAudioContext) | |
| catch e | |
| console.log "No web audio oscillator support in this browser" | |
| return | |
| ) window | |
| # oscillator prototype | |
| Oscillator = (tone) -> | |
| this.tone = tone | |
| this.play = () -> | |
| # capturing current time for play start and stop | |
| current_time = audio_context.currentTime | |
| # create oscillator | |
| osc = audio_context.createOscillator() | |
| # create gain | |
| gn = audio_context.createGain() | |
| # set frequency | |
| osc.frequency.value = this.tone.frequency | |
| # set waveform | |
| osc.type = this.tone.wave | |
| # connect oscillator to gain | |
| osc.connect gn | |
| # connect gain to output | |
| gn.connect audio_context.destination | |
| # set gain amount | |
| gn.gain.value = 0.3 | |
| # play it | |
| osc.start(current_time) | |
| # stop after sustain | |
| osc.stop(current_time + this.tone.sustain) | |
| return this | |
| # building a rhythm | |
| Rhythm = (composition, params) -> | |
| # unpacking params | |
| {name, min, tendency, presence, tone} = params | |
| # next common beat | |
| if tendency then commonbeat = tendency.on | |
| beats = [] | |
| # for each bar | |
| for bar in [1..composition.bars] | |
| # next beat | |
| nextbeat = 1 | |
| # for each beat | |
| for beat in [1..(composition.beats * (composition.resolution / 4))] | |
| # if next beat on grid is our minimum length | |
| if nextbeat % beat == 0 | |
| nextbeat += composition.resolution * min | |
| # if common, increase potential hit | |
| if commonbeat && (beat == commonbeat) | |
| # increase potential hit by 20% for tendency beats | |
| freq = presence * 1.2 | |
| # set up next common beat | |
| commonbeat += tendency.every * composition.resolution | |
| else | |
| freq = presence | |
| # get our random chance | |
| chance = Math.random() | |
| # push pos or neg value | |
| if chance < freq then beats.push 1 else beats.push 0 | |
| else | |
| beats.push false | |
| # return the rhythm | |
| this.name = name | |
| this.beats = beats | |
| this.tone = tone | |
| this.osc = () -> | |
| return new Oscillator(tone) | |
| # console.log this | |
| return this | |
| # beat interval for clearing later | |
| beat_interval = undefined | |
| # loop beats | |
| loop_beats = (composition, rhythms) -> | |
| index = 0 | |
| # total beat count | |
| beats = composition.bars * (composition.beats * (composition.resolution / 4)) | |
| # css width of beat | |
| beat_width = 100 / beats | |
| # indicator element | |
| $indicator = $('#indicator') | |
| # single beat instance | |
| next_beat = () -> | |
| for rhythm in rhythms | |
| # if beat in any rhythm array has value | |
| if rhythm.beats[index] == 1 | |
| # new oscillator | |
| o = rhythm.osc() | |
| # play oscillator | |
| o.play() | |
| # remove active class from all beats in previous bar | |
| $('.beat.active').removeClass 'active' | |
| # add active class to all beats in this bar | |
| $('.beat:nth-child(' + (index + 1) + ')').addClass 'active' | |
| # position indicator | |
| $indicator.css({'left': beat_width * index + '%'}) | |
| # update index | |
| index = (index + 1) % beats | |
| # first call of next beat | |
| next_beat() | |
| # bpm to ms | |
| bpm_time = 60000 / composition.bpm | |
| # ms to relative speed (based on resolution) | |
| time = bpm_time / (composition.resolution / 4) | |
| # set interval for next beat to occur at approriate time | |
| beat_interval = window.setInterval(next_beat, time) | |
| # metronome oscillator (unfinished) | |
| ### | |
| quarters = 1 | |
| if (index + 1) % 4 == 0 | |
| if (quarters - 1) % composition.beats == 0 || quarters == 1 | |
| frequency = 3200 | |
| else | |
| frequency = 3000 | |
| quarters++ | |
| else | |
| frequency = 3000 | |
| metronome = new Oscillator({frequency: frequency, sustain: 0.025, wave: 'sine'}) | |
| if (index + 1) % 4 == 0 | |
| metronome.play() | |
| ### | |
| # draw beat in dom | |
| draw_beat = (comp, rhythms) -> | |
| # total beat count | |
| beats = composition.bars * (composition.beats * (composition.resolution / 4)) | |
| # css beat width | |
| beat_width = 100 / beats + '%' | |
| # indicator | |
| $indicator = $('#indicator') | |
| # set indicator width | |
| $indicator.css({'width': beat_width}) | |
| # main timeline | |
| header_beats = '' | |
| header_label = comp.bpm + ' bpm ' + comp.beats + '/4' | |
| for bar in [1..comp.bars] | |
| start = 1 | |
| beat = 1 | |
| for resolution in [1..(comp.beats * (comp.resolution / 4))] | |
| if resolution == start | |
| x = beat | |
| beat++ | |
| start += (comp.resolution / 4) | |
| else if (resolution - 1) % ((comp.resolution / 4) / 2) == 0 | |
| x = '+' | |
| else if (resolution - 1) % ((comp.resolution / 4) / 4) == 0 | |
| x = '' | |
| else | |
| x = ' ' | |
| header_beats += ('<span class="beat" symbol="' + x + '" style="width: ' + beat_width + '"></span>') | |
| # for each instrument | |
| instruments = '' | |
| for rhythm in rhythms | |
| instruments += '<div class="rhythm"><div class="pattern"><div class="bar"><div class="beats">' | |
| beat_i = 1 | |
| for beat in rhythm.beats | |
| beat_i++ | |
| if beat == 0 || beat == false | |
| c = '' | |
| else | |
| c = 'hit' | |
| instruments += '<span symbol="•" class="beat ' + c + '" style="width: ' + beat_width + '"></span>' | |
| instruments += '</div></div></div><div class="label">' + rhythm.name + '</div></div>' | |
| $('#composition .header .beats').html(header_beats) | |
| $('#composition .header .label').html(header_label) | |
| $('#composition .rhythms').html(instruments) | |
| # beat components | |
| composition = undefined | |
| kick = undefined | |
| snare = undefined | |
| hats = undefined | |
| # composition-level parameters | |
| composition_params = { | |
| bars: 2 # bars to generate | |
| beats: 4 # beats per bar | |
| resolution: 16 # beat resolution | |
| bpm: 120 # beats per minute | |
| } | |
| kick_params = { | |
| name: 'kick' # name of instrument | |
| min: 1 / 8 # minimum beat (in this case every eighth note) | |
| tendency: {on: 1, every: 1/2} # tends to be on a half note, starting on 1 so every 1 and 3 | |
| presence: 0.3 # 30% chance of appearing on every min (increased on tendency) | |
| tone: {frequency: 60, sustain: 0.2, wave: 'square'} # oscillator freq, sustain, and waveform | |
| } | |
| snare_params = { | |
| name: 'snare' | |
| min: 1 / 8 | |
| tendency: {on: 2, every: 1/4} | |
| presence: 0.2 | |
| tone: {frequency: 300, sustain: 0.08, wave: 'triangle'} | |
| } | |
| hats_params = { | |
| name: 'hats' | |
| min: 1 / 16 | |
| tendency: {on: 2, every: 1/8} | |
| presence: 0.3 | |
| tone: {frequency: 1000, sustain: 0.025, wave: 'triangle'} | |
| } | |
| # setting the beat | |
| set_beat = () -> | |
| composition = composition_params | |
| kick = new Rhythm(composition, kick_params) | |
| snare = new Rhythm(composition, snare_params) | |
| hats = new Rhythm(composition, hats_params) | |
| set_beat() | |
| draw_beat(composition, [kick,snare,hats]) | |
| stop_beat = () -> | |
| window.clearInterval(beat_interval) | |
| # control handlers | |
| playing = false | |
| play_handler = (p) -> | |
| if p == true | |
| loop_beats(composition, [kick, snare, hats]) | |
| else | |
| stop_beat() | |
| playing = p | |
| new_handler = () -> | |
| kick = new Rhythm(composition, kick_params) | |
| snare = new Rhythm(composition, snare_params) | |
| hats = new Rhythm(composition, hats_params) | |
| draw_beat(composition, [kick,snare,hats]) | |
| $('#play').click () -> | |
| $(this).toggleClass 'playing' | |
| $('#new').toggleClass 'inactive' | |
| play_handler(!playing) | |
| $('#new').click () -> | |
| new_handler() | |
| jakealbaughSignature() |
| @import url(http://fonts.googleapis.com/css?family=Raleway:300,500); | |
| $max-w: 900px; | |
| $min-w: 640px; | |
| @mixin clearfix { | |
| &:after { | |
| content: ""; | |
| display: table; | |
| clear: both; | |
| } | |
| } | |
| $c-primary: #246068; | |
| $c-dark: darken($c-primary,10%); | |
| $c-black: darken($c-dark,10%); | |
| $c-med: lighten($c-primary,10%); | |
| $c-gray: #c0c0c0; | |
| $c-light: #f0f0f0; | |
| $c-white: #ffffff; | |
| $beat-h: 30px; | |
| body { | |
| color: $c-dark; | |
| background-color: $c-dark; | |
| font-family: Raleway, "Helvetica", "Arial", sans-serif; | |
| font-weight: 300; | |
| font-size: 13px; | |
| strong { | |
| font-weight: 500; | |
| } | |
| } | |
| h1 { | |
| color: $c-white; | |
| text-align: center; | |
| font-size: 2em; | |
| text-transform: uppercase; | |
| font-weight: 300; | |
| margin-top: 1em; | |
| } | |
| #indicator { | |
| background-color: transparentize($c-dark,0.9); | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| } | |
| #composition { | |
| max-width: $max-w; | |
| min-width: $min-w; | |
| width: 60%; | |
| margin: 24px auto; | |
| position: relative; | |
| border: 1px solid $c-black; | |
| box-sizing: border-box; | |
| .rhythm, .header { | |
| position: relative; | |
| width: 100%; | |
| .pattern { | |
| display: table; | |
| vertical-align: middle; | |
| width: 100%; | |
| background-color: $c-white; | |
| .bar { | |
| display: table-cell; | |
| .beats { | |
| @include clearfix; | |
| width: 100%; | |
| .beat { | |
| display: block; | |
| float: left; | |
| text-align: center; | |
| height: $beat-h; | |
| position: relative; | |
| box-sizing: border-box; | |
| border-left: 1px solid $c-light; | |
| &.bar { | |
| background-color: $c-light; | |
| } | |
| &.bar:first-child, &:first-child { | |
| border-left: none; | |
| } | |
| &::after { | |
| position: absolute; | |
| content: attr(symbol); | |
| top: 50%; | |
| left: 50%; | |
| text-align: center; | |
| transform: translate3d(-50%, -50%, 0) scale(1); | |
| transition: transform 100ms; | |
| } | |
| &.hit::after { | |
| font-size: 2.5em; | |
| color: $c-black; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| .label { | |
| width: 80px; | |
| position: absolute; | |
| left: -80px; | |
| box-sizing: border-box; | |
| padding-right: 8px; | |
| height: $beat-h; | |
| line-height: $beat-h; | |
| top: 0; | |
| padding-left: 12px; | |
| box-sizing: border-box; | |
| color: $c-white; | |
| text-transform: uppercase; | |
| font-size: 0.8em; | |
| text-align: right; | |
| background-color: $c-med; | |
| transform: translateZ(0); | |
| } | |
| } | |
| .rhythm { | |
| .pattern .bar .beats .beat.active { | |
| &::after { | |
| transform: translate3d(-50%, -50%, 0) scale(0.2); | |
| } | |
| &.hit.active::after { | |
| transform: translate3d(-50%, -50%, 0) scale(1.8); | |
| } | |
| } | |
| } | |
| .header { | |
| .label { | |
| background-color: $c-primary; | |
| } | |
| .pattern { | |
| background-color: $c-light; | |
| .bar .beats .beat { | |
| border-left: none; | |
| } | |
| } | |
| } | |
| } | |
| .triggers { | |
| width: 100%; | |
| margin-top: 24px; | |
| text-align: center; | |
| a { | |
| color: $c-white; | |
| font-weight: 100; | |
| cursor: pointer; | |
| padding: 8px 12px; | |
| display: inline-block; | |
| width: 80px; | |
| &.inactive { | |
| opacity: 0.4; | |
| pointer-events: none; | |
| } | |
| &#play { | |
| background-color: $c-med; | |
| .play { display: inline-block;} | |
| .stop { display: none;} | |
| &.playing { | |
| background-color: $c-black; | |
| .play { display: none;} | |
| .stop { display: inline-block;} | |
| } | |
| } | |
| &#new { background-color: $c-primary; } | |
| } | |
| } | |
| article { | |
| background: $c-white; | |
| padding: 1em 0; | |
| margin: 2em 0 0; | |
| line-height: 1.4; | |
| main { | |
| width: 80%; | |
| margin: 0 auto; | |
| max-width: $max-w; | |
| min-width: $min-w; | |
| } | |
| a { | |
| color: $c-primary; | |
| text-decoration: none; | |
| font-weight: 500; | |
| } | |
| h1 { | |
| color: $c-dark; | |
| text-align: left; | |
| margin-top: 1em; | |
| } | |
| h2 { | |
| text-transform: uppercase; | |
| font-size: 1.2em; | |
| margin: 0.5em 0; | |
| } | |
| pre { | |
| background: $c-light; | |
| box-sizing: border-box; | |
| padding: 1em; | |
| margin: 1em 0; | |
| font-family: monospace; | |
| strong { | |
| color: $c-gray; | |
| font-style: italic; | |
| } | |
| } | |
| ul { | |
| list-style-type: disc; | |
| margin-left: 2em; | |
| } | |
| p { | |
| margin-bottom: 0.5em; | |
| } | |
| } |