From e0623dd4c637926db353ba7d55356dd2cc78f8a0 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:47:51 +0100 Subject: [PATCH] Feat: play(start, stop) --- cypress/e2e/spectrogram.cy.js | 4 +-- examples/record-sync.js | 2 +- examples/regions.js | 2 +- src/plugins/regions.ts | 13 ++++--- src/plugins/spectrogram.ts | 66 ++++++++++++++++------------------- src/renderer.ts | 3 +- src/wavesurfer.ts | 36 ++++++++++++++++--- 7 files changed, 75 insertions(+), 51 deletions(-) diff --git a/cypress/e2e/spectrogram.cy.js b/cypress/e2e/spectrogram.cy.js index 89938c72e..18d711c17 100644 --- a/cypress/e2e/spectrogram.cy.js +++ b/cypress/e2e/spectrogram.cy.js @@ -83,7 +83,7 @@ xdescribe('WaveSurfer Spectrogram plugin tests', () => { }) }) - scales.forEach(scale => { + scales.forEach((scale) => { it(`should display correct frequency labels with 1kHz tone (${scale})`, () => { cy.visit('cypress/e2e/index.html') cy.window().then((win) => { @@ -99,7 +99,7 @@ xdescribe('WaveSurfer Spectrogram plugin tests', () => { scale: scale, frequencyMin: 0, frequencyMax: 4000, - splitChannels: false + splitChannels: false, }), ], }) diff --git a/examples/record-sync.js b/examples/record-sync.js index f814fc362..d672fa9fa 100644 --- a/examples/record-sync.js +++ b/examples/record-sync.js @@ -21,7 +21,7 @@ const wavesurfer2 = WaveSurfer.create({ barRadius: 2, }) -wavesurfer2.on('ready', function() { +wavesurfer2.on('ready', function () { const createWaveSurfer = () => { // Destroy the previous wavesurfer instance if (wavesurfer) { diff --git a/examples/regions.js b/examples/regions.js index 198c10e5e..5beeccbf9 100644 --- a/examples/regions.js +++ b/examples/regions.js @@ -93,7 +93,7 @@ document.querySelector('input[type="checkbox"]').onclick = (e) => { regions.on('region-clicked', (region, e) => { e.stopPropagation() // prevent triggering a click on the waveform activeRegion = region - region.play() + region.play(true) region.setOptions({ color: randomColor() }) }) // Reset the active region when the user clicks anywhere in the waveform diff --git a/src/plugins/regions.ts b/src/plugins/regions.ts index fa99f55b2..55bdba9fa 100644 --- a/src/plugins/regions.ts +++ b/src/plugins/regions.ts @@ -38,7 +38,7 @@ export type RegionEvents = { /** When dragging or resizing is finished */ 'update-end': [] /** On play */ - play: [] + play: [end?: number] /** On mouse click */ click: [event: MouseEvent] /** Double click */ @@ -334,9 +334,9 @@ class SingleRegion extends EventEmitter implements Region { this.renderPosition() } - /** Play the region from the start */ - public play() { - this.emit('play') + /** Play the region from the start, pass `true` to stop at region end */ + public play(stopAtEnd?: boolean) { + this.emit('play', stopAtEnd ? this.end : undefined) } /** Set the HTML content of the region */ @@ -588,9 +588,8 @@ class RegionsPlugin extends BasePlugin { - this.wavesurfer?.play() - this.wavesurfer?.setTime(region.start) + region.on('play', (end?: number) => { + this.wavesurfer?.play(region.start, end) }), region.on('click', (e) => { diff --git a/src/plugins/spectrogram.ts b/src/plugins/spectrogram.ts index 46658fb35..c09ea79ec 100644 --- a/src/plugins/spectrogram.ts +++ b/src/plugins/spectrogram.ts @@ -284,11 +284,7 @@ export type SpectrogramPluginOptions = { * - igray: Inverted gray scale. * - roseus: From https://github.com/dofuuz/roseus/blob/main/roseus/cmap/roseus.py */ - colorMap?: - | number[][] - | 'gray' - | 'igray' - | 'roseus' + colorMap?: number[][] | 'gray' | 'igray' | 'roseus' /** Render a spectrogram for each channel independently when true. */ splitChannels?: boolean /** URL with pre-computed spectrogram JSON data, the data must be a Uint8Array[][] **/ @@ -557,13 +553,7 @@ class SpectrogramPlugin extends BasePlugin { - spectrCc.drawImage( - bitmap, - 0, - height * (c + 1 - rMax1 / rMax), - width, - height * rMax1 / rMax, - ) + spectrCc.drawImage(bitmap, 0, height * (c + 1 - rMax1 / rMax), width, (height * rMax1) / rMax) }) } @@ -593,30 +583,38 @@ class SpectrogramPlugin extends BasePlugin Array(this.fftSamples / 2 + 1).fill(0)) - const scale = (sampleRate / this.fftSamples) + const scale = sampleRate / this.fftSamples for (let i = 0; i < numFilters; i++) { - let hz = scaleToHz(filterMin + (i / numFilters) * (filterMax - filterMin)) - let j = Math.floor(hz / scale) - let hzLow = j * scale - let hzHigh = (j + 1) * scale - let r = (hz - hzLow) / (hzHigh - hzLow) - filterBank[i][j] = 1 - r - filterBank[i][j + 1] = r + let hz = scaleToHz(filterMin + (i / numFilters) * (filterMax - filterMin)) + let j = Math.floor(hz / scale) + let hzLow = j * scale + let hzHigh = (j + 1) * scale + let r = (hz - hzLow) / (hzHigh - hzLow) + filterBank[i][j] = 1 - r + filterBank[i][j + 1] = r } return filterBank } - private hzToMel(hz: number) { return 2595 * Math.log10(1 + hz / 700) } + private hzToMel(hz: number) { + return 2595 * Math.log10(1 + hz / 700) + } - private melToHz(mel: number) { return 700 * (Math.pow(10, mel / 2595) - 1) } + private melToHz(mel: number) { + return 700 * (Math.pow(10, mel / 2595) - 1) + } private createMelFilterBank(numMelFilters: number, sampleRate: number): number[][] { return this.createFilterBank(numMelFilters, sampleRate, this.hzToMel, this.melToHz) } - private hzToLog(hz: number) { return Math.log10(Math.max(1, hz)) } + private hzToLog(hz: number) { + return Math.log10(Math.max(1, hz)) + } - private logToHz(log: number) { return Math.pow(10, log) } + private logToHz(log: number) { + return Math.pow(10, log) + } private createLogFilterBank(numLogFilters: number, sampleRate: number): number[][] { return this.createFilterBank(numLogFilters, sampleRate, this.hzToLog, this.logToHz) @@ -708,7 +706,8 @@ class SpectrogramPlugin extends BasePlugin -this.gainDB) { array[j] = 255 } else { - array[j] = (valueDB + this.gainDB) / this.rangeDB * 255 + 256 + array[j] = ((valueDB + this.gainDB) / this.rangeDB) * 255 + 256 } } channelFreq.push(array) @@ -789,10 +788,7 @@ class SpectrogramPlugin extends BasePlugin= 1000 ? 'kHz' : 'Hz' } - private getLabelFrequency( - index: number, - labelIndex: number, - ) { + private getLabelFrequency(index: number, labelIndex: number) { const scaleMin = this.hzToScale(this.frequencyMin) const scaleMax = this.hzToScale(this.frequencyMax) return this.scaleToHz(scaleMin + (index / labelIndex) * (scaleMax - scaleMin)) diff --git a/src/renderer.ts b/src/renderer.ts index 1368f5fb9..e6c0c274d 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -169,7 +169,8 @@ class Renderer extends EventEmitter { const div = document.createElement('div') const shadow = div.attachShadow({ mode: 'open' }) - const cspNonce = this.options.cspNonce && typeof this.options.cspNonce === 'string' ? this.options.cspNonce.replace(/"/g, '') : ''; + const cspNonce = + this.options.cspNonce && typeof this.options.cspNonce === 'string' ? this.options.cspNonce.replace(/"/g, '') : '' shadow.innerHTML = ` diff --git a/src/wavesurfer.ts b/src/wavesurfer.ts index feb777e07..420a47fea 100644 --- a/src/wavesurfer.ts +++ b/src/wavesurfer.ts @@ -150,6 +150,7 @@ class WaveSurfer extends Player { private timer: Timer private plugins: GenericPlugin[] = [] private decodedData: AudioBuffer | null = null + private stopAtPosition: number | null = null protected subscriptions: Array<() => void> = [] protected mediaSubscriptions: Array<() => void> = [] protected abortController: AbortController | null = null @@ -217,6 +218,11 @@ class WaveSurfer extends Player { const currentTime = this.updateProgress() this.emit('timeupdate', currentTime) this.emit('audioprocess', currentTime) + + // Pause audio when it reaches the stopAtPosition + if (this.stopAtPosition != null && this.isPlaying() && currentTime >= this.stopAtPosition) { + this.pause() + } } }), ) @@ -242,15 +248,18 @@ class WaveSurfer extends Player { this.onMediaEvent('pause', () => { this.emit('pause') this.timer.stop() + this.stopAtPosition = null }), this.onMediaEvent('emptied', () => { this.timer.stop() + this.stopAtPosition = null }), this.onMediaEvent('ended', () => { this.emit('timeupdate', this.getDuration()) this.emit('finish') + this.stopAtPosition = null }), this.onMediaEvent('seeking', () => { @@ -259,6 +268,7 @@ class WaveSurfer extends Player { this.onMediaEvent('error', (err) => { this.emit('error', (this.getMediaElement().error ?? new Error('Media error')) as Error) + this.stopAtPosition = null }), ) } @@ -360,10 +370,7 @@ class WaveSurfer extends Player { } if (options.peaks && options.duration) { // Create new decoded data buffer from peaks and duration - this.decodedData = Decoder.createBuffer( - options.peaks, - options.duration - ); + this.decodedData = Decoder.createBuffer(options.peaks, options.duration) } this.renderer.setOptions(this.options) @@ -427,6 +434,7 @@ class WaveSurfer extends Player { if (!this.options.media && this.isPlaying()) this.pause() this.decodedData = null + this.stopAtPosition = null // Fetch the entire audio as a blob if pre-decoded data is not provided if (!blob && !channelData) { @@ -558,6 +566,7 @@ class WaveSurfer extends Player { /** Jump to a specific time in the audio (in seconds) */ public setTime(time: number) { + this.stopAtPosition = null super.setTime(time) this.updateProgress(time) this.emit('timeupdate', time) @@ -569,6 +578,25 @@ class WaveSurfer extends Player { this.setTime(time) } + /** Start playing the audio */ + public async play(start?: number, end?: number): Promise { + if (start != null) { + this.setTime(start) + } + + const playPromise = super.play() + + if (end != null) { + if (this.media instanceof WebAudioPlayer) { + this.media.stopAt(end) + } else { + this.stopAtPosition = end + } + } + + return playPromise + } + /** Play or pause the audio */ public async playPause(): Promise { return this.isPlaying() ? this.pause() : this.play()