Skip to content

Commit

Permalink
fix: (Record) use setInterval and 100 FPS
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Aug 18, 2024
1 parent a984cde commit d9bceef
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 15 deletions.
201 changes: 201 additions & 0 deletions examples/record-sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Record plugin

import WaveSurfer from 'wavesurfer.js'
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.esm.js'

let wavesurfer, record
let scrollingWaveform = false
let continuousWaveform = true

const wavesurfer2 = WaveSurfer.create({
container: document.body,
waveColor: 'rgb(200, 0, 200)',
progressColor: 'rgb(100, 0, 100)',
url: '/examples/audio/audio.wav',

// Set a bar width
barWidth: 2,
// Optionally, specify the spacing between bars
barGap: 1,
// And the bar radius
barRadius: 2,
})

wavesurfer2.on('ready', function() {
const createWaveSurfer = () => {
// Destroy the previous wavesurfer instance
if (wavesurfer) {
wavesurfer.destroy()
}

// Create a new Wavesurfer instance
wavesurfer = WaveSurfer.create({
container: '#mic',
waveColor: 'rgb(200, 0, 200)',
progressColor: 'rgb(100, 0, 100)',
})

// Initialize the Record plugin
record = wavesurfer.registerPlugin(
RecordPlugin.create({
renderRecordedAudio: false,
scrollingWaveform,
continuousWaveform,
continuousWaveformDuration: wavesurfer2.getDuration(),
}),
)

// Render recorded audio
record.on('record-end', (blob) => {
const container = document.querySelector('#recordings')
const recordedUrl = URL.createObjectURL(blob)

// Create wavesurfer from the recorded audio
const wavesurfer = WaveSurfer.create({
container,
waveColor: 'rgb(200, 100, 0)',
progressColor: 'rgb(100, 50, 0)',
url: recordedUrl,
})

// Play button
const button = container.appendChild(document.createElement('button'))
button.textContent = 'Play'
button.onclick = () => wavesurfer.playPause()
wavesurfer.on('pause', () => (button.textContent = 'Play'))
wavesurfer.on('play', () => (button.textContent = 'Pause'))

// Download link
const link = container.appendChild(document.createElement('a'))
Object.assign(link, {
href: recordedUrl,
download: 'recording.' + blob.type.split(';')[0].split('/')[1] || 'webm',
textContent: 'Download recording',
})
})
pauseButton.style.display = 'none'
recButton.textContent = 'Record'

record.on('record-progress', (time) => {
updateProgress(time)
})
}

const progress = document.querySelector('#progress')
const updateProgress = (time) => {
// time will be in milliseconds, convert it to mm:ss format
const formattedTime = [
Math.floor((time % 3600000) / 60000), // minutes
Math.floor((time % 60000) / 1000), // seconds
]
.map((v) => (v < 10 ? '0' + v : v))
.join(':')
progress.textContent = formattedTime
}

const pauseButton = document.querySelector('#pause')
pauseButton.onclick = () => {
if (record.isPaused()) {
wavesurfer2.play()
record.resumeRecording()
pauseButton.textContent = 'Pause'
return
}

wavesurfer2.pause()
record.pauseRecording()
pauseButton.textContent = 'Resume'
}

const micSelect = document.querySelector('#mic-select')
{
// Mic selection
RecordPlugin.getAvailableAudioDevices().then((devices) => {
devices.forEach((device) => {
const option = document.createElement('option')
option.value = device.deviceId
option.text = device.label || device.deviceId
micSelect.appendChild(option)
})
})
}
// Record button
const recButton = document.querySelector('#record')

recButton.onclick = () => {
wavesurfer2.play()
if (record.isRecording() || record.isPaused()) {
wavesurfer2.pause()
record.stopRecording()
recButton.textContent = 'Record'
pauseButton.style.display = 'none'
return
}

recButton.disabled = true

// reset the wavesurfer instance

// get selected device
const deviceId = micSelect.value
record.startRecording({ deviceId }).then(() => {
recButton.textContent = 'Stop'
recButton.disabled = false
pauseButton.style.display = 'inline'
})
}

document.querySelector('#scrollingWaveform').onclick = (e) => {
scrollingWaveform = e.target.checked
if (continuousWaveform && scrollingWaveform) {
continuousWaveform = false
document.querySelector('#continuousWaveform').checked = false
}
createWaveSurfer()
}

document.querySelector('#continuousWaveform').onclick = (e) => {
continuousWaveform = e.target.checked
if (continuousWaveform && scrollingWaveform) {
scrollingWaveform = false
document.querySelector('#scrollingWaveform').checked = false
}
createWaveSurfer()
}

createWaveSurfer()
})

