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> |
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; | |
} | |
} |