Audio
For a long time I wished to create music programmatically, but thought that this is a complicated process. Today I finally want to try to implement a simple piano and some generative music.
After googling I get a table of tones for each piano notes and realized that each octave is doubling that value. Now we have an AudioContext
in all modern browsers.
AudioContext
can play a wave, and it provides a build in wave generator (AudioContext.createOscillator
).
To play note we just need to connect this generator to the destination
output and ask it to generate the wave.
Oscilator can generate sine
, square
, sawtooth
, triangle
, and custom
waves. For now, I would just use the sine
one.
After some debugging I got this piano:
style.background = '#192a30';
var notes = {
C: 16.351,
'C#': 17.324,
D: 18.354,
'D#': 19.445,
E: 20.601,
F: 21.827,
'F#': 23.124,
G: 24.499,
'G#': 25.956,
A: 27.5,
'A#': 29.135,
B: 30.868
};
var audio = {
createContext: function(){
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
},
release: 200,
delay: 100,
volume: 0.5,
attack: 10,
playNote: function(name, octave){
if(octave === void 0)
octave = 3;
if(!this.audioContext)
this.createContext();
if(!this.audioContext)
return;
var audioContext = this.audioContext;
var frequency = notes[name.toUpperCase()] * Math.pow(2, octave || 0);
this.attack = this.attack || 1;
this.release = this.release || 1;
var gain = audioContext.createGain();
var now = audioContext.currentTime;
gain.gain.setValueAtTime(this.volume, now);
gain.connect(audioContext.destination);
gain.gain.setValueAtTime(0, now);
gain.gain.setTargetAtTime(this.volume, now, this.attack / 1000);
gain.gain.setTargetAtTime(0, now + this.attack / 1000, this.release / 1000);
var oscilator = audioContext.createOscillator();
oscilator.frequency.setValueAtTime(frequency, now);
oscilator.type = 'sine';
oscilator.connect(gain);
oscilator.start();
setTimeout(function() {
oscilator.stop();
oscilator.disconnect(gain);
gain.gain.cancelScheduledValues(audioContext.currentTime);
gain.disconnect(audioContext.destination);
}, (this.attack + this.release)*5);
},
stack: [],
timeout: null,
initPlayLoop: function(){
if(!this.timeout){
var note = this.stack.shift();
if(!note)
return;
this.playNote(note.note, note.octave);
this.timeout = setTimeout(()=>{
this.timeout = void 0;
this.initPlayLoop();
}, this.delay+this.attack);
}
},
play: function(note, octave){
this.stack.push({note, octave});
this.initPlayLoop();
}
}
var keyWidth = 24, keyHeight = h-16;
var inRect = function(p, x,y,w,h){
return p.x > x && p.y > y && p.x <= x+w && p.y <= y+h;
}
var collision = false, collisionID = 0, collisionType = -1;
move = function(){
rect(0,0,w,h, 0x113355);
rect(8,8,w-16,h-16, 0xFFFFFF);
collision = false, collisionID = 0, collisionType = -1;
[0,1, 3,4,5].forEach(function(i){
if(inRect(mouse, 8+keyWidth*i+keyWidth/2+4,8, keyWidth/2+4, h/2)){
collision = true; collisionID = i; collisionType = 2;
}
});
if(!collision) {
for( var i = 0; i < 8; i++ ) {
if(inRect(mouse, 8+keyWidth*i,8, keyWidth, h-16)){
collision = true; collisionID = i; collisionType = 1;
}
}
}
for(var i = 0; i < 8; i++){
if(collision && collisionType === 1 && collisionID === i) {
rect( 8 + keyWidth * i, 8, keyWidth, h - 16, 0x6699CC );
strokeRect( 8 + keyWidth * i, 8, keyWidth, h - 16, 0x000000 );
}else
strokeRect(8+keyWidth*i,8, keyWidth, h-16, 0x000000);
print('CDEFGAB'[i%7], 8+keyWidth*i+8, h-24)
}
[0,1, 3,4,5].forEach(function(i){
rect(8+keyWidth*i+keyWidth/2+4,8, keyWidth/2+4, h/2, 0x000000);
if(collision && collisionType === 2 && collisionID === i) {
rect(8+keyWidth*i+keyWidth/2+4+1,8+1, keyWidth/2+4-2, h/2-2, 0x006699);
}
print('CDEFGAB'[i%7], 8+keyWidth*i+8, h-24)
});
}
mouse.down = function(){
if(collision){
if(collisionType === 1){
audio.play('CDEFGAB'[collisionID%7], 4+collisionID/7|0)
}
if(collisionType === 2){
audio.play('CDFGAB'[collisionID%7]+'#', 4)
}
}
}
Music generation
I saw a lot of generative music on the YouTube. Usually people used some cyclic motion and collisions, so the first idea was to put some circles flying around the center and colliding with notes and it worked. Not the best music, but a fair start on the way of exploration!
Turn sound on by clicking on the audio icon in the corner:
var soundON = false;
var MAX_ORBIT = 6;
var circles = [];//0,0,0,0,0,0,0];
var Orbital = function({orbit, val}){
this.val = val; this.orbit = orbit;
this.i = 0;
this.count = orbit;
this.init();
};
Orbital.prototype = {
init: function(){
this.radius = (this.orbit+1)*8;
},
changeOrbit: function(){
this.i++;
if(this.i>this.count){
this.i = 0;
if(this.orbit === 0){
this.orbit++;
}else if( this.orbit === MAX_ORBIT){
this.orbit--;
}else{
this.orbit += random()>0.5?1:-1;
}
this.init();
}
}
}
for(var i = 0; i <= MAX_ORBIT; i++){
circles.push(new Orbital({orbit: i, val: 0}))
circles.push(new Orbital({orbit: i, val: PI}))
}
var playBar = function(octave, angle){
if(soundON) {
this.lastPlay = +new Date();
play( this.note, octave );
}
}
var bars = [
{angle: 0, note: 'F', play: playBar},
{angle: PI/2, note: 'C', play: playBar},
{angle: PI, note: 'D', play: playBar},
{angle: PI/2*3, note: 'E', play: playBar}
];
bars.forEach(bar => {
bar.line = new Line( new Point, new Point );
});
var play = function(note, octave){
audio.play( note, octave );
};
var T = PI*2;
var t0 = 0;
var tmpLine = new Line(new Point(0,0), new Point(0,0));
update = function(dt, t){
rect(0,0,w,h, 0x113355);
for(var i = 0; i < circles.length; i++){
var orbital = circles[i],
val = orbital.val,
radius = orbital.radius;
tmpLine.from.x = cos(val)*radius;
tmpLine.from.y = sin(val)*radius;
orbital.val += 10/(PI*2*radius )+ sin(i+5+(t||0))/100;
tmpLine.to.x = cos(orbital.val)*radius;
tmpLine.to.y = sin(orbital.val)*radius;
var octave = 4;
if( i > 4 )
octave++;
bars.forEach(function(bar){
var intersection;
if(intersection = bar.line.intersect(tmpLine)) {
var p = intersection.point.addClone(center);
circle(p.x, p.y, 5, 0xAAFF00)
bar.play(octave, orbital.val);
random()>0.5 && orbital.changeOrbit();
}
});
var color = 0xFFFFFF-i*0x102035;
circle(center.x, center.y, radius, color);
var p = center.addClone(cos(orbital.val)*radius, sin(orbital.val)*radius);
fillCircle(p.x, p.y, 2, color);
}
var now = +new Date();
bars.forEach(function(bar){
var color = 0x77AAFF;
if(bar.lastPlay){
if(now-bar.lastPlay < 300){
color = 0xFFAAFF;
}
}
bar.angle+=0.01+sin(t0)/40;
bar.line.to.x = cos(bar.angle)*w/2;
bar.line.to.y = sin(bar.angle)*w/2;
line(bar.line.from.addClone(center), bar.line.to.addClone(center), color);
});
t0+=dt;
bitmap(`
##
# #
# #
#### #
# #
# #
#### #
# #
# #
##
`, 4, 3, 0xFFFFFF
);
if(!soundON)
line(13,4,3,12,0xFF0000)
};
mouse.down = function(){
if(mouse.x <15 && mouse.y < 15)
soundON = !soundON;
}
I would try to make other sound generators, I predict a lot of potential in combining springs with Voronoi diagrams, Navier–Stokes fluid simulation, and gravity laws. Definitely would try them all!