Skip to content

Commit

Permalink
feat: merge with piano repo
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasmafra committed Jan 5, 2024
1 parent 1a3a2cd commit cdd32aa
Show file tree
Hide file tree
Showing 16 changed files with 624 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules/
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# scale-explorer
# music-explorer

## scale explorer
Objective: Make an intuitive music scale explorer, since fifth circle until greek modes.

# running locally
## running locally

$ npm install -g http-server # install dependency
$ http-server -p 8000
install:
`$ yarn`

start:
`$ yarn start`
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "music-explorer",
"license": "MIT",
"version": "0.0.0",
"main": "index.js",
"scripts": {
"start": "yarn http-server -p 8000"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nickmafra/music-explorer.git"
},
"dependencies": {
"http-server": "^14.1.1"
}
}
57 changes: 57 additions & 0 deletions piano/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
var core = {

audioCtx: null,

start: function() {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
},

calcFrequency: function(note, octave) {
return 440 * Math.pow(2, octave - 4 + note/12);
},

makeEnvelopeNode: function(adsr, time) {
if (!time) time = this.audioCtx.currentTime;

const env = new GainNode(this.audioCtx);
env.gain.cancelScheduledValues(time);
env.gain.setValueAtTime(0, time);
env.gain.linearRampToValueAtTime(adsr.attackAmplitude, time + adsr.attackDuration);
env.gain.linearRampToValueAtTime(
adsr.decayAmplitude,
time + adsr.attackDuration + adsr.decayDuration
);
if (adsr.sustainDuration) {
env.gain.linearRampToValueAtTime(
0,
time + adsr.attackDuration + adsr.decayDuration + adsr.sustainDuration
);
}
return env;
},

releaseEnvelope: function(env, adsr, time) {
if (!time) time = this.audioCtx.currentTime;

env.gain.linearRampToValueAtTime(
0,
time + adsr.releaseDuration
);
},

makeHarmonicNode: function(frequency, harmonics) {

const wave = new PeriodicWave(this.audioCtx, {
real: new Float32Array(harmonics),
imag: new Float32Array(harmonics.length)
});

const oscillator = new OscillatorNode(this.audioCtx, {
frequency,
type: "custom",
periodicWave: wave
});
return oscillator;
}

};
73 changes: 73 additions & 0 deletions piano/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
var engine = {

updateFunction: () => {},
objects: [],
clickStartObject: null,
clickingObject: null,
clickStopObject: null,

start: function() {
this.canvas = document.createElement("canvas");
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
this.ctx = this.canvas.getContext("2d");

addEventListener("resize", () => this.resize());
this.resize();

document.addEventListener('mousedown', (e) => this.onClickStart(e));
document.addEventListener('mouseup', (e) => this.onClickStop(e));

this.loop();
},

resize: function() {
this.canvas.width = document.body.clientWidth;
this.canvas.height = document.body.clientHeight;
this.unit = Math.min(this.canvas.width, this.canvas.height)/2; // center to nearest border
},

loop: function() {
this.updateFunction();
this.draw();

this.clickStartObject = null;
this.clickStopObject = null;

requestAnimationFrame(() => this.loop());
},

draw: function() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = 'gray';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

this.objects.forEach(obj => {
this.ctx.fillStyle = obj.c;
this.ctx.fillRect(
this.canvas.width/2 + this.unit * (obj.x - obj.w/2),
this.canvas.height/2 + this.unit * (obj.y - obj.h/2),
this.unit * obj.w,
this.unit * obj.h
);
});
},

onClickStart: function(e) {
var x = (e.clientX - this.canvas.width/2) / this.unit;
var y = (e.clientY - this.canvas.height/2) / this.unit;

for (var i = this.objects.length - 1; i >= 0; i--) {
var obj = this.objects[i];
if (obj.clickable && Math.abs(x - obj.x) <= obj.w/2 && Math.abs(y - obj.y) <= obj.h/2) {
this.clickStartObject = obj;
this.clickingObject = obj;
return;
}
}
},

onClickStop: function(e) {
this.clickStopObject = this.clickingObject;
this.clickingObject = null;
}
}
23 changes: 23 additions & 0 deletions piano/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Sounzapp, the real-music game!</title>

<style>
html, body, canvas {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
</style>

</head>
<body onload="main.start()">
<script src="engine.js"></script>
<script src="core.js"></script>
<script src="piano.js"></script>
<script src="main.js"></script>
</body>
</html>
45 changes: 45 additions & 0 deletions piano/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
var main = {

start: function() {
core.start();

this.objects = piano.makeKeyObjects(0, 1, 37);

engine.updateFunction = () => this.update();
engine.start();
},

update: function() {
engine.objects = [...this.objects];
if (engine.clickStartObject != null && engine.clickStartObject.audioPlayer) {
engine.clickStartObject.audioPlayer.start();
}
if (engine.clickStopObject != null && engine.clickStopObject.audioPlayer) {
engine.clickStopObject.audioPlayer.stop();
}
},

randomColorString: function() {
var rgb = Array(3).fill(255).map(x => Math.floor(x * Math.random()));
return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
},

objects: [
{
x: 0,
y: 0,
w: 1,
h: 1,
c: 'gray',
clickable: true
},
{
x: 0.5,
y: 0.5,
w: 0.5,
h: 0.5,
c: 'cyan',
clickable: true
},
]
}
101 changes: 101 additions & 0 deletions piano/piano.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
var piano = {

adsr: {
attackAmplitude: 1,
attackDuration: 0.02,
decayAmplitude: 0.5,
decayDuration: 0.2,
sustainDuration: 2,
releaseDuration: 0.1
},
harmonics: [
1.0,
0.6,
0.2,
0.3,
0.1, // 5
0.5,
0.1,
0.4,
0.1,
0.1, // 10
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
],

makeKeyObjects: function(y, width, keyCount = 88) {
var whiteKeyCountEstimate = keyCount - Math.floor(keyCount*5/12);
var scale = 1 * width * 88 / whiteKeyCountEstimate;
const space = scale * 0.035;
const whiteKeyWidth = scale * 0.033;
const whiteKeyHeight = scale * 0.2;
const blackKeyWidth = whiteKeyWidth*2/3;
const blackKeyHeight = whiteKeyHeight*3/5;
const blackIndexes = [1,4,6,9,11];
var note = keyCount == 88 ? 0 : 3;
var octave = Math.floor((88 - keyCount) / 24);
var keyX = -whiteKeyCountEstimate/2 * space;
var keys = [];
while (keys.length < keyCount) {
var isBlack = blackIndexes.includes(note);
var freq = core.calcFrequency(note, octave);
var key = {
note,
octave,
isBlack,
audioPlayer: this.audioPlayer(freq),
clickable: true
};
if (isBlack) {
key.c = '#222';
key.w = blackKeyWidth;
key.h = blackKeyHeight;
key.x = keyX - whiteKeyWidth/2;
key.y = y - whiteKeyHeight/2 + blackKeyHeight/2;
} else {
key.c = '#eee';
key.w = whiteKeyWidth;
key.h = whiteKeyHeight;
key.x = keyX;
keyX += space;
key.y = y;
}
keys.push(key);

note++;
if (note == 12) {
note = 0;
octave++;
}
}
keys.sort((a,b) => a.isBlack ? 1 : b.isBlack ? -1 : 0);
return keys;
},

audioPlayer: function(freq) {
const audioPlayer = {};
audioPlayer.start = () => {
if (audioPlayer.oscillator) {
audioPlayer.oscillator.stop();
}

audioPlayer.oscillator = core.makeHarmonicNode(freq, this.harmonics);
audioPlayer.envelope = core.makeEnvelopeNode(this.adsr);

audioPlayer.oscillator
.connect(audioPlayer.envelope)
.connect(core.audioCtx.destination);

audioPlayer.oscillator.start();
};
audioPlayer.stop = () => {
core.releaseEnvelope(audioPlayer.envelope, this.adsr);
audioPlayer.oscillator.stop(core.audioCtx.currentTime + this.adsr.releaseDuration);
};
return audioPlayer;
}
};
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit cdd32aa

Please sign in to comment.