Skip to content

Commit 095e8d4

Browse files
committed
优化 Recorder 类结构,增加音频信号和实例记录管理对象。解决并发实例冲突问题和实例销毁对其他实例的副作用
1 parent d88400b commit 095e8d4

File tree

2 files changed

+122
-97
lines changed

2 files changed

+122
-97
lines changed

src/lib/recorder.js

+101-85
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,119 @@
11
import recordWorker from './record-worker'
22
const URL = window.URL || window.webkitURL
3-
let recorderInstance = null
4-
function Recorder () {
5-
this.config = null
6-
this.recording = false
7-
this.callback = null
8-
this.worker = null
3+
4+
const globalProxy = {
5+
headInstanceReady: false,
6+
proxyInstanceCount: 0,
7+
defaultConfig: {
8+
sampleRate: 48000, // 采样率(48000),注意:设定的值必须为 48000 的约数
9+
bufferSize: 4096, // 缓存大小,用来缓存声音
10+
sampleBits: 16, // 采样比特率,8 或 16
11+
twoChannel: false // 双声道
12+
},
13+
stream: null,
14+
recorderProxy: null,
15+
proxyInstance: null
16+
}
17+
function throwError (message) {
18+
if (message.toString().includes('NotFoundError')) {
19+
alert('未找到可用的录音设备!')
20+
return
21+
}
22+
console.error(message)
23+
}
24+
class Recorder {
25+
constructor (config = {}) {
26+
this.config = Object.assign({}, globalProxy.defaultConfig, config)
27+
this.recording = false
28+
this.callback = null
29+
this.worker = null
30+
this.ready = false,
31+
this.createTime = new Date().getTime()
32+
}
933
}
1034
Recorder.prototype = {
11-
ready: function () {
12-
this.proxyInstanceCount++
13-
document.dispatchEvent(new Event('recorder-init'))
14-
},
15-
start: function () {
35+
start () {
1636
if (this.recording) return
37+
globalProxy.recorderProxy.onaudioprocess = this.holdBuffer.bind(this)
1738
this.recording = true
1839
},
19-
stop: function (success) {
40+
41+
stop (success) {
2042
if (!this.recording) return
2143
this.callback = success
2244
this.recording = false
45+
globalProxy.recorderProxy.onaudioprocess = null
2346
},
24-
clear: function () {
47+
48+
clear () {
2549
if (this.recording) {
26-
this.throwError('请先停止录音!')
50+
throwError('请先停止录音!')
2751
return
2852
}
2953
this.worker.postMessage({
3054
command: 'clear'
3155
})
3256
},
33-
destroy: function () {
34-
this.proxyInstanceCount--
57+
58+
destroy () {
59+
globalProxy.proxyInstanceCount--
3560
this.config = null
3661
this.worker && this.worker.terminate()
3762
this.worker = null
38-
if (this.proxyInstanceCount < 1) {
39-
this.recorderProxy = null
40-
if (this.proxyInstance && this.proxyInstance.state !== 'closed') {
41-
this.proxyInstance.suspend()
42-
this.proxyInstance.close()
43-
this.proxyInstance = null
63+
this.ready = false
64+
if (globalProxy.proxyInstanceCount < 1) {
65+
globalProxy.headInstanceReady = false
66+
globalProxy.recorderProxy = null
67+
if (globalProxy.proxyInstance && globalProxy.proxyInstance.state !== 'closed') {
68+
globalProxy.proxyInstance.suspend()
69+
globalProxy.proxyInstance.close()
70+
globalProxy.proxyInstance = null
4471
}
45-
this.stream && this.stream.getTracks().forEach(track => track.stop())
46-
this.stream = null
47-
window.isAudioAvailable = this.isAudioAvailable = false
72+
globalProxy.stream && globalProxy.stream.getTracks().forEach(track => track.stop())
73+
globalProxy.stream = null
4874
}
49-
recorderInstance = null
5075
},
51-
handleStream: function (stream) {
52-
this.stream = stream
76+
77+
handleStream (stream) {
78+
globalProxy.stream = stream
5379
const channelNumber = this.config.twoChannel ? 2 : 1
5480
// 创建一个音频环境对象
5581
const ACProxy = window.AudioContext || window.webkitAudioContext
5682
if (!ACProxy) {
57-
this.throwError('浏览器不支持录音功能!')
83+
throwError('浏览器不支持录音功能!')
5884
return
5985
}
60-
this.proxyInstance = new ACProxy()
86+
globalProxy.proxyInstance = new ACProxy()
6187

62-
if (this.proxyInstance.createScriptProcessor) {
63-
this.recorderProxy = this.proxyInstance.createScriptProcessor(this.config.bufferSize, channelNumber, channelNumber)
64-
} else if (recorder.proxyInstance.createJavaScriptNode) {
65-
this.recorderProxy = this.proxyInstance.createJavaScriptNode(this.config.bufferSize, channelNumber, channelNumber)
88+
if (globalProxy.proxyInstance.createScriptProcessor) {
89+
globalProxy.recorderProxy = globalProxy.proxyInstance.createScriptProcessor(this.config.bufferSize, channelNumber, channelNumber)
90+
} else if (globalProxy.proxyInstance.createJavaScriptNode) {
91+
globalProxy.recorderProxy = globalProxy.proxyInstance.createJavaScriptNode(this.config.bufferSize, channelNumber, channelNumber)
6692
} else {
67-
this.throwError('浏览器不支持录音功能!')
93+
throwError('浏览器不支持录音功能!')
6894
}
6995

7096
// 将声音输入这个对像
71-
const audioInputSource = this.proxyInstance.createMediaStreamSource(this.stream)
97+
const audioInputSource = globalProxy.proxyInstance.createMediaStreamSource(globalProxy.stream)
7298

73-
audioInputSource.connect(this.recorderProxy)
74-
this.recorderProxy.connect(audioInputSource.context.destination)
99+
audioInputSource.connect(globalProxy.recorderProxy)
100+
globalProxy.recorderProxy.connect(audioInputSource.context.destination)
75101

76-
this.ready()
102+
this.ready = true
77103

78-
this.recorderProxy.onaudioprocess = this.holdBuffer.bind(this)
79-
80-
window.isAudioAvailable = this.isAudioAvailable = true
104+
globalProxy.headInstanceReady = true
81105
},
82-
init: function (config = {}) {
83-
this.config = Object.assign({}, this.defaultConfig, config)
106+
107+
init () {
108+
if (globalProxy.proxyInstanceCount && !globalProxy.headInstanceReady) {
109+
const waitHeadInstance = this
110+
requestAnimationFrame(function () {
111+
waitHeadInstance.init()
112+
})
113+
// console.info('wait head instance')
114+
return
115+
}
116+
globalProxy.proxyInstanceCount++
84117

85118
// 加载并启动 record worker
86119
let workerString = recordWorker.toString()
@@ -91,7 +124,7 @@ Recorder.prototype = {
91124
const workerURL = URL.createObjectURL(workerBlob)
92125
this.worker = new Worker(workerURL)
93126
URL.revokeObjectURL(workerURL)
94-
127+
95128
const instance = this
96129
this.worker.onmessage = function (e) {
97130
instance.callback && instance.callback(e.data)
@@ -102,8 +135,8 @@ Recorder.prototype = {
102135
config: this.config
103136
})
104137

105-
if (this.recorderProxy) {
106-
this.ready()
138+
if (globalProxy.recorderProxy) {
139+
this.ready = true
107140
} else {
108141
if (!navigator.mediaDevices) {
109142
navigator.mediaDevices = {}
@@ -120,59 +153,42 @@ Recorder.prototype = {
120153
})
121154
}
122155
}
123-
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(this.handleStream.bind(this)).catch(this.throwError)
124-
}
125-
},
126-
defaultConfig: {
127-
sampleRate: 48000, // 采样率(48000),注意:设定的值必须为 48000 的约数
128-
bufferSize: 4096, // 缓存大小,用来缓存声音
129-
sampleBits: 16, // 采样比特率,8 或 16
130-
twoChannel: false // 双声道
131-
},
132-
proxyInstanceCount: 0,
133-
stream: null,
134-
recorderProxy: null,
135-
proxyInstance: null,
136-
holdBuffer: function (e) {
137-
if (this.recording) {
138-
const data = e.inputBuffer
139-
const buffer = !this.config.twoChannel ? [
140-
data.getChannelData(0)
141-
] : [
142-
data.getChannelData(0),
143-
data.getChannelData(1)
144-
]
145-
this.worker.postMessage({
146-
command: 'record',
147-
buffer
148-
})
156+
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(this.handleStream.bind(this)).catch(throwError)
149157
}
150158
},
151-
exportWAV: function (success, type) {
159+
160+
exportWAV (success, type) {
152161
this.callback = success
153162
type = type || this.config.type || 'audio/wav'
154163
this.worker.postMessage({
155164
command: 'exportWAV',
156165
type
157166
})
158167
},
159-
getSource: function () {
168+
169+
getSource () {
160170
return new Promise((resolve) => {
161171
this.exportWAV(function (data) {
162172
resolve(data)
163173
}, 'audio/wav')
164174
})
165175
},
166-
throwError: (this.config && this.config.error) || function (message) {
167-
if (message.toString().includes('NotFoundError')) {
168-
alert('未找到可用的录音设备!')
169-
return
176+
177+
holdBuffer (e) {
178+
if (this.recording) {
179+
const data = e.inputBuffer
180+
const buffer = !this.config.twoChannel ? [
181+
data.getChannelData(0)
182+
] : [
183+
data.getChannelData(0),
184+
data.getChannelData(1)
185+
]
186+
this.worker.postMessage({
187+
command: 'record',
188+
buffer
189+
})
170190
}
171-
console.error(message)
172-
},
173-
isAudioAvailable: false
191+
}
174192
}
175193

176-
recorderInstance = new Recorder()
177-
178-
export default recorderInstance
194+
export default Recorder

src/lib/voice-input-button.vue

+21-12
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@
2323
</template>
2424

2525
<script>
26-
import recorder from './recorder'
26+
import Recorder from './recorder'
2727
import { IAT } from './iat-api'
2828
import ASRConfig from './asr-config'
2929
import loading from './components/icons/loading'
3030
import recordingIcon from './components/icons/recording-icon'
3131
import microphone from './components/icons/microphone'
3232
import recordingTip from './components/recording-tip'
33+
const freezeProperty = (obj, key) => {
34+
Object.defineProperty(obj, key, {
35+
configurable: false
36+
})
37+
}
3338
export default {
3439
name: 'voice-input-button',
3540
components: {
@@ -57,7 +62,7 @@ export default {
5762
},
5863
data () {
5964
return {
60-
recorder,
65+
recorder: null,
6166
processing: false,
6267
startTime: 0,
6368
time: 0,
@@ -78,7 +83,7 @@ export default {
7883
},
7984
start (e) {
8085
e.preventDefault()
81-
if (!this.recorder.isAudioAvailable) {
86+
if (!this.isAudioAvailable) {
8287
alert('无法录音:未找到录音设备、当前浏览器不支持录音或用户未授权!')
8388
return
8489
}
@@ -127,15 +132,8 @@ export default {
127132
console.warn(error)
128133
this.processing = false
129134
})
130-
},
131-
enableAudioButton () {
132-
this.isAudioAvailable = true
133-
this.$emit('record-ready')
134135
}
135136
},
136-
created () {
137-
document.addEventListener('recorder-init', this.enableAudioButton)
138-
},
139137
computed: {
140138
pressMode () {
141139
return this.interactiveMode !== 'touch'
@@ -144,12 +142,23 @@ export default {
144142
return this.interactiveMode === 'touch'
145143
}
146144
},
145+
watch: {
146+
'recorder.ready' (value) {
147+
this.isAudioAvailable = value
148+
value && this.$emit('record-ready')
149+
}
150+
},
147151
mounted () {
148152
let {sampleRate, sampleBits} = ASRConfig
149-
this.recorder.init({sampleRate, sampleBits})
153+
const recorder = new Recorder({sampleRate, sampleBits})
154+
const freezKeys = ['worker', 'config', 'callback', 'createTime']
155+
freezKeys.forEach(key => {
156+
freezeProperty(recorder, key)
157+
})
158+
this.recorder = recorder
159+
this.recorder.init()
150160
},
151161
beforeDestroy () {
152-
document.removeEventListener('recorder-init', this.enableAudioButton)
153162
this.recorder.destroy()
154163
this.recorder = null
155164
}

0 commit comments

Comments
 (0)