hydra-synth
Version:
base synth for hydra-editor
218 lines (193 loc) • 6.42 kB
JavaScript
import Meyda from 'meyda'
class Audio {
constructor ({
numBins = 4,
cutoff = 2,
smooth = 0.4,
max = 15,
scale = 10,
isDrawing = false,
parentEl = document.body
}) {
this.vol = 0
this.scale = scale
this.max = max
this.cutoff = cutoff
this.smooth = smooth
this.setBins(numBins)
// beat detection from: https://github.com/therewasaguy/p5-music-viz/blob/gh-pages/demos/01d_beat_detect_amplitude/sketch.js
this.beat = {
holdFrames: 20,
threshold: 40,
_cutoff: 0, // adaptive based on sound state
decay: 0.98,
_framesSinceBeat: 0 // keeps track of frames
}
this.onBeat = () => {
// console.log("beat")
}
this.canvas = document.createElement('canvas')
this.canvas.width = 100
this.canvas.height = 80
this.canvas.style.width = "100px"
this.canvas.style.height = "80px"
this.canvas.style.position = 'absolute'
this.canvas.style.right = '0px'
this.canvas.style.bottom = '0px'
parentEl.appendChild(this.canvas)
this.isDrawing = isDrawing
this.ctx = this.canvas.getContext('2d')
this.ctx.fillStyle="#DFFFFF"
this.ctx.strokeStyle="#0ff"
this.ctx.lineWidth=0.5
if(window.navigator.mediaDevices) {
window.navigator.mediaDevices.getUserMedia({video: false, audio: true})
.then((stream) => {
// console.log('got mic stream', stream)
this.stream = stream
this.context = new AudioContext()
// this.context = new AudioContext()
let audio_stream = this.context.createMediaStreamSource(stream)
// console.log(this.context)
this.meyda = Meyda.createMeydaAnalyzer({
audioContext: this.context,
source: audio_stream,
featureExtractors: [
'loudness',
// 'perceptualSpread',
// 'perceptualSharpness',
// 'spectralCentroid'
]
})
})
.catch((err) => console.log('ERROR', err))
}
}
detectBeat (level) {
//console.log(level, this.beat._cutoff)
if (level > this.beat._cutoff && level > this.beat.threshold) {
this.onBeat()
this.beat._cutoff = level *1.2
this.beat._framesSinceBeat = 0
} else {
if (this.beat._framesSinceBeat <= this.beat.holdFrames){
this.beat._framesSinceBeat ++;
} else {
this.beat._cutoff *= this.beat.decay
this.beat._cutoff = Math.max( this.beat._cutoff, this.beat.threshold);
}
}
}
tick() {
if(this.meyda){
var features = this.meyda.get()
if(features && features !== null){
this.vol = features.loudness.total
this.detectBeat(this.vol)
// reduce loudness array to number of bins
const reducer = (accumulator, currentValue) => accumulator + currentValue;
let spacing = Math.floor(features.loudness.specific.length/this.bins.length)
this.prevBins = this.bins.slice(0)
this.bins = this.bins.map((bin, index) => {
return features.loudness.specific.slice(index * spacing, (index + 1)*spacing).reduce(reducer)
}).map((bin, index) => {
// map to specified range
// return (bin * (1.0 - this.smooth) + this.prevBins[index] * this.smooth)
return (bin * (1.0 - this.settings[index].smooth) + this.prevBins[index] * this.settings[index].smooth)
})
// var y = this.canvas.height - scale*this.settings[index].cutoff
// this.ctx.beginPath()
// this.ctx.moveTo(index*spacing, y)
// this.ctx.lineTo((index+1)*spacing, y)
// this.ctx.stroke()
//
// var yMax = this.canvas.height - scale*(this.settings[index].scale + this.settings[index].cutoff)
this.fft = this.bins.map((bin, index) => (
// Math.max(0, (bin - this.cutoff) / (this.max - this.cutoff))
Math.max(0, (bin - this.settings[index].cutoff)/this.settings[index].scale)
))
if(this.isDrawing) this.draw()
}
}
}
setCutoff (cutoff) {
this.cutoff = cutoff
this.settings = this.settings.map((el) => {
el.cutoff = cutoff
return el
})
}
setSmooth (smooth) {
this.smooth = smooth
this.settings = this.settings.map((el) => {
el.smooth = smooth
return el
})
}
setBins (numBins) {
this.bins = Array(numBins).fill(0)
this.prevBins = Array(numBins).fill(0)
this.fft = Array(numBins).fill(0)
this.settings = Array(numBins).fill(0).map(() => ({
cutoff: this.cutoff,
scale: this.scale,
smooth: this.smooth
}))
// to do: what to do in non-global mode?
this.bins.forEach((bin, index) => {
window['a' + index] = (scale = 1, offset = 0) => () => (a.fft[index] * scale + offset)
})
// console.log(this.settings)
}
setScale(scale){
this.scale = scale
this.settings = this.settings.map((el) => {
el.scale = scale
return el
})
}
setMax(max) {
this.max = max
console.log('set max is deprecated')
}
hide() {
this.isDrawing = false
this.canvas.style.display = 'none'
}
show() {
this.isDrawing = true
this.canvas.style.display = 'block'
}
draw () {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
var spacing = this.canvas.width / this.bins.length
var scale = this.canvas.height / (this.max * 2)
// console.log(this.bins)
this.bins.forEach((bin, index) => {
var height = bin * scale
this.ctx.fillRect(index * spacing, this.canvas.height - height, spacing, height)
// console.log(this.settings[index])
var y = this.canvas.height - scale*this.settings[index].cutoff
this.ctx.beginPath()
this.ctx.moveTo(index*spacing, y)
this.ctx.lineTo((index+1)*spacing, y)
this.ctx.stroke()
var yMax = this.canvas.height - scale*(this.settings[index].scale + this.settings[index].cutoff)
this.ctx.beginPath()
this.ctx.moveTo(index*spacing, yMax)
this.ctx.lineTo((index+1)*spacing, yMax)
this.ctx.stroke()
})
/*var y = this.canvas.height - scale*this.cutoff
this.ctx.beginPath()
this.ctx.moveTo(0, y)
this.ctx.lineTo(this.canvas.width, y)
this.ctx.stroke()
var yMax = this.canvas.height - scale*this.max
this.ctx.beginPath()
this.ctx.moveTo(0, yMax)
this.ctx.lineTo(this.canvas.width, yMax)
this.ctx.stroke()*/
}
}
export default Audio