-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
019f28e
commit 4df0e0f
Showing
6 changed files
with
241 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// reference: https://github.com/stasilo/BeatDetector/blob/master/beatdetector.js | ||
|
||
export type BassDetector = ReturnType<typeof createBassDetector>; | ||
|
||
export const createBassDetector = (analyser: AnalyserNode) => { | ||
const MAX_COLLECT_SIZE = 43 * (analyser.fftSize / 2); | ||
const COLLECT_SIZE = 1; | ||
const sens = 0.05; | ||
const bufferLength = analyser.frequencyBinCount; | ||
|
||
let historyBuffer = []; | ||
let instantEnergy = 0; | ||
let prevTime = 0; | ||
let bpmTable = []; | ||
|
||
const isOnBeat = () => { | ||
let localAverageEnergy = 0; | ||
let instantCounter = 0; | ||
let isBeat = false; | ||
|
||
let bpmArray = new Uint8Array(bufferLength); | ||
analyser.getByteFrequencyData(bpmArray); | ||
|
||
// fill history buffer | ||
for (var i = 0; i < bpmArray.length - 1; i++, ++instantCounter) { | ||
historyBuffer.push(bpmArray[i]); //add sample to historyBuffer | ||
instantEnergy += bpmArray[i]; | ||
} | ||
|
||
//done collecting MAX_COLLECT_SIZE history samples | ||
//have COLLECT_SIZE nr of samples as instant energy value | ||
if (instantCounter > COLLECT_SIZE - 1 && | ||
historyBuffer.length > MAX_COLLECT_SIZE - 1) { | ||
instantEnergy = instantEnergy / (COLLECT_SIZE * (analyser.fftSize / 2)); | ||
|
||
var average = 0; | ||
for (var i = 0; i < historyBuffer.length - 1; i++) { | ||
average += historyBuffer[i]; | ||
} | ||
|
||
localAverageEnergy = average / historyBuffer.length; | ||
|
||
var timeDiff = analyser.context.currentTime - prevTime; | ||
|
||
// timeDiff > 2 is out of normal song bpm range, but if it is a multiple of range [0.3, 1.5] | ||
// we probably have missed a beat before but now have a match in the bpm table. | ||
if (timeDiff > 2 && bpmTable.length > 0) { | ||
//check if we have a multiple of range in bpm table | ||
for (var j = 0; j < bpmTable.length - 1; j++) { | ||
// mutiply by 10 to avoid float rounding errors | ||
var timeDiffInteger = Math.round((timeDiff / bpmTable[j]['time']) * 1000); | ||
|
||
// timeDiffInteger should now be a multiple of a number in range [3, 15] | ||
// if we have a match | ||
|
||
if (timeDiffInteger % (Math.round(bpmTable[j]['time']) * 1000) == 0) { | ||
timeDiff = Number(bpmTable[j]['time']); | ||
} | ||
} | ||
} | ||
|
||
|
||
//still? | ||
if (timeDiff > 3) { | ||
prevTime = timeDiff = 0; | ||
} | ||
|
||
// MAIN BPM HIT CHECK // | ||
|
||
// CHECK IF WE HAVE A BEAT BETWEEN 200 AND 40 BPM (every 0.29 to 2s), or else ignore it. | ||
// Also check if we have _any_ found prev beats | ||
if (analyser.context.currentTime > 0.29 && instantEnergy > localAverageEnergy && | ||
(instantEnergy > (sens * localAverageEnergy)) && | ||
((timeDiff < 2.0 && timeDiff > 0.29) || prevTime == 0)) { | ||
|
||
isBeat = true; | ||
|
||
prevTime = analyser.context.currentTime; | ||
|
||
let bpm: any = { | ||
time: Number(timeDiff.toFixed(3)), | ||
counter: 1, | ||
}; | ||
|
||
|
||
for (var j = 0; j < bpmTable.length; j++) { | ||
//FOUND ANOTHER MATCH FOR ALREADY GUESSED BEAT | ||
|
||
if (bpmTable[j]['time'] == bpm['time']) { | ||
bpmTable[j]['counter']++; | ||
bpm = 0; | ||
|
||
if (bpmTable[j]['counter'] > 3 && j < 2) { | ||
console.log("WE HAVE A BEAT MATCH IN TABLE!!!!!!!!!!"); | ||
} | ||
|
||
break; | ||
} | ||
} | ||
|
||
if (bpm || bpmTable.length == 0) { | ||
bpmTable.push(bpm); | ||
} | ||
|
||
//sort and draw 10 most current bpm-guesses | ||
bpmTable.sort((a, b) => | ||
b.counter - a.counter | ||
); | ||
} | ||
|
||
var temp = historyBuffer.slice(0); //get copy of buffer | ||
|
||
historyBuffer = []; //clear buffer | ||
|
||
// make room in array by deleting the last COLLECT_SIZE samples. | ||
historyBuffer = temp.slice(COLLECT_SIZE * (analyser.fftSize / 2), temp.length); | ||
|
||
instantCounter = 0; | ||
instantEnergy = 0; | ||
|
||
localAverageEnergy = 0; | ||
|
||
return isBeat; | ||
} | ||
}; | ||
|
||
const getBPMGuess = () => { | ||
let allGuesses = 0; | ||
let guesses = 0; | ||
let counter = 0; | ||
|
||
if (bpmTable.length <= 2) { | ||
return null; | ||
} | ||
|
||
for (var i = 0; i < bpmTable.length; i++) { | ||
allGuesses += Number(bpmTable[i].time); | ||
|
||
if (bpmTable[i]['counter'] > 1) { | ||
guesses += Number(bpmTable[i].time); | ||
|
||
counter++; | ||
} | ||
} | ||
|
||
return { | ||
conservative: Math.round(60 / (guesses / counter)), | ||
all: Math.round(60 / (allGuesses / bpmTable.length)) | ||
}; | ||
} | ||
|
||
return { | ||
isOnBeat, | ||
getBPMGuess, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
export interface Coord { | ||
x: number; | ||
y: number; | ||
} | ||
|
||
export type Particle = Coord & { | ||
d: number[]; | ||
}; | ||
|
||
export interface ParticleHandler { | ||
generate: (ctx: CanvasRenderingContext2D) => Particle; | ||
move: (ctx: CanvasRenderingContext2D, particle: Particle, dt: number) => Particle | null; | ||
draw: (ctx: CanvasRenderingContext2D, particle: Particle) => void; | ||
}; | ||
|
||
const identity = <T>(x: T) => x; | ||
|
||
export const store: Record<string, ParticleHandler> = { | ||
bass: { | ||
generate: (ctx) => ({ | ||
d: [], | ||
x: 0, | ||
y: ctx.canvas.height, | ||
}), | ||
|
||
draw(ctx, { y }) { | ||
//ctx.filter = "invert(1)"; | ||
ctx.beginPath(); | ||
ctx.strokeStyle = "white"; | ||
ctx.globalAlpha = (y / ctx.canvas.height) * 0.5; | ||
ctx.moveTo(0, y); | ||
ctx.lineTo(ctx.canvas.width, y); | ||
ctx.stroke(); | ||
ctx.globalAlpha = 1; | ||
//ctx.filter = ""; | ||
}, | ||
|
||
move: (ctx, { | ||
d, | ||
x, | ||
y, | ||
}, dt) => (y < 0) ? null : ({ | ||
d, | ||
x, | ||
y: y - (1 * dt), | ||
}), | ||
}, | ||
} as const; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters