wavesurfer.js 7.0.0-alpha.1 → 7.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts DELETED
@@ -1,252 +0,0 @@
1
- import Fetcher from './fetcher.js'
2
- import Decoder from './decoder.js'
3
- import Renderer from './renderer.js'
4
- import Player from './player.js'
5
- import WebAudioPlayer from './player-webaudio.js'
6
- import EventEmitter, { type GeneralEventTypes } from './event-emitter.js'
7
- import Timer from './timer.js'
8
- import BasePlugin from './base-plugin.js'
9
-
10
- export enum PlayerType {
11
- WebAudio = 'WebAudio',
12
- MediaElement = 'MediaElement',
13
- }
14
-
15
- export type WaveSurferOptions = {
16
- /** HTML element or CSS selector */
17
- container: HTMLElement | string | null
18
- /** Height of the waveform in pixels */
19
- height?: number
20
- /** The color of the waveform */
21
- waveColor?: string
22
- /** The color of the progress mask */
23
- progressColor?: string
24
- /** If set, the waveform will be rendered in bars like so: ▁ ▂ ▇ ▃ ▅ ▂ */
25
- barWidth?: number
26
- /** Spacing between bars in pixels */
27
- barGap?: number
28
- /** Rounded borders for bars */
29
- barRadius?: number
30
- /** Minimum pixels per second of audio (zoom) */
31
- minPxPerSec?: number
32
- /** Audio URL */
33
- url?: string
34
- /** Pre-computed audio data */
35
- channelData?: Float32Array[]
36
- /** Pre-computed duration */
37
- duration?: number
38
- /** Player "backend", the default is MediaElement */
39
- backend?: PlayerType
40
- /** Use an existing media element instead of creating one */
41
- media?: HTMLMediaElement
42
- }
43
-
44
- const defaultOptions = {
45
- height: 128,
46
- waveColor: '#999',
47
- progressColor: '#555',
48
- minPxPerSec: 0,
49
- backend: 'MediaElement',
50
- }
51
-
52
- export type WaveSurferEvents = {
53
- ready: { duration: number }
54
- canplay: void
55
- play: void
56
- pause: void
57
- audioprocess: { currentTime: number }
58
- seek: { time: number }
59
- }
60
-
61
- export type WaveSurferPluginParams = {
62
- wavesurfer: WaveSurfer
63
- renderer: WaveSurfer['renderer']
64
- }
65
-
66
- export class WaveSurfer extends EventEmitter<WaveSurferEvents> {
67
- private options: WaveSurferOptions & typeof defaultOptions
68
-
69
- private fetcher: Fetcher
70
- private decoder: Decoder
71
- private renderer: Renderer
72
- private player: Player
73
- private timer: Timer
74
-
75
- private plugins: BasePlugin<GeneralEventTypes>[] = []
76
- private subscriptions: Array<() => void> = []
77
- private channelData: Float32Array[] | null = null
78
- private duration = 0
79
-
80
- /** Create a new WaveSurfer instance */
81
- public static create(options: WaveSurferOptions) {
82
- return new WaveSurfer(options)
83
- }
84
-
85
- /** Create a new WaveSurfer instance */
86
- constructor(options: WaveSurferOptions) {
87
- super()
88
-
89
- this.options = Object.assign({}, defaultOptions, options)
90
-
91
- this.fetcher = new Fetcher()
92
- this.decoder = new Decoder()
93
- this.timer = new Timer()
94
-
95
- this.player = new (this.options.backend === PlayerType.WebAudio ? WebAudioPlayer : Player)({
96
- media: this.options.media,
97
- })
98
-
99
- this.renderer = new Renderer({
100
- container: this.options.container,
101
- height: this.options.height,
102
- waveColor: this.options.waveColor,
103
- progressColor: this.options.progressColor,
104
- minPxPerSec: this.options.minPxPerSec,
105
- barWidth: this.options.barWidth,
106
- barGap: this.options.barGap,
107
- barRadius: this.options.barRadius,
108
- })
109
-
110
- this.initPlayerEvents()
111
- this.initRendererEvents()
112
- this.initTimerEvents()
113
-
114
- const url = this.options.url || this.options.media?.src
115
- if (url) {
116
- this.load(url, this.options.channelData, this.options.duration)
117
- }
118
- }
119
-
120
- private initPlayerEvents() {
121
- this.subscriptions.push(
122
- this.player.on('timeupdate', () => {
123
- const currentTime = this.getCurrentTime()
124
- this.renderer.renderProgress(currentTime / this.duration, this.isPlaying())
125
- this.emit('audioprocess', { currentTime })
126
- }),
127
-
128
- this.player.on('play', () => {
129
- this.emit('play')
130
- }),
131
-
132
- this.player.on('pause', () => {
133
- this.emit('pause')
134
- }),
135
-
136
- this.player.on('canplay', () => {
137
- this.emit('canplay')
138
- }),
139
-
140
- this.player.on('seeking', () => {
141
- this.emit('seek', { time: this.getCurrentTime() })
142
- }),
143
- )
144
- }
145
-
146
- private initRendererEvents() {
147
- // Seek on click
148
- this.subscriptions.push(
149
- this.renderer.on('click', ({ relativeX }) => {
150
- const time = this.getDuration() * relativeX
151
- this.seekTo(time)
152
- }),
153
- )
154
- }
155
-
156
- private initTimerEvents() {
157
- // The timer fires every 16ms for a smooth progress animation
158
- this.subscriptions.push(
159
- this.timer.on('tick', () => {
160
- if (this.isPlaying()) {
161
- const currentTime = this.getCurrentTime()
162
- this.renderer.renderProgress(currentTime / this.duration, true)
163
- this.emit('audioprocess', { currentTime })
164
- }
165
- }),
166
- )
167
- }
168
-
169
- /** Unmount wavesurfer */
170
- public destroy() {
171
- this.subscriptions.forEach((unsubscribe) => unsubscribe())
172
- this.plugins.forEach((plugin) => plugin.destroy())
173
- this.timer.destroy()
174
- this.player.destroy()
175
- this.decoder.destroy()
176
- this.renderer.destroy()
177
- }
178
-
179
- /** Load an audio file by URL */
180
- public async load(url: string, channelData?: Float32Array[], duration?: number) {
181
- this.player.loadUrl(url)
182
-
183
- // Fetch and decode the audio of no pre-computed audio data is provided
184
- if (channelData == null || duration == null) {
185
- const audio = await this.fetcher.load(url)
186
- const data = await this.decoder.decode(audio)
187
- channelData = data.channelData
188
- duration = data.duration
189
- }
190
-
191
- this.channelData = channelData
192
- this.duration = duration
193
-
194
- this.renderer.render(this.channelData, this.duration)
195
-
196
- this.emit('ready', { duration: this.duration })
197
- }
198
-
199
- /** Zoom in or out */
200
- public zoom(minPxPerSec: number) {
201
- if (this.channelData == null || this.duration == null) {
202
- throw new Error('No audio loaded')
203
- }
204
- this.renderer.render(this.channelData, this.duration, minPxPerSec)
205
- }
206
-
207
- /** Start playing the audio */
208
- public play() {
209
- this.player.play()
210
- }
211
-
212
- /** Pause the audio */
213
- public pause() {
214
- this.player.pause()
215
- }
216
-
217
- /** Skip to a time position in seconds */
218
- public seekTo(time: number) {
219
- this.player.seekTo(time)
220
- }
221
-
222
- /** Check if the audio is playing */
223
- public isPlaying(): boolean {
224
- return this.player.isPlaying()
225
- }
226
-
227
- /** Get the duration of the audio in seconds */
228
- public getDuration(): number {
229
- return this.duration
230
- }
231
-
232
- /** Get the current audio position in seconds */
233
- public getCurrentTime(): number {
234
- return this.player.getCurrentTime()
235
- }
236
-
237
- /** Register and initialize a plugin */
238
- public registerPlugin<T extends BasePlugin<GeneralEventTypes>>(
239
- CustomPlugin: new (params: WaveSurferPluginParams) => T,
240
- ): T {
241
- const plugin = new CustomPlugin({
242
- wavesurfer: this,
243
- renderer: this.renderer,
244
- })
245
-
246
- this.plugins.push(plugin)
247
-
248
- return plugin
249
- }
250
- }
251
-
252
- export default WaveSurfer
@@ -1,34 +0,0 @@
1
- import Player from './player.js'
2
-
3
- class WebAudioPlayer extends Player {
4
- audioCtx: AudioContext | null = null
5
- sourceNode: MediaElementAudioSourceNode | null = null
6
-
7
- destroy() {
8
- this.sourceNode?.disconnect()
9
- this.sourceNode = null
10
- this.audioCtx?.close()
11
- this.audioCtx = null
12
-
13
- super.destroy()
14
- }
15
-
16
- loadUrl(url: string) {
17
- super.loadUrl(url)
18
-
19
- if (!this.audioCtx) {
20
- this.audioCtx = new (window.AudioContext ||
21
- (window as unknown as { webkitAudioContext: AudioContext }).webkitAudioContext)({
22
- latencyHint: 'playback',
23
- })
24
- }
25
-
26
- if (this.sourceNode) {
27
- this.sourceNode.disconnect()
28
- }
29
- this.sourceNode = this.audioCtx.createMediaElementSource(this.media)
30
- this.sourceNode.connect(this.audioCtx.destination)
31
- }
32
- }
33
-
34
- export default WebAudioPlayer
package/src/player.ts DELETED
@@ -1,50 +0,0 @@
1
- class Player {
2
- protected media: HTMLMediaElement
3
- private isExternalMedia = false
4
-
5
- constructor({ media }: { media?: HTMLMediaElement }) {
6
- if (media) {
7
- this.media = media
8
- this.isExternalMedia = true
9
- } else {
10
- this.media = document.createElement('audio')
11
- }
12
- }
13
-
14
- on(event: keyof HTMLMediaElementEventMap, callback: () => void): () => void {
15
- this.media.addEventListener(event, callback)
16
- return () => this.media.removeEventListener(event, callback)
17
- }
18
-
19
- destroy() {
20
- if (!this.isExternalMedia) {
21
- this.media.remove()
22
- }
23
- }
24
-
25
- loadUrl(src: string) {
26
- this.media.src = src
27
- }
28
-
29
- getCurrentTime() {
30
- return this.media.currentTime
31
- }
32
-
33
- play() {
34
- this.media.play()
35
- }
36
-
37
- pause() {
38
- this.media.pause()
39
- }
40
-
41
- isPlaying() {
42
- return this.media.currentTime > 0 && !this.media.paused && !this.media.ended
43
- }
44
-
45
- seekTo(time: number) {
46
- this.media.currentTime = time
47
- }
48
- }
49
-
50
- export default Player
@@ -1,240 +0,0 @@
1
- import BasePlugin from '../base-plugin.js'
2
- import { WaveSurferPluginParams } from '../index.js'
3
-
4
- const MIN_WIDTH = 10
5
-
6
- type Region = {
7
- startTime: number
8
- endTime: number
9
- title: string
10
- start: number
11
- end: number
12
- element: HTMLElement
13
- }
14
-
15
- type RegionsPluginEvents = {
16
- 'region-created': { region: Region }
17
- 'region-updated': { region: Region }
18
- 'region-clicked': { region: Region }
19
- }
20
-
21
- class RegionsPlugin extends BasePlugin<RegionsPluginEvents> {
22
- private dragStart = NaN
23
- private container: HTMLElement
24
- private regions: Region[] = []
25
- private createdRegion: Region | null = null
26
- private modifiedRegion: Region | null = null
27
- private isResizingLeft = false
28
- private isMoving = false
29
-
30
- /** Create an instance of RegionsPlugin */
31
- constructor(params: WaveSurferPluginParams) {
32
- super(params)
33
-
34
- this.container = this.initContainer()
35
-
36
- const unsubscribeReady = this.wavesurfer.on('ready', () => {
37
- const wrapper = this.renderer.getContainer()
38
- wrapper.appendChild(this.container)
39
- unsubscribeReady()
40
- })
41
- this.subscriptions.push(unsubscribeReady)
42
-
43
- const wsContainer = this.renderer.getContainer()
44
- wsContainer.addEventListener('mousedown', this.handleMouseDown)
45
- document.addEventListener('mousemove', this.handleMouseMove)
46
- document.addEventListener('mouseup', this.handleMouseUp)
47
- }
48
-
49
- /** Unmounts the regions */
50
- public destroy() {
51
- this.renderer.getContainer().removeEventListener('mousedown', this.handleMouseDown)
52
- document.removeEventListener('mousemove', this.handleMouseMove)
53
- document.removeEventListener('mouseup', this.handleMouseUp, true)
54
-
55
- this.container?.remove()
56
-
57
- super.destroy()
58
- }
59
-
60
- private initContainer(): HTMLElement {
61
- const div = document.createElement('div')
62
- div.style.position = 'absolute'
63
- div.style.top = '0'
64
- div.style.left = '0'
65
- div.style.width = '100%'
66
- div.style.height = '100%'
67
- div.style.zIndex = '3'
68
- div.style.pointerEvents = 'none'
69
- return div
70
- }
71
-
72
- private handleMouseDown = (e: MouseEvent) => {
73
- this.dragStart = e.clientX - this.container.getBoundingClientRect().left
74
- }
75
-
76
- private handleMouseMove = (e: MouseEvent) => {
77
- const dragEnd = e.clientX - this.container.getBoundingClientRect().left
78
-
79
- if (this.modifiedRegion && this.isMoving) {
80
- this.moveRegion(this.modifiedRegion, dragEnd - this.dragStart)
81
- this.dragStart = dragEnd
82
- return
83
- }
84
-
85
- if (this.modifiedRegion) {
86
- this.updateRegion(
87
- this.modifiedRegion,
88
- this.isResizingLeft ? dragEnd : undefined,
89
- this.isResizingLeft ? undefined : dragEnd,
90
- )
91
- return
92
- }
93
-
94
- if (!isNaN(this.dragStart)) {
95
- const dragEnd = e.clientX - this.container.getBoundingClientRect().left
96
-
97
- if (dragEnd - this.dragStart >= MIN_WIDTH) {
98
- if (!this.createdRegion) {
99
- this.renderer.getContainer().style.pointerEvents = 'none'
100
-
101
- this.createdRegion = this.createRegion(this.dragStart, dragEnd)
102
- } else {
103
- this.updateRegion(this.createdRegion, this.dragStart, dragEnd)
104
- }
105
- }
106
- }
107
- }
108
-
109
- private handleMouseUp = () => {
110
- if (this.createdRegion) {
111
- this.addRegion(this.createdRegion)
112
- this.createdRegion = null
113
- }
114
- this.modifiedRegion = null
115
- this.isMoving = false
116
- this.dragStart = NaN
117
- this.renderer.getContainer().style.pointerEvents = ''
118
- }
119
-
120
- private createRegionElement(start: number, end: number, title = ''): HTMLElement {
121
- const el = document.createElement('div')
122
- el.style.position = 'absolute'
123
- el.style.left = `${start}px`
124
- el.style.width = `${end - start}px`
125
- el.style.height = '100%'
126
- el.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'
127
- el.style.transition = 'background-color 0.2s ease'
128
- el.style.cursor = 'move'
129
- el.style.pointerEvents = 'all'
130
- el.title = title
131
-
132
- const leftHandle = document.createElement('div')
133
- leftHandle.style.position = 'absolute'
134
- leftHandle.style.left = '0'
135
- leftHandle.style.width = '6px'
136
- leftHandle.style.height = '100%'
137
- leftHandle.style.borderLeft = '2px solid rgba(0, 0, 0, 0.5)'
138
- leftHandle.style.cursor = 'ew-resize'
139
- leftHandle.style.pointerEvents = 'all'
140
- el.appendChild(leftHandle)
141
-
142
- const rightHandle = document.createElement('div')
143
- rightHandle.style.position = 'absolute'
144
- rightHandle.style.right = '0'
145
- rightHandle.style.width = '6px'
146
- rightHandle.style.height = '100%'
147
- rightHandle.style.borderRight = '2px solid rgba(0, 0, 0, 0.5)'
148
- rightHandle.style.cursor = 'ew-resize'
149
- rightHandle.style.pointerEvents = 'all'
150
- el.appendChild(rightHandle)
151
-
152
- leftHandle.addEventListener('mousedown', (e) => {
153
- e.stopPropagation()
154
- this.modifiedRegion = this.regions.find((r) => r.element === el) || null
155
- this.isResizingLeft = true
156
- this.isMoving = false
157
- })
158
-
159
- rightHandle.addEventListener('mousedown', (e) => {
160
- e.stopPropagation()
161
- this.modifiedRegion = this.regions.find((r) => r.element === el) || null
162
- this.isResizingLeft = false
163
- this.isMoving = false
164
- })
165
-
166
- el.addEventListener('mousedown', () => {
167
- this.modifiedRegion = this.regions.find((r) => r.element === el) || null
168
- this.isMoving = true
169
- })
170
-
171
- el.addEventListener('click', () => {
172
- const region = this.regions.find((r) => r.element === el)
173
- if (region) {
174
- this.emit('region-clicked', { region })
175
- }
176
- })
177
-
178
- this.container.appendChild(el)
179
-
180
- return el
181
- }
182
-
183
- private createRegion(start: number, end: number, title = ''): Region {
184
- const duration = this.wavesurfer.getDuration()
185
- const width = this.container.clientWidth
186
-
187
- return {
188
- element: this.createRegionElement(start, end, title),
189
- start,
190
- end,
191
- startTime: (start / width) * duration,
192
- endTime: (end / width) * duration,
193
- title,
194
- }
195
- }
196
-
197
- private addRegion(region: Region) {
198
- this.regions.push(region)
199
-
200
- this.emit('region-created', { region })
201
- }
202
-
203
- private updateRegion(region: Region, start?: number, end?: number) {
204
- if (start != null) {
205
- region.start = start ?? region.start
206
- region.element.style.left = `${region.start}px`
207
- region.startTime = (start / this.container.clientWidth) * this.wavesurfer.getDuration()
208
- }
209
-
210
- if (end != null) {
211
- region.end = end
212
- region.element.style.width = `${region.end - region.start}px`
213
- region.endTime = (end / this.container.clientWidth) * this.wavesurfer.getDuration()
214
- }
215
-
216
- this.emit('region-updated', { region })
217
- }
218
-
219
- private moveRegion(region: Region, delta: number) {
220
- this.updateRegion(region, region.start + delta, region.end + delta)
221
- }
222
-
223
- /** Create a region at a given start and end time, with an optional title */
224
- public addRegionAtTime(startTime: number, endTime: number, title = '') {
225
- const duration = this.wavesurfer.getDuration()
226
- const width = this.container.clientWidth
227
- const start = (startTime / duration) * width
228
- const end = (endTime / duration) * width
229
- const region = this.createRegion(start, end, title)
230
- this.addRegion(region)
231
- return region
232
- }
233
-
234
- /** Set the background color of a region */
235
- public setRegionColor(region: Region, color: string) {
236
- region.element.style.backgroundColor = color
237
- }
238
- }
239
-
240
- export default RegionsPlugin