Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ja-k-e/19fb9733b0597e45f19b to your computer and use it in GitHub Desktop.
Save ja-k-e/19fb9733b0597e45f19b to your computer and use it in GitHub Desktop.
Random Beat Sequencer (AudioContext Oscillators FTW)
<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)

Takes composition data, and data relative to each instrument and generates a random beat sequence.

A Pen by Jake Albaugh on CodePen.

License.

# 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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment