chiro-canto/public/larynx/scripts/piano.js

321 lines
8.2 KiB
JavaScript
Executable File

/******************
Microphone to Piano original Code by Vamoss
Original code link:
https://www.openprocessing.org/sketch/760560
Author links:
http://vamoss.com.br
http://twitter.com/vamoss
http://github.com/vamoss
******************/
const micSampleRate = 44100;
const freqDefinition = 8192;
const freqSteps = micSampleRate/freqDefinition;
const hitKeyThreshold = 80;//minimum level to hit the piano key
const minFreqHz = 130;//C3
const maxFreqHz = 2903;//C7
const minFreq = Math.floor(minFreqHz/2/1.445);//2/1.445 is a magic number to convert Hz to MIC frequency, dont know why...
const maxFreq = Math.floor(maxFreqHz/2/1.445);//2/1.445 is a magic number to convert Hz to MIC frequency, dont know why...
const scaleIndexToNote = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
let mic, fft, spectrum;
let historygram;
let piano = [];
let pressedKey = -1;
let keyHeight;
let state, nextState, startTime, duration;
const STATES = {CALIBRATING: "CALIBRATING", RECORDING: "RECORDING", PLAYING: "PLAYING"};
const calibrationDuration = 5 * 1000;//milliseconds
let calibrateSum = [];
let avarageSpectrum;//array to threshold the noise from the microphone
const recordDuration = 10 * 1000;//milliseconds
let recordedSequence = [];
function setup() {
createCanvas(windowWidth, windowHeight);
//historygram = createGraphics(width/4,maxFreq-minFreq);
historygram = createGraphics(width/2,height);
mic = new p5.AudioIn();
mic.start();
fft = new p5.FFT(0.0, freqDefinition);
mic.connect(fft);
//setup the Piano array
let totalKeys = freqToMidi(maxFreqHz) - freqToMidi(minFreqHz);
let keyX = width / 2;
let keyWidth = width / 2 / (totalKeys/12*7);
keyHeight = width / 1920 * 100;
for(let midi = freqToMidi(minFreqHz); midi < freqToMidi(maxFreqHz); midi++){
let index = midi % 12;
//black key indices
//c c# d d# e f f# g g# a a# b
if(index == 1 || index == 3 || index == 6 || index == 8 || index == 10){
createKey(midi+1, "WHITE", keyX, keyWidth);
createKey(midi, "BLACK", keyX, keyWidth);
midi++;
}else{
createKey(midi, "WHITE", keyX, keyWidth);
}
keyX += keyWidth;
}
avarageSpectrum = new Array(maxFreq-minFreq);
avarageSpectrum.fill(0);
textAlign(CENTER, CENTER);
textSize(24);
setState(STATES.CALIBRATING);
}
function draw() {
background(255);
spectrum = fft.analyze();
//CALCULATE AVARAGE NOISE FROM MICROPHONE
if(state == STATES.CALIBRATING){
calibrateSum.push(spectrum.slice());
for (let i = minFreq; i < maxFreq; i++) {
let index = i - minFreq;
let sum = 0;
for (let j = 0; j < calibrateSum.length; j++) {
sum += calibrateSum[j][i] * 1.1;
}
avarageSpectrum[index] = sum / calibrateSum.length;
}
}
//DRAW MIC SPECTRUM
stroke(0);
fill(255,255,0, 150);
beginShape();
vertex(width*3/4,height);
for (let i = minFreq; i < maxFreq; i++) {
let x = (1 - spectrum[i] / 255) * width*1/4;
let y = (i - minFreq) / (maxFreq - minFreq - 1) * height;
vertex(width-x, height-y);
}
vertex(width*3/4,0);
endShape(CLOSE);
//DRAW AVARAGE SPECTRUM
stroke(0);
fill(0, 150);
beginShape();
vertex(width*3/4,height);
for (let i = minFreq; i < maxFreq; i++) {
let index = i - minFreq;
let x = (1 - avarageSpectrum[index] / 255) * width/4;
let y = index / (maxFreq - minFreq - 1) * height;
//let y = index / (height - 1) * width / 2;
vertex(width-x,height- y);
}
vertex(width*3/4,0);
endShape(CLOSE);
//DRAW HISTORYGRAM
historygram.image(historygram, -1,0);
for (let i = maxFreq; i >= minFreq; i--) {
let index = i - minFreq;
let intensity = (spectrum[i] - avarageSpectrum[index]) * 3;
historygram.stroke(255-intensity,255-intensity/2,intensity);
historygram.point(historygram.width-1,height- index);
}
fill(255);
push();
scale(1,1)
image(historygram, 0,0, width* 3/4, height);
pop();
//FIND THE HIGHEST LEVEL OF AUDIO FREQUENCY
let selectedFreq = -1;
let selectedDist = 0;
for (let i = minFreq; i < maxFreq; i++) {
let index = i - minFreq;
if(spectrum[i] - avarageSpectrum[index] > selectedDist){
selectedDist = spectrum[i] - avarageSpectrum[index];
selectedFreq = i;
}
}
//DRAW HIGHEST LEVEL
const magicNumber = 1.091;//I had to multiply to this magic number, to be able to get the right Hz, I dont know why...
let freq = selectedFreq * freqSteps/2 * magicNumber;
let tempPressedKey = freqToMidi(freq);
if(selectedDist > hitKeyThreshold){
let xSelected = (selectedFreq-minFreq) / (maxFreq-minFreq) *height;
stroke(255, 0, 255);
line(width*3/4,height- xSelected,width,height-xSelected);
noStroke();
fill(255, 0,255);
text(freq.toFixed(2) + "Hz",width*7/8, height- xSelected-20);
let note = scaleIndexToNote[tempPressedKey % 12] + (floor(tempPressedKey/12)-1);
text(note, width*7/8, height- xSelected+30);
}
if(state == STATES.RECORDING){
//RECORD
if(selectedDist > hitKeyThreshold && piano[tempPressedKey]){
if(pressedKey != tempPressedKey) {
//PRESS KEY
pressedKey = tempPressedKey;
recordedSequence.push({
key: pressedKey,
start: new Date().getTime() - startTime,
duration: 100
});
}else{
//still pressing the same key
let lastIndex = recordedSequence.length-1;
recordedSequence[lastIndex].duration = new Date().getTime() - startTime - recordedSequence[lastIndex].start;
}
}else{
//RELEASE KEY
pressedKey = -1;
}
//DRAW RECORDING SEQUENCE
let scrollY = (new Date().getTime() - startTime)/10;
noStroke();
fill(255);
for(let i = recordedSequence.length-1; i >= 0; i--){
let record = recordedSequence[i];
let pianoKey = piano[record.key];
let x = pianoKey.x;
let y = keyHeight + scrollY - record.start/10;
let w = pianoKey.w;
let h = -record.duration/10;
rect(x, y, w, h);
}
}
//DRAW PLAYING SEQUENCE
if(state == STATES.PLAYING){
let time = (new Date().getTime() - startTime);
let scrollY = time/10;
pressedKey = -1;
noStroke();
fill(255);
for(let i = recordedSequence.length-1; i >= 0; i--){
let record = recordedSequence[i];
let pianoKey = piano[record.key];
let x = pianoKey.x;
let y = keyHeight - scrollY + record.start/10;
let w = pianoKey.w;
let h = record.duration/10;
rect(x, y, w, h);
//PLAY SOUND
if(time > record.start && time < record.start + record.duration){
pressedKey = record.key;
if(!record.played){
record.played = true;
pianoKey.osc.start();
pianoKey.envelope.play(pianoKey.osc, 0, record.duration/1000);
}
}else{
record.played = false;
}
}
}
//DRAW PIANO
noStroke();
for(let midi = freqToMidi(minFreqHz); midi < freqToMidi(maxFreqHz); midi++){
if(piano[midi].type == "BLACK"){
//first draw the next white key
if(pressedKey == midi+1){
fill(255, 255, 0);
}else{
fill(255);
}
rect(piano[midi+1].x, 0, piano[midi+1].w, piano[midi+1].h);
//then draw the black key
if(pressedKey == midi){
fill(255, 255, 0);
}else{
fill(0);
}
rect(piano[midi].x, 0, piano[midi].w, piano[midi].h);
midi++;
}else{
//white key
if(pressedKey == midi){
fill(255, 255, 0);
}else{
fill(255);
}
rect(piano[midi].x, 0, piano[midi].w, piano[midi].h);
}
}
//CHECK NEXT STATE
let time = new Date().getTime();
let remaining = floor((duration - (time - startTime))/1000);
fill(255, 0, 255);
text(state + ": " + remaining + "s", width / 4, height / 2 - 30);
if(time - startTime > duration){
setState(nextState);
}
}
function setState(s){
state = s;
startTime = new Date().getTime();
switch (state) {
case STATES.CALIBRATING:
duration = calibrationDuration;
nextState = STATES.RECORDING;
break;
case STATES.RECORDING:
duration = recordDuration;
nextState = STATES.PLAYING;
break;
case STATES.PLAYING:
break;
}
}
function createKey(midi, type, x, w){
let h = keyHeight;
if(type == "BLACK"){
x -= w*0.3;
w /= 1.5;
h /= 2;
}
let envelope = new p5.Envelope();
envelope.setRange(0.07, 0);//attackLevel, releaseLevel
envelope.setADSR(0.001, 0.8, 0.1, 0.5);//attackTime, decayTime, sustainRatio, releaseTime
let osc = new p5.SinOsc();
osc.freq(midiToFreq(midi));
piano[midi] = {
envelope: envelope,
osc: osc,
midi: midi,
freq: midiToFreq(midi),
x: x,
w: w,
h: h,
type: type,
played: false
};
}