diff --git a/src/clip/Clip.jsx b/src/clip/Clip.jsx index 59a9765..c303a89 100644 --- a/src/clip/Clip.jsx +++ b/src/clip/Clip.jsx @@ -16,6 +16,7 @@ import Slider from '@material-ui/core/Slider' import { ButtonLoadAudioFile } from './ButtonLoadAudioFile' import { actionsContent, actionsViewSettings } from '../global-state' import { AudioDriverOutMenu } from './AudioDriverOutMenu' +import { VuMeter } from './VuMeter' import context from '../global-state/context' import { isSafari } from '../utils/is-safari' @@ -63,6 +64,7 @@ export function Clip({ url, tracksId, clipId }) { const options = formWaveSurferOptions(waveformRef.current) wavesurfer.current = WaveSurfer.create(options) wavesurfer.current.load(url) + wavesurfer.current.on('finish', () => { const duration = wavesurfer.current.getDuration() nrOfCycles.current++ @@ -236,12 +238,28 @@ export function Clip({ url, tracksId, clipId }) {
+ > + {wavesurfer.current && ( + + )} +
+
= this.clipLevel) { + this.clipping = true + this.lastClip = window.performance.now() + } + sum += x * x + } + + // ... then take the square root of the sum. + const rms = Math.sqrt(sum / bufLength) + + // Now smooth this out with the averaging factor applied + // to the previous sample - take the max here because we + // want "fast attack, slow release." + this.volume = Math.max(rms, this.volume * this.averaging) + document.getElementById('audio-value').innerHTML = this.volume +} + +function getAverageVolume(array) { + var values = 0 + var average + + var length = array.length + + // get all the frequency amplitudes + for (var i = 0; i < length; i++) { + values += array[i] + } + + average = values / length + return average +} diff --git a/src/clip/VuMeter.jsx b/src/clip/VuMeter.jsx new file mode 100644 index 0000000..b30cf64 --- /dev/null +++ b/src/clip/VuMeter.jsx @@ -0,0 +1,184 @@ +import PropTypes from 'prop-types' +import React, { useRef, useEffect, useState } from 'react' +//import { useRaf } from '../utils/useRaf' + +export function VuMeter({ wavesurfer }) { + const canvasRef = useRef(null) + const [volumeCur, setVolumeCur] = useState(0) + + useEffect(() => { + wavesurfer.backend.analyser.fftsize = 8 + + wavesurfer.on('audioprocess', () => { + // get the average, bincount is fftsize / 2 + const array = new Uint8Array( + wavesurfer.backend.analyser.frequencyBinCount + ) + wavesurfer.backend.analyser.getByteFrequencyData(array) + const average = getAverageVolume(array) + + renderVuMeter( + canvasRef.current, + { + boxCount: 15, + boxGapFraction: 0.25, + max: 100 + }, + average + ) + }) + }, []) + + return ( +
+ + No canvas + +
+ ) + function renderVuMeter(elem, config, average) { + // Settings + var max = config.max || 100 + var boxCount = config.boxCount || 10 + var boxCountRed = config.boxCountRed || 2 + var boxCountYellow = config.boxCountYellow || 3 + var boxGapFraction = config.boxGapFraction || 0.2 + var jitter = config.jitter || 0.02 + + // Colours + var redOn = 'rgba(255,47,30,0.9)' + var redOff = 'rgba(64,12,8,0.9)' + var yellowOn = 'rgba(255,215,5,0.9)' + var yellowOff = 'rgba(64,53,0,0.9)' + var greenOn = 'rgba(53,255,30,0.9)' + var greenOff = 'rgba(13,64,8,0.9)' + + // Derived and starting values + var width = elem.width + var height = elem.height + var curVal = 0 + + // Gap between boxes and box height + var boxHeight = height / (boxCount + (boxCount + 1) * boxGapFraction) + var boxGapY = boxHeight * boxGapFraction + + var boxWidth = width - boxGapY * 2 + var boxGapX = (width - boxWidth) / 2 + + // Canvas starting state + var c = elem.getContext('2d') + + // Main draw loop + function draw() { + var targetVal = parseInt(elem.dataset.val, 10) + + // Gradual approach + if (curVal <= targetVal) { + curVal += (targetVal - curVal) / 5 + } else { + curVal -= (curVal - targetVal) / 5 + } + + // Apply jitter + if (jitter > 0 && curVal > 0) { + var amount = Math.random() * jitter * max + if (Math.random() > 0.5) { + amount = -amount + } + curVal += amount + } + if (curVal < 0) { + curVal = 0 + } + + c.save() + c.beginPath() + c.rect(0, 0, width, height) + c.fillStyle = 'rgb(32,32,32)' + c.fill() + c.restore() + drawBoxes(c, curVal) + setVolumeCur(average) + + requestAnimationFrame(draw) + } + + // Draw the boxes + function drawBoxes(c, val) { + c.save() + c.translate(boxGapX, boxGapY) + for (var i = 0; i < boxCount; i++) { + var id = getId(i) + + c.beginPath() + if (isOn(id, val)) { + c.shadowBlur = 10 + c.shadowColor = getBoxColor(id, val) + } + c.rect(0, 0, boxWidth, boxHeight) + c.fillStyle = getBoxColor(id, val) + c.fill() + c.translate(0, boxHeight + boxGapY) + } + c.restore() + } + + // Get the color of a box given it's ID and the current value + function getBoxColor(id, val) { + // on colours + if (id > boxCount - boxCountRed) { + return isOn(id, val) ? redOn : redOff + } + if (id > boxCount - boxCountRed - boxCountYellow) { + return isOn(id, val) ? yellowOn : yellowOff + } + return isOn(id, val) ? greenOn : greenOff + } + + function getId(index) { + // The ids are flipped, so zero is at the top and + // boxCount-1 is at the bottom. The values work + // the other way around, so align them first to + // make things easier to think about. + return Math.abs(index - (boxCount - 1)) + 1 + } + + function isOn(id, val) { + // We need to scale the input value (0-max) + // so that it fits into the number of boxes + var maxOn = Math.ceil((val / max) * boxCount) + return id <= maxOn + } + + // Trigger the animation + draw() + } +} + +VuMeter.propTypes = { + wavesurfer: PropTypes.shape({ + backend: PropTypes.shape({ + analyser: PropTypes.shape({ + fftsize: PropTypes.any, + frequencyBinCount: PropTypes.any, + getByteFrequencyData: PropTypes.func + }) + }), + on: PropTypes.func + }) +} + +function getAverageVolume(array) { + var values = 0 + var average + + var length = array.length + + // get all the frequency amplitudes + for (var i = 0; i < length; i++) { + values += array[i] + } + + average = values / length + return average +}