Skip to content

Commit

Permalink
Feat: play(start, stop)
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Jan 30, 2025
1 parent 045e95e commit e0623dd
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 51 deletions.
4 changes: 2 additions & 2 deletions cypress/e2e/spectrogram.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -99,7 +99,7 @@ xdescribe('WaveSurfer Spectrogram plugin tests', () => {
scale: scale,
frequencyMin: 0,
frequencyMax: 4000,
splitChannels: false
splitChannels: false,
}),
],
})
Expand Down
2 changes: 1 addition & 1 deletion examples/record-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion examples/regions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions src/plugins/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -334,9 +334,9 @@ class SingleRegion extends EventEmitter<RegionEvents> 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 */
Expand Down Expand Up @@ -588,9 +588,8 @@ class RegionsPlugin extends BasePlugin<RegionsPluginEvents, RegionsPluginOptions
this.emit('region-updated', region)
}),

region.on('play', () => {
this.wavesurfer?.play()
this.wavesurfer?.setTime(region.start)
region.on('play', (end?: number) => {
this.wavesurfer?.play(region.start, end)
}),

region.on('click', (e) => {
Expand Down
66 changes: 31 additions & 35 deletions src/plugins/spectrogram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][] **/
Expand Down Expand Up @@ -557,13 +553,7 @@ class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramP
width,
Math.round(bitmapHeight * (rMax1 - rMin)),
).then((bitmap) => {
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)
})
}

Expand Down Expand Up @@ -593,30 +583,38 @@ class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramP
const filterMin = hzToScale(0)
const filterMax = hzToScale(sampleRate / 2)
const filterBank = Array.from({ length: numFilters }, () => 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)
Expand Down Expand Up @@ -708,7 +706,8 @@ class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramP

private getFrequencies(buffer: AudioBuffer): Uint8Array[][] {
const fftSamples = this.fftSamples
const channels = this.options.splitChannels ?? this.wavesurfer?.options.splitChannels ? buffer.numberOfChannels : 1
const channels =
(this.options.splitChannels ?? this.wavesurfer?.options.splitChannels) ? buffer.numberOfChannels : 1

this.frequencyMax = this.frequencyMax || buffer.sampleRate / 2

Expand All @@ -732,16 +731,16 @@ class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramP
switch (this.scale) {
case 'mel':
filterBank = this.createFilterBank(this.numMelFilters, sampleRate, this.hzToMel, this.melToHz)
break;
break
case 'logarithmic':
filterBank = this.createFilterBank(this.numLogFilters, sampleRate, this.hzToLog, this.logToHz)
break;
break
case 'bark':
filterBank = this.createFilterBank(this.numBarkFilters, sampleRate, this.hzToBark, this.barkToHz)
break;
break
case 'erb':
filterBank = this.createFilterBank(this.numErbFilters, sampleRate, this.hzToErb, this.erbToHz)
break;
break
}

for (let c = 0; c < channels; c++) {
Expand All @@ -753,7 +752,7 @@ class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramP
while (currentOffset + fftSamples < channelData.length) {
const segment = channelData.slice(currentOffset, currentOffset + fftSamples)
const array = new Uint8Array(fftSamples / 2)
let spectrum = fft.calculateSpectrum(segment);
let spectrum = fft.calculateSpectrum(segment)
if (filterBank) {
spectrum = this.applyFilterBank(spectrum, filterBank)
}
Expand All @@ -766,7 +765,7 @@ class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramP
} else if (valueDB > -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)
Expand All @@ -789,10 +788,7 @@ class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, SpectrogramP
return freq >= 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))
Expand Down
3 changes: 2 additions & 1 deletion src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ class Renderer extends EventEmitter<RendererEvents> {
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 = `
<style${cspNonce ? ` nonce="${cspNonce}"` : ''}>
Expand Down
36 changes: 32 additions & 4 deletions src/wavesurfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ class WaveSurfer extends Player<WaveSurferEvents> {
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
Expand Down Expand Up @@ -217,6 +218,11 @@ class WaveSurfer extends Player<WaveSurferEvents> {
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()
}
}
}),
)
Expand All @@ -242,15 +248,18 @@ class WaveSurfer extends Player<WaveSurferEvents> {
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', () => {
Expand All @@ -259,6 +268,7 @@ class WaveSurfer extends Player<WaveSurferEvents> {

this.onMediaEvent('error', (err) => {
this.emit('error', (this.getMediaElement().error ?? new Error('Media error')) as Error)
this.stopAtPosition = null
}),
)
}
Expand Down Expand Up @@ -360,10 +370,7 @@ class WaveSurfer extends Player<WaveSurferEvents> {
}
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)

Expand Down Expand Up @@ -427,6 +434,7 @@ class WaveSurfer extends Player<WaveSurferEvents> {
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) {
Expand Down Expand Up @@ -558,6 +566,7 @@ class WaveSurfer extends Player<WaveSurferEvents> {

/** 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)
Expand All @@ -569,6 +578,25 @@ class WaveSurfer extends Player<WaveSurferEvents> {
this.setTime(time)
}

/** Start playing the audio */
public async play(start?: number, end?: number): Promise<void> {
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<void> {
return this.isPlaying() ? this.pause() : this.play()
Expand Down

0 comments on commit e0623dd

Please sign in to comment.