先上 Saladict 中实现的效果:
以及源码例子:
WaveSurfer
首先对于音频区间选择的交互,我第一反应是要做成音频处理软件那样的效果:显示波形,然后可以直接在波形上选择区间。
做 Waveform 算是小众需求,所以这方面的库不多,比较一番之后认为 WaveSurfer 这个开源项目相对靠谱,而且它可以通过插件支持区间选择。
但是它一个纯 JS 的库,结合 React 我们需要做些简单的处理,主要是组件挂载时加载以及销毁时释放。
export default class Waveform extends React.PureComponent {
componentDidMount () {
this.wavesurfer = WaveSurfer.create({
container: '#waveform-container',
})
}
componentWillUnmount () {
if (this.wavesurfer) {
this.wavesurfer.destroy()
this.wavesurfer = null
}
}
render () {
return {
<div id="waveform-container" />
}
}
}
这里面就有一个坑,Chrome 在不久前改了政策,在页面刚加载发生用户交互之前 AudioContext 是处于一个 suspended
状态,即无法进行播放,需要监听 statechange
事件在状态变成 running
之后再调用 resume()
方法恢复。
然而实测这个 statechange
事件发生的时机有点玄学,且通过 wavesurfer.backend.ac.resume()
之后不知为何还是不能加载出波形。所以最保险的方式是懒加载 WaveSurfer,在第一次播放的时候才初始化,这时几乎可以保证用户已经进行了交互。
export default class Waveform extends React.PureComponent {
initWaveSurfer = () => { this.wavesurfer = WaveSurfer.create({
container: '#waveform-container',
})
}
loadAudio = (src) => {if (this.wavesurfer) {this.reset()} else {this.initWaveSurfer()}this.wavesurfer.load(src)}reset = () => {if (this.wavesurfer) {this.wavesurfer.pause()this.wavesurfer.empty()}}
componentDidMount () {
// 监听交互事件// 回调 this.loadAudio(src) }
componentWillUnmount () {
if (this.wavesurfer) {
this.wavesurfer.destroy()
this.wavesurfer = null
}
}
render () {
return {
<div id="waveform-container" />
}
}
}
接下来处理播放和暂停,比较直接的方式是监听 Wavesurfer 的 play
和 pause
再来改变组件 state
,然而考虑到后面需要做其它处理,而这两个方法在我们后面特殊处理循环的时候触发频率会比较高,所以就不监听而分开处理。
export default class Waveform extends React.PureComponent {
state = {
isPlaying: false,
loop: true
}
initWaveSurfer = () => {
this.wavesurfer = WaveSurfer.create({
container: '#waveform-container',
})
wavesurfer.on('ready', this.play)
wavesurfer.on('finish', this.onPlayEnd)
}
play = () => {
this.setState({ isPlaying: true })
if (this.wavesurfer) {
this.wavesurfer.play()
}
}
pause = () => {
this.setState({ isPlaying: false })
if (this.wavesurfer) {
this.wavesurfer.pause()
}
}
togglePlay = () => {
this.state.isPlaying ? this.pause() : this.play()
}
onPlayEnd = () => {
this.state.loop ? this.play() : this.pause()
}
// ...
}
区间选择
接下来就是实现区间选择。Wavesurfer 提供了 Regions plugin,我们需要针对交互做些调整。
import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.min.js'
export default class Waveform extends React.PureComponent {
state = {
isPlaying: false,
loop: true
}
initWaveSurfer = () => {
this.wavesurfer = WaveSurfer.create({
container: '#waveform-container',
plugins: [RegionsPlugin.create()] })
// 允许鼠标划选区间
wavesurfer.enableDragSelection({})
// 鼠标按下去那一刻触发
wavesurfer.on('region-created', region => {
// Region 插件支持多个区间,我们只保留最新一个
this.removeRegion()
this.region = region
})
// 鼠标放开那一刻触发
wavesurfer.on('region-update-end', this.play)
// 播放指示器刚出区间区域那一刻触发
wavesurfer.on('region-out', this.onPlayEnd)
// 鼠标点任意波形区域时触发,改变指示器位置
wavesurfer.on('seek', () => {
if (!this.isInRegion()) {
// 用户点击了区间外,把区间移除掉
this.removeRegion()
}
})
wavesurfer.on('ready', this.play)
wavesurfer.on('finish', this.onPlayEnd)
}
/** 检查当前指示器是否在区间中 */
isInRegion = (region = this.region) => {
if (region && this.wavesurfer) {
const curTime = this.wavesurfer.getCurrentTime()
return curTime >= region.start && curTime <= region.end
}
return false
}
removeRegion = () => {
if (this.region) {
this.region.remove()
}
this.region = null
}
play = () => {
this.setState({ isPlaying: true })
if (this.region && !this.isInRegion()) {
// 如果指示器不在区间中则重新从区间起点播放
this.wavesurfer.play(this.region.start)
} else {
// 否则继续播放
this.wavesurfer.play()
}
}
// ...
}
注意 Regions 插件中其实提供了循环播放的方法,但是为了接下来更细粒度的控制,我们改为监听指示器移出区间右侧时的 region-out
事件,然后与 finish
事件用同样的方法处理。
本文介绍了如何结合 React 和 Wavesurfer 显示音频波形,实现区间选择和循环播放。下篇文章中我将继续分享如何实现音频的加速和减速。
评论