/*
<html>
<h1 style="margin-top: 0">Press Record to start recording 🎙️</h1>
<p>
📖 <a href="https://wavesurfer.xyz/docs/classes/plugins_record.RecordPlugin">Record plugin docs</a>
</p>
<button id="record">Record</button>
<button id="pause" style="display: none;">Pause</button>
<select id="mic-select">
<option value="" hidden>Select mic</option>
</select>
<label><input type="checkbox" id="scrollingWaveform" /> Scrolling waveform</label>
<label><input type="checkbox" id="continuousWaveform" checked="checked" /> Continuous waveform</label>
<p id="progress">00:00</p>
<div id="mic" style="border-radius: 4px; margin-top: 1rem"></div>
<div id="recordings" style="margin: 1rem 0"></div>
<style>
button {
min-width: 5rem;
margin: 1rem 1rem 1rem 0;
}
</style>
</html>
*/
33 changes: 18 additions & 15 deletions src/plugins/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type MicStream = {

const DEFAULT_BITS_PER_SECOND = 128000
const DEFAULT_SCROLLING_WAVEFORM_WINDOW = 5
const FPS = 60
const FPS = 100

const MIME_TYPES = ['audio/webm', 'audio/wav', 'audio/mpeg', 'audio/mp4', 'audio/mp3']
const findSupportedMimeType = () => MIME_TYPES.find((mimeType) => MediaRecorder.isTypeSupported(mimeType))
Expand Down Expand Up @@ -102,10 +102,12 @@ class RecordPlugin extends BasePlugin<RecordPluginEvents, RecordPluginOptions> {
const analyser = audioContext.createAnalyser()
source.connect(analyser)

if (this.options.continuousWaveform) {
analyser.fftSize = 32
}
const bufferLength = analyser.frequencyBinCount
const dataArray = new Float32Array(bufferLength)

let animationId: number
let sampleIdx = 0

if (this.wavesurfer) {
Expand All @@ -120,14 +122,12 @@ class RecordPlugin extends BasePlugin<RecordPluginEvents, RecordPluginOptions> {
}

const drawWaveform = () => {
if (this.isWaveformPaused) {
animationId = requestAnimationFrame(drawWaveform)
return
}
if (this.isWaveformPaused) return

analyser.getFloatTimeDomainData(dataArray)

if (this.options.scrollingWaveform) {
// Scrolling waveform
const windowSize = Math.floor((this.options.scrollingWaveformWindow || 0) * audioContext.sampleRate)
const newLength = Math.min(windowSize, this.dataWindow ? this.dataWindow.length + bufferLength : bufferLength)
const tempArray = new Float32Array(windowSize) // Always make it the size of the window, filling with zeros by default
Expand All @@ -140,23 +140,29 @@ class RecordPlugin extends BasePlugin<RecordPluginEvents, RecordPluginOptions> {
tempArray.set(dataArray, windowSize - bufferLength)
this.dataWindow = tempArray
} else if (this.options.continuousWaveform) {
// Continuous waveform
if (!this.dataWindow) {
const size = this.options.continuousWaveformDuration
? Math.round(this.options.continuousWaveformDuration * FPS)
: (this.wavesurfer?.getWidth() ?? 0) * window.devicePixelRatio
this.dataWindow = new Float32Array(size)
}

const maxValue = Math.max(...dataArray)
let maxValue = 0
for (let i = 0; i < bufferLength; i++) {
const value = Math.abs(dataArray[i])
if (value > maxValue) {
maxValue = value
}
}

// Append the max value to the data window at the right position
if (sampleIdx + 1 > this.dataWindow.length) {
const tempArray = new Float32Array(this.dataWindow.length * 2)
tempArray.set(this.dataWindow, 0)
this.dataWindow = tempArray
}

this.dataWindow.set([maxValue], sampleIdx)
this.dataWindow[sampleIdx] = maxValue
sampleIdx++
} else {
this.dataWindow = dataArray
Expand All @@ -169,7 +175,6 @@ class RecordPlugin extends BasePlugin<RecordPluginEvents, RecordPluginOptions> {
if (this.wavesurfer.options.barWidth) {
position += this.wavesurfer.options.barWidth / this.wavesurfer.getWidth()
}

this.wavesurfer
.load(
'',
Expand All @@ -191,21 +196,19 @@ class RecordPlugin extends BasePlugin<RecordPluginEvents, RecordPluginOptions> {
console.error('Error rendering real-time recording data:', err)
})
}

animationId = requestAnimationFrame(drawWaveform)
}

drawWaveform()
const intervalId = setInterval(drawWaveform, 1000 / FPS)

return {
onDestroy: () => {
cancelAnimationFrame(animationId)
clearInterval(intervalId)
source?.disconnect()
audioContext?.close()
},
onEnd: () => {
this.isWaveformPaused = true
cancelAnimationFrame(animationId)
clearInterval(intervalId)
this.stopMic()
},
}
Expand Down

0 comments on commit d9bceef

Please sign in to comment.