Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 97 additions & 5 deletions src/clip/Clip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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++
Expand Down Expand Up @@ -236,12 +238,28 @@ export function Clip({ url, tracksId, clipId }) {
</div>
<div
style={{
width: '100%',
display: isWaveformShown ? 'unset' : 'none'
display: 'flex',
width: '100%'
}}
id='waveform'
ref={waveformRef}
/>
>
{wavesurfer.current && (
<VuMeter
style={{
width: '10%',
display: isWaveformShown ? 'unset' : 'none'
}}
wavesurfer={wavesurfer.current}
></VuMeter>
)}
<div
style={{
width: '90%',
display: isWaveformShown ? 'unset' : 'none'
}}
id='waveform'
ref={waveformRef}
/>
</div>

<div style={{ width: '100%' }}>
<Slider
Expand Down Expand Up @@ -325,3 +343,77 @@ Clip.propTypes = {
tracksId: PropTypes.any,
url: PropTypes.string
}

/* eslint-disable */
function createAudioMeter(audioContext, clipLevel, averaging, clipLag) {
const processor = audioContext.createScriptProcessor(512)
processor.onaudioprocess = volumeAudioProcess
processor.clipping = false
processor.lastClip = 0
processor.volume = 0
processor.clipLevel = clipLevel || 0.98
processor.averaging = averaging || 0.95
processor.clipLag = clipLag || 750

// this will have no effect, since we don't copy the input to the output,
// but works around a current Chrome bug.
processor.connect(audioContext.destination)

processor.checkClipping = function () {
if (!this.clipping) {
return false
}
if (this.lastClip + this.clipLag < window.performance.now()) {
this.clipping = false
}
return this.clipping
}

processor.shutdown = function () {
this.disconnect()
this.onaudioprocess = null
}

return processor
}

function volumeAudioProcess(event) {
const buf = event.inputBuffer.getChannelData(0)
const bufLength = buf.length
let sum = 0
let x

// Do a root-mean-square on the samples: sum up the squares...
for (var i = 0; i < bufLength; i++) {
x = buf[i]
if (Math.abs(x) >= 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
}
184 changes: 184 additions & 0 deletions src/clip/VuMeter.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<section style={{ width: 40, height: 80 }}>
<canvas ref={canvasRef} width='40' height='80' data-val={volumeCur}>
No canvas
</canvas>
</section>
)
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
}