nemabruh404 commited on
Commit
b4e1f3c
·
verified ·
1 Parent(s): c5dcb55

Create recorder.js

Browse files
Files changed (1) hide show
  1. ui/recorder.js +193 -0
ui/recorder.js ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ function init() {
3
+ /* ---- refs ---- */
4
+ const micBtn = document.getElementById('lumi-mic-btn');
5
+ if (!micBtn) return false; // DOM chưa sẵn sàng, thử lại sau
6
+
7
+ const recLabel = document.getElementById('lumi-rec-label');
8
+ const canvas = document.getElementById('lumi-canvas');
9
+ const ctx2d = canvas.getContext('2d');
10
+ const procBar = document.getElementById('lumi-proc-bar');
11
+ const playWrap = document.getElementById('lumi-audio-playback');
12
+ const audioEl = document.getElementById('lumi-audio-el');
13
+
14
+ /* ---- state ---- */
15
+ let isRecording = false;
16
+ let mediaRecorder = null;
17
+ let audioChunks = [];
18
+ let audioCtx = null;
19
+ let analyser = null;
20
+ let micStream = null;
21
+ let animId = null;
22
+
23
+ /* ---- mic button ---- */
24
+ micBtn.addEventListener('click', () => {
25
+ isRecording ? stopRecording() : startRecording();
26
+ });
27
+
28
+ /* ========================
29
+ RECORDING
30
+ ======================== */
31
+ async function startRecording() {
32
+ try {
33
+ micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
34
+ } catch (e) {
35
+ recLabel.textContent = '❌ Không có quyền truy cập micro!';
36
+ return;
37
+ }
38
+
39
+ // Web Audio → visualizer
40
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
41
+ analyser = audioCtx.createAnalyser();
42
+ analyser.fftSize = 128;
43
+ audioCtx.createMediaStreamSource(micStream).connect(analyser);
44
+
45
+ // MediaRecorder → lưu blob
46
+ audioChunks = [];
47
+ const mime = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg']
48
+ .find(t => MediaRecorder.isTypeSupported(t)) || '';
49
+ mediaRecorder = new MediaRecorder(micStream, mime ? { mimeType: mime } : {});
50
+ mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
51
+ mediaRecorder.onstop = onRecordingDone;
52
+ mediaRecorder.start(100);
53
+
54
+ isRecording = true;
55
+ micBtn.classList.add('recording');
56
+ micBtn.textContent = '⏹️';
57
+ recLabel.textContent = '🔴 Đang ghi... Nhấn để dừng';
58
+ recLabel.classList.add('recording');
59
+ playWrap.style.display = 'none';
60
+ procBar.style.display = 'none';
61
+ drawWave();
62
+ }
63
+
64
+ function stopRecording() {
65
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
66
+ if (micStream) micStream.getTracks().forEach(t => t.stop());
67
+ if (audioCtx) audioCtx.close();
68
+ if (animId) cancelAnimationFrame(animId);
69
+ micStream = audioCtx = analyser = null;
70
+
71
+ isRecording = false;
72
+ micBtn.classList.remove('recording');
73
+ micBtn.textContent = '🎙️';
74
+ recLabel.textContent = 'Nhấn để ghi âm lại';
75
+ recLabel.classList.remove('recording');
76
+ procBar.style.display = 'block';
77
+ drawFlatLine();
78
+ }
79
+
80
+ function onRecordingDone() {
81
+ const mime = audioChunks[0]?.type || 'audio/webm';
82
+ const blob = new Blob(audioChunks, { type: mime });
83
+
84
+ // Playback
85
+ const url = URL.createObjectURL(blob);
86
+ audioEl.src = url;
87
+ audioEl.oncanplay = () => {
88
+ procBar.style.display = 'none';
89
+ playWrap.style.display = 'flex';
90
+ };
91
+
92
+ injectFileToGradio(blob, mime);
93
+ }
94
+
95
+ /* ========================
96
+ INJECT VÀO GRADIO
97
+ ======================== */
98
+ function injectFileToGradio(blob, mime) {
99
+ const ext = mime.includes('ogg') ? 'ogg' : 'webm';
100
+ const file = new File([blob], 'recording.' + ext, { type: mime });
101
+
102
+ function attempt(tries) {
103
+ const inp = document.querySelector('.hidden-audio-wrap input[type="file"]');
104
+ if (inp) {
105
+ const dt = new DataTransfer();
106
+ dt.items.add(file);
107
+ inp.files = dt.files;
108
+ inp.dispatchEvent(new Event('change', { bubbles: true }));
109
+ } else if (tries > 0) {
110
+ setTimeout(() => attempt(tries - 1), 300);
111
+ }
112
+ }
113
+ attempt(10);
114
+ }
115
+
116
+ /* ========================
117
+ WAVEFORM VISUALIZER
118
+ ======================== */
119
+ function drawWave() {
120
+ if (!analyser) return;
121
+ animId = requestAnimationFrame(drawWave);
122
+
123
+ const buf = analyser.frequencyBinCount;
124
+ const data = new Uint8Array(buf);
125
+ analyser.getByteTimeDomainData(data);
126
+
127
+ const W = canvas.width, H = canvas.height;
128
+ ctx2d.clearRect(0, 0, W, H);
129
+
130
+ const rms = Math.sqrt(data.reduce((s, v) => s + (v - 128) ** 2, 0) / buf);
131
+ const intensity = Math.min(rms / 28, 1);
132
+
133
+ // Glow rings
134
+ if (intensity > 0.04) {
135
+ const cx = W / 2, cy = H / 2;
136
+ [0, 1, 2].forEach(r => {
137
+ ctx2d.beginPath();
138
+ ctx2d.arc(cx, cy, 16 + r * 13 + intensity * 18, 0, Math.PI * 2);
139
+ ctx2d.strokeStyle = `rgba(167,139,250,${(0.20 - r * 0.05) * intensity})`;
140
+ ctx2d.lineWidth = 9 - r * 2.5;
141
+ ctx2d.stroke();
142
+ });
143
+ }
144
+
145
+ // Waveform
146
+ const g = ctx2d.createLinearGradient(0, 0, W, 0);
147
+ g.addColorStop(0, 'rgba(124,111,247,0)');
148
+ g.addColorStop(0.2, `rgba(124,111,247,${0.45 + intensity * 0.55})`);
149
+ g.addColorStop(0.5, `rgba(167,139,250,${0.75 + intensity * 0.25})`);
150
+ g.addColorStop(0.8, `rgba(56,189,248,${0.45 + intensity * 0.55})`);
151
+ g.addColorStop(1, 'rgba(56,189,248,0)');
152
+
153
+ ctx2d.beginPath();
154
+ ctx2d.lineWidth = 2.5;
155
+ ctx2d.strokeStyle = g;
156
+ ctx2d.shadowColor = '#7c6ff7';
157
+ ctx2d.shadowBlur = 10 * intensity;
158
+
159
+ const sw = W / buf;
160
+ for (let i = 0; i < buf; i++) {
161
+ const y = ((data[i] / 128) * H) / 2;
162
+ i === 0 ? ctx2d.moveTo(0, y) : ctx2d.lineTo(i * sw, y);
163
+ }
164
+ ctx2d.stroke();
165
+ ctx2d.shadowBlur = 0;
166
+ }
167
+
168
+ function drawFlatLine() {
169
+ ctx2d.clearRect(0, 0, canvas.width, canvas.height);
170
+ ctx2d.beginPath();
171
+ ctx2d.moveTo(0, canvas.height / 2);
172
+ ctx2d.lineTo(canvas.width, canvas.height / 2);
173
+ ctx2d.strokeStyle = 'rgba(124,111,247,0.18)';
174
+ ctx2d.lineWidth = 1.5;
175
+ ctx2d.stroke();
176
+ }
177
+
178
+ drawFlatLine(); // init
179
+ return true;
180
+ }
181
+
182
+ /* ========================
183
+ ĐỢI DOM SẴN SÀNG
184
+ Thử init ngay, nếu chưa được thì
185
+ MutationObserver sẽ thử lại mỗi khi DOM thay đổi
186
+ ======================== */
187
+ if (!init()) {
188
+ const observer = new MutationObserver(() => {
189
+ if (init()) observer.disconnect();
190
+ });
191
+ observer.observe(document.body, { childList: true, subtree: true });
192
+ }
193
+ })();