/
recording.html
193 lines (167 loc) · 7.39 KB
/
recording.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
<html>
<head>
<meta charset="utf-8">
<title>Lyra Example</title>
</head>
<body>
<h1>Lyra Example</h1>
<a href="https://github.com/google/lyra">google/lyra</a> で提供されている音声コーデックを WebAssembly を使ってブラウザで動作させるデモです。<br />
<small>※ このデモは Chrome や Edge などの Chromium ベースのブラウザでのみ動作します</small>
<br /><br />
WebAssembly ビルド用のリポジトリ: <a href="https://github.com/shiguredo/lyra-wasm">shiguredo/lyra-wasm</a><br />
<h2>使用手順</h2>
<ol>
<li>末尾の「録画開始」ボタンを押下すると、マイク入力の録音が始まります(5 秒間)</li>
<li>録音完了後に「再生開始(Lyra)」ボタンを押すと Lyra でエンコードされた音声が再生されます</li>
<li>「再生開始(オリジナル)」ボタンを押すと比較用にオリジナルの録音音声が再生されます</li>
</ol>
<h2>エンコード設定</h2>
<b>ビットレート: </b>
<select id="bitrate" size="1">
<option value="3200">3200 bps</option>
<option value="6000" selected>6000 bps</option>
<option value="9200">9200 bps</option>
</select><br />
<b>DTX: </b>
<input type="checkbox" id="dtx">有効にする<br />
<small>※ 設定変更時に「録画開始」をやり直す必要はありません</small>
<h2>操作</h2>
<input value="録音開始" type="button" onClick="startRecording()">
<input value="再生開始(Lyra)" type="button" onClick="playProcessedAudio()">
<input value="再生開始(オリジナル)" type="button" onClick="playOriginalAudio()">
<br />
<audio id="audio" autoplay playsinline></audio>
<script src="./lyra.js"></script>
<script>
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js').then((registration) => {
registration.addEventListener('updatefound', () => {
const newServiceWorker = registration.installing;
newServiceWorker.addEventListener('statechange', () => {
if (newServiceWorker.state == 'activated') {
location.reload();
}
});
});
});
}
let originalAudioDataList = [];
let audioContext;
let lyraModule;
const recordingDuration = 5 * 1000 * 1000; // micro secs
function getUserMedia() {
const constraints = {audio: {
sampleRate: {exact: 48000},
channelCount: {exact: 1}
}};
return navigator.mediaDevices.getUserMedia(constraints);
}
function startRecording() {
originalAudioDataList = [];
getUserMedia().then(async (inputStream) => {
// モデルをロード
if (lyraModule === undefined) {
const WASM_PATH = "./";
const MODEL_PATH = "https://lyra-wasm.shiguredo.app/2022.2.0/";
lyraModule = await Shiguredo.LyraModule.load(WASM_PATH, MODEL_PATH);
}
audioContext = new AudioContext({sampleRate: 48000});
// 入力音声を録音
const abortController = new AbortController();
const signal = abortController.signal;
let duration = 0;
const inputAudioGenerator = new MediaStreamTrackGenerator({ kind: "audio" });
const inputAudioProcessor = new MediaStreamTrackProcessor({ track: inputStream.getAudioTracks()[0] });
inputAudioProcessor.readable
.pipeThrough(
new TransformStream({
transform: (data, controller) => {
originalAudioDataList.push(data);
duration += data.duration;
if (duration > recordingDuration) {
abortController.abort();
alert("録音完了");
}
}
}),
{ signal })
.pipeTo(inputAudioGenerator.writable)
.catch((e) => {
if (!signal.aborted) {
console.log("Input stream transform stopped:", e);
}
});
});
}
async function playProcessedAudio() {
const processedAudioDataList = [];
const bitrate = Number(document.getElementById('bitrate').value);
const enableDtx = document.getElementById('dtx').checked;
const encoder = await lyraModule.createEncoder({sampleRate: 48000, bitrate, enableDtx});
const decoder = await lyraModule.createDecoder({sampleRate: 48000});
const halfFrameSize = encoder.frameSize / 2;
let totalEncodedSize = 0;
let startTime = performance.now();
for (let i = 0; i < originalAudioDataList.length; i += 2) {
const audioData = new Float32Array(encoder.frameSize);
const audioDataInt16 = new Int16Array(encoder.frameSize);
if (originalAudioDataList.length == i + 1) {
break;
}
// NOTE: ここは originalAudioDataList の各要素の長さが 10ms であることを仮定している
// Android / iOS では別の長さ(e.g., Android なら 40ms )になるようなので要修正
originalAudioDataList[i].copyTo(audioData.subarray(0, halfFrameSize), { planeIndex: 0 });
originalAudioDataList[i + 1].copyTo(audioData.subarray(halfFrameSize), { planeIndex: 0 });
for (let [i, v] of audioData.entries()) {
audioDataInt16[i] = v * 0x7FFF;
}
const encoded = await encoder.encode(audioDataInt16);
if (encoded !== undefined) {
totalEncodedSize += encoded.length;
}
const processedDataInt16 = await decoder.decode(encoded);
const processedData = new Float32Array(processedDataInt16.length);
for (let [i, v] of processedDataInt16.entries()) {
processedData[i] = v / 0x7FFF;
}
processedAudioDataList.push(
new AudioData({
format: originalAudioDataList[i].format,
sampleRate: 48000,
numberOfFrames: encoder.frameSize,
numberOfChannels: 1,
timestamp: originalAudioDataList[i].timestamp,
data: processedData,
})
);
}
console.log(`Encoded Size: ${totalEncodedSize} bytes`);
console.log(`Elapsed Time: ${performance.now() - startTime} ms`);
encoder.destroy();
decoder.destroy();
playAudio(processedAudioDataList);
}
function playOriginalAudio() {
// NOTE: Android / iOS ではなぜか音声が再生されない(要調査)
playAudio(originalAudioDataList);
}
function playAudio(audioDataList) {
const frames = audioDataList[0].numberOfFrames;
const buffer = audioContext.createBuffer(
1,
frames * audioDataList.length,
audioDataList[0].sampleRate
);
const tmpBuffer = new Float32Array(frames);
for (const [i, audioData] of audioDataList.entries()) {
audioData.copyTo(tmpBuffer, { planeIndex: 0 });
buffer.copyToChannel(tmpBuffer, 0, i * frames);
}
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start();
}
</script>
</body>
</html>