321 lines
8.2 KiB
JavaScript
Executable File
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
|
|
};
|
|
} |