app.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. const socket = io();
  2. const state = {
  3. mode: "beamforming",
  4. gain_db: 0,
  5. agc: true,
  6. attack_ms: 6,
  7. release_ms: 280,
  8. noise_suppression: true,
  9. speech_gate: false,
  10. hum_filter: true,
  11. limiter: true,
  12. beam_clarity: true,
  13. hifi_mode: false,
  14. hifi_mic: "mic1",
  15. angle: 0,
  16. auto_beam: true,
  17. auto_angle: 0,
  18. speech_detected: false,
  19. monitor_on: false,
  20. monitor_source: "beam",
  21. sample_rate: 16000,
  22. recording: false,
  23. recDuration: 0,
  24. record_duration_sec: 0,
  25. buffers: {
  26. mic1: [],
  27. mic2: [],
  28. beam: [],
  29. mono_mix: [],
  30. },
  31. };
  32. const MAX_POINTS = 3500;
  33. const els = {
  34. status: document.getElementById("serverStatus"),
  35. audioStatus: document.getElementById("audioStatus"),
  36. waveMic1: document.getElementById("waveMic1"),
  37. waveMic2: document.getElementById("waveMic2"),
  38. waveBeam: document.getElementById("waveBeam"),
  39. beamBlock: document.getElementById("beamBlock"),
  40. vuMic1: document.getElementById("vuMic1"),
  41. vuMic2: document.getElementById("vuMic2"),
  42. vuBeam: document.getElementById("vuBeam"),
  43. gainDb: document.getElementById("gainDb"),
  44. gainValue: document.getElementById("gainValue"),
  45. agcEnabled: document.getElementById("agcEnabled"),
  46. agcControls: document.getElementById("agcControls"),
  47. attackMs: document.getElementById("attackMs"),
  48. attackValue: document.getElementById("attackValue"),
  49. releaseMs: document.getElementById("releaseMs"),
  50. releaseValue: document.getElementById("releaseValue"),
  51. noiseSuppression: document.getElementById("noiseSuppression"),
  52. speechGate: document.getElementById("speechGate"),
  53. humFilter: document.getElementById("humFilter"),
  54. limiterEnabled: document.getElementById("limiterEnabled"),
  55. beamClarity: document.getElementById("beamClarity"),
  56. hifiMode: document.getElementById("hifiMode"),
  57. hifiMic: document.getElementById("hifiMic"),
  58. beamControls: document.getElementById("beamControls"),
  59. beamAuto: document.getElementById("beamAuto"),
  60. manualBeamRow: document.getElementById("manualBeamRow"),
  61. beamAngle: document.getElementById("beamAngle"),
  62. angleValue: document.getElementById("angleValue"),
  63. autoAngleValue: document.getElementById("autoAngleValue"),
  64. speechState: document.getElementById("speechState"),
  65. monitorOn: document.getElementById("monitorOn"),
  66. monitorSource: document.getElementById("monitorSource"),
  67. sampleRate: document.getElementById("sampleRate"),
  68. recordBtn: document.getElementById("recordBtn"),
  69. recordSource: document.getElementById("recordSource"),
  70. recordDurationSec: document.getElementById("recordDurationSec"),
  71. recordTimer: document.getElementById("recordTimer"),
  72. recordingsBody: document.getElementById("recordingsBody"),
  73. };
  74. const monitorAudio = {
  75. context: null,
  76. nextTime: 0,
  77. };
  78. function clamp(value, min, max) {
  79. return Math.min(Math.max(value, min), max);
  80. }
  81. function formatDuration(totalSeconds) {
  82. const sec = Math.max(0, Math.floor(totalSeconds));
  83. const h = String(Math.floor(sec / 3600)).padStart(2, "0");
  84. const m = String(Math.floor((sec % 3600) / 60)).padStart(2, "0");
  85. const s = String(sec % 60).padStart(2, "0");
  86. return `${h}:${m}:${s}`;
  87. }
  88. function formatBytes(bytes) {
  89. if (bytes < 1024) return `${bytes} B`;
  90. if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  91. return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
  92. }
  93. function pushWaveData(key, samples) {
  94. if (!Array.isArray(samples) || samples.length === 0) return;
  95. const target = state.buffers[key];
  96. target.push(...samples);
  97. if (target.length > MAX_POINTS) {
  98. target.splice(0, target.length - MAX_POINTS);
  99. }
  100. }
  101. function drawWave(canvas, samples, color) {
  102. const ctx = canvas.getContext("2d");
  103. const width = canvas.width;
  104. const height = canvas.height;
  105. ctx.clearRect(0, 0, width, height);
  106. ctx.strokeStyle = "rgba(140, 190, 200, 0.35)";
  107. ctx.lineWidth = 1;
  108. ctx.beginPath();
  109. ctx.moveTo(0, height / 2);
  110. ctx.lineTo(width, height / 2);
  111. ctx.stroke();
  112. if (!samples.length) return;
  113. const step = samples.length / width;
  114. ctx.strokeStyle = color;
  115. ctx.lineWidth = 1.7;
  116. ctx.beginPath();
  117. for (let x = 0; x < width; x += 1) {
  118. const idx = Math.floor(x * step);
  119. const sample = clamp(samples[idx] ?? 0, -1, 1);
  120. const y = (1 - (sample + 1) / 2) * height;
  121. if (x === 0) {
  122. ctx.moveTo(x, y);
  123. } else {
  124. ctx.lineTo(x, y);
  125. }
  126. }
  127. ctx.stroke();
  128. }
  129. function setVu(el, rms) {
  130. const db = 20 * Math.log10(Math.max(rms, 1e-6));
  131. const norm = clamp((db + 60) / 60, 0, 1);
  132. el.style.width = `${Math.round(norm * 100)}%`;
  133. }
  134. function ensureMonitorContext() {
  135. if (!monitorAudio.context) {
  136. const Ctx = window.AudioContext || window.webkitAudioContext;
  137. if (!Ctx) return null;
  138. monitorAudio.context = new Ctx({ sampleRate: 48000 });
  139. }
  140. return monitorAudio.context;
  141. }
  142. function stopMonitorPlayback() {
  143. const ctx = monitorAudio.context;
  144. if (!ctx) return;
  145. monitorAudio.nextTime = ctx.currentTime;
  146. }
  147. function decodePcm16Base64(base64Chunk) {
  148. if (!base64Chunk) return null;
  149. const binary = atob(base64Chunk);
  150. const len = binary.length;
  151. if (len < 2) return null;
  152. const samples = new Float32Array(Math.floor(len / 2));
  153. for (let i = 0, s = 0; i + 1 < len; i += 2, s += 1) {
  154. let value = (binary.charCodeAt(i) | (binary.charCodeAt(i + 1) << 8));
  155. if (value & 0x8000) value -= 0x10000;
  156. samples[s] = value / 32768.0;
  157. }
  158. return samples;
  159. }
  160. function playMonitorChunk(base64Chunk, sampleRate) {
  161. const chunk = decodePcm16Base64(base64Chunk);
  162. if (!chunk || chunk.length === 0) return;
  163. const ctx = ensureMonitorContext();
  164. if (!ctx) return;
  165. if (ctx.state === "suspended") {
  166. void ctx.resume();
  167. }
  168. const now = ctx.currentTime;
  169. if (monitorAudio.nextTime < now + 0.02) {
  170. monitorAudio.nextTime = now + 0.02;
  171. } else if (monitorAudio.nextTime > now + 0.6) {
  172. monitorAudio.nextTime = now + 0.05;
  173. }
  174. const buffer = ctx.createBuffer(1, chunk.length, sampleRate);
  175. buffer.copyToChannel(chunk, 0);
  176. const src = ctx.createBufferSource();
  177. src.buffer = buffer;
  178. src.connect(ctx.destination);
  179. src.start(monitorAudio.nextTime);
  180. monitorAudio.nextTime += buffer.duration;
  181. }
  182. function sendSettings() {
  183. const payload = {
  184. type: "settings",
  185. mode: state.mode,
  186. gain_db: state.gain_db,
  187. agc: state.agc,
  188. attack_ms: state.attack_ms,
  189. release_ms: state.release_ms,
  190. noise_suppression: state.noise_suppression,
  191. speech_gate: state.speech_gate,
  192. hum_filter: state.hum_filter,
  193. limiter: state.limiter,
  194. beam_clarity: state.beam_clarity,
  195. hifi_mode: state.hifi_mode,
  196. hifi_mic: state.hifi_mic,
  197. angle: state.angle,
  198. auto_beam: state.auto_beam,
  199. monitor_on: state.monitor_on,
  200. monitor_source: state.monitor_source,
  201. sample_rate: state.sample_rate,
  202. };
  203. socket.emit("client_message", payload);
  204. }
  205. function refreshControlVisibility() {
  206. const hifi = state.hifi_mode;
  207. const beamVisible = state.mode === "beamforming" && !hifi;
  208. els.beamControls.classList.toggle("hidden", !beamVisible);
  209. els.beamBlock.classList.toggle("hidden", !beamVisible);
  210. if (els.manualBeamRow) {
  211. els.manualBeamRow.classList.toggle("hidden", !beamVisible || state.auto_beam);
  212. }
  213. if (els.monitorSource) {
  214. els.monitorSource.disabled = hifi || !state.monitor_on;
  215. }
  216. els.agcControls.classList.toggle("hidden", hifi || !state.agc);
  217. document.querySelectorAll("input[name='mode']").forEach((radio) => {
  218. radio.disabled = hifi;
  219. });
  220. if (els.gainDb) els.gainDb.disabled = hifi;
  221. if (els.agcEnabled) els.agcEnabled.disabled = hifi;
  222. if (els.attackMs) els.attackMs.disabled = hifi || !state.agc;
  223. if (els.releaseMs) els.releaseMs.disabled = hifi || !state.agc;
  224. if (els.noiseSuppression) els.noiseSuppression.disabled = hifi;
  225. if (els.speechGate) els.speechGate.disabled = hifi;
  226. if (els.humFilter) els.humFilter.disabled = hifi;
  227. if (els.limiterEnabled) els.limiterEnabled.disabled = hifi;
  228. if (els.beamClarity) els.beamClarity.disabled = hifi;
  229. if (els.beamAuto) els.beamAuto.disabled = hifi || !beamVisible;
  230. if (els.beamAngle) els.beamAngle.disabled = hifi || !beamVisible || state.auto_beam;
  231. if (els.sampleRate) els.sampleRate.disabled = hifi;
  232. if (els.monitorOn) els.monitorOn.disabled = hifi;
  233. if (els.hifiMic) els.hifiMic.disabled = !hifi;
  234. if (els.recordSource) {
  235. if (hifi) {
  236. els.recordSource.value = "hifi_raw";
  237. els.recordSource.disabled = true;
  238. } else {
  239. els.recordSource.disabled = false;
  240. if (els.recordSource.value === "hifi_raw") {
  241. els.recordSource.value = "beam";
  242. }
  243. }
  244. }
  245. }
  246. function syncUiFromState() {
  247. document.querySelectorAll("input[name='mode']").forEach((radio) => {
  248. radio.checked = radio.value === state.mode;
  249. });
  250. els.gainDb.value = String(state.gain_db);
  251. els.gainValue.textContent = String(state.gain_db);
  252. els.agcEnabled.checked = state.agc;
  253. els.attackMs.value = String(state.attack_ms);
  254. els.attackValue.textContent = String(state.attack_ms);
  255. els.releaseMs.value = String(state.release_ms);
  256. els.releaseValue.textContent = String(state.release_ms);
  257. if (els.noiseSuppression) {
  258. els.noiseSuppression.checked = state.noise_suppression;
  259. }
  260. if (els.speechGate) {
  261. els.speechGate.checked = state.speech_gate;
  262. }
  263. if (els.humFilter) {
  264. els.humFilter.checked = state.hum_filter;
  265. }
  266. if (els.limiterEnabled) {
  267. els.limiterEnabled.checked = state.limiter;
  268. }
  269. if (els.beamClarity) {
  270. els.beamClarity.checked = state.beam_clarity;
  271. }
  272. if (els.hifiMode) {
  273. els.hifiMode.checked = state.hifi_mode;
  274. }
  275. if (els.hifiMic) {
  276. els.hifiMic.value = state.hifi_mic;
  277. }
  278. els.beamAngle.value = String(state.angle);
  279. els.angleValue.textContent = String(state.angle);
  280. if (els.beamAuto) {
  281. els.beamAuto.checked = state.auto_beam;
  282. }
  283. if (els.autoAngleValue) {
  284. els.autoAngleValue.textContent = Number(state.auto_angle).toFixed(1);
  285. }
  286. if (els.speechState) {
  287. els.speechState.textContent = state.speech_detected ? "mowa" : "cisza";
  288. }
  289. if (els.monitorOn) {
  290. els.monitorOn.checked = state.monitor_on;
  291. }
  292. if (els.monitorSource) {
  293. els.monitorSource.value = state.monitor_source;
  294. }
  295. els.sampleRate.value = String(state.sample_rate);
  296. els.recordBtn.classList.toggle("recording", state.recording);
  297. els.recordBtn.textContent = state.recording ? "STOP" : "RECORD";
  298. refreshControlVisibility();
  299. }
  300. async function loadStatus() {
  301. const response = await fetch("/api/status");
  302. const data = await response.json();
  303. if (data.settings) {
  304. state.mode = data.settings.mode;
  305. state.gain_db = data.settings.gain_db;
  306. state.agc = data.settings.agc;
  307. state.attack_ms = data.settings.attack_ms;
  308. state.release_ms = data.settings.release_ms;
  309. state.noise_suppression = Boolean(data.settings.noise_suppression);
  310. state.speech_gate = Boolean(data.settings.speech_gate);
  311. state.hum_filter = Boolean(data.settings.hum_filter);
  312. state.limiter = Boolean(data.settings.limiter);
  313. state.beam_clarity = Boolean(data.settings.beam_clarity);
  314. state.hifi_mode = Boolean(data.settings.hifi_mode);
  315. state.hifi_mic = data.settings.hifi_mic || "mic1";
  316. state.angle = data.settings.angle;
  317. state.auto_beam = Boolean(data.settings.auto_beam);
  318. state.monitor_on = Boolean(data.settings.monitor_on);
  319. state.monitor_source = data.settings.monitor_source || "beam";
  320. state.sample_rate = data.settings.sample_rate;
  321. }
  322. state.auto_angle = Number(data.auto_beam_angle_deg ?? state.angle ?? 0);
  323. state.speech_detected = false;
  324. state.recording = Boolean(data.recording);
  325. els.audioStatus.textContent = data.audio_error
  326. ? `Audio: blad (${data.audio_error})`
  327. : data.audio_running
  328. ? "Audio: aktywne"
  329. : "Audio: zatrzymane";
  330. syncUiFromState();
  331. }
  332. async function loadRecordings() {
  333. const response = await fetch("/api/recordings");
  334. const recordings = await response.json();
  335. els.recordingsBody.innerHTML = "";
  336. recordings.forEach((rec) => {
  337. const tr = document.createElement("tr");
  338. const nameTd = document.createElement("td");
  339. nameTd.textContent = rec.filename;
  340. const dateTd = document.createElement("td");
  341. dateTd.textContent = rec.date;
  342. const durTd = document.createElement("td");
  343. durTd.textContent = formatDuration(rec.duration_sec || 0);
  344. const sizeTd = document.createElement("td");
  345. sizeTd.textContent = formatBytes(rec.size_bytes || 0);
  346. const actionsTd = document.createElement("td");
  347. actionsTd.className = "actions";
  348. const playBtn = document.createElement("button");
  349. playBtn.className = "btn-small";
  350. playBtn.textContent = "Play";
  351. playBtn.addEventListener("click", () => {
  352. const audio = new Audio(`/api/recordings/${encodeURIComponent(rec.filename)}`);
  353. void audio.play();
  354. });
  355. const dlBtn = document.createElement("a");
  356. dlBtn.className = "btn-small";
  357. dlBtn.textContent = "Download";
  358. dlBtn.href = `/api/recordings/${encodeURIComponent(rec.filename)}`;
  359. const delBtn = document.createElement("button");
  360. delBtn.className = "btn-small danger";
  361. delBtn.textContent = "Delete";
  362. delBtn.addEventListener("click", async () => {
  363. await fetch(`/api/recordings/${encodeURIComponent(rec.filename)}`, {
  364. method: "DELETE",
  365. });
  366. await loadRecordings();
  367. });
  368. actionsTd.append(playBtn, dlBtn, delBtn);
  369. tr.append(nameTd, dateTd, durTd, sizeTd, actionsTd);
  370. els.recordingsBody.appendChild(tr);
  371. });
  372. }
  373. function bindControls() {
  374. document.querySelectorAll("input[name='mode']").forEach((radio) => {
  375. radio.addEventListener("change", () => {
  376. state.mode = radio.value;
  377. refreshControlVisibility();
  378. sendSettings();
  379. });
  380. });
  381. els.gainDb.addEventListener("input", () => {
  382. state.gain_db = Number(els.gainDb.value);
  383. els.gainValue.textContent = String(state.gain_db);
  384. });
  385. els.gainDb.addEventListener("change", sendSettings);
  386. els.agcEnabled.addEventListener("change", () => {
  387. state.agc = els.agcEnabled.checked;
  388. refreshControlVisibility();
  389. sendSettings();
  390. });
  391. els.attackMs.addEventListener("input", () => {
  392. state.attack_ms = Number(els.attackMs.value);
  393. els.attackValue.textContent = String(state.attack_ms);
  394. });
  395. els.attackMs.addEventListener("change", sendSettings);
  396. els.releaseMs.addEventListener("input", () => {
  397. state.release_ms = Number(els.releaseMs.value);
  398. els.releaseValue.textContent = String(state.release_ms);
  399. });
  400. els.releaseMs.addEventListener("change", sendSettings);
  401. if (els.noiseSuppression) {
  402. els.noiseSuppression.addEventListener("change", () => {
  403. state.noise_suppression = els.noiseSuppression.checked;
  404. sendSettings();
  405. });
  406. }
  407. if (els.speechGate) {
  408. els.speechGate.addEventListener("change", () => {
  409. state.speech_gate = els.speechGate.checked;
  410. sendSettings();
  411. });
  412. }
  413. if (els.humFilter) {
  414. els.humFilter.addEventListener("change", () => {
  415. state.hum_filter = els.humFilter.checked;
  416. sendSettings();
  417. });
  418. }
  419. if (els.limiterEnabled) {
  420. els.limiterEnabled.addEventListener("change", () => {
  421. state.limiter = els.limiterEnabled.checked;
  422. sendSettings();
  423. });
  424. }
  425. if (els.beamClarity) {
  426. els.beamClarity.addEventListener("change", () => {
  427. state.beam_clarity = els.beamClarity.checked;
  428. sendSettings();
  429. });
  430. }
  431. if (els.hifiMode) {
  432. els.hifiMode.addEventListener("change", () => {
  433. state.hifi_mode = els.hifiMode.checked;
  434. if (state.hifi_mode) {
  435. state.monitor_on = false;
  436. }
  437. refreshControlVisibility();
  438. sendSettings();
  439. });
  440. }
  441. if (els.hifiMic) {
  442. els.hifiMic.addEventListener("change", () => {
  443. state.hifi_mic = els.hifiMic.value;
  444. sendSettings();
  445. });
  446. }
  447. els.beamAngle.addEventListener("input", () => {
  448. state.angle = Number(els.beamAngle.value);
  449. els.angleValue.textContent = String(state.angle);
  450. });
  451. els.beamAngle.addEventListener("change", sendSettings);
  452. if (els.beamAuto) {
  453. els.beamAuto.addEventListener("change", () => {
  454. state.auto_beam = els.beamAuto.checked;
  455. refreshControlVisibility();
  456. sendSettings();
  457. });
  458. }
  459. els.sampleRate.addEventListener("change", () => {
  460. state.sample_rate = Number(els.sampleRate.value);
  461. sendSettings();
  462. });
  463. els.recordBtn.addEventListener("click", () => {
  464. if (!state.recording) {
  465. const duration = Number(els.recordDurationSec?.value || 0);
  466. state.record_duration_sec = Number.isFinite(duration) ? Math.max(0, duration) : 0;
  467. socket.emit("client_message", {
  468. type: "record_start",
  469. source: els.recordSource.value,
  470. duration_sec: state.record_duration_sec,
  471. });
  472. return;
  473. }
  474. socket.emit("client_message", { type: "record_stop" });
  475. });
  476. if (els.recordDurationSec) {
  477. els.recordDurationSec.addEventListener("change", () => {
  478. const duration = Number(els.recordDurationSec.value || 0);
  479. state.record_duration_sec = Number.isFinite(duration)
  480. ? clamp(duration, 0, 3600)
  481. : 0;
  482. els.recordDurationSec.value = String(Math.round(state.record_duration_sec));
  483. });
  484. }
  485. if (els.monitorOn) {
  486. els.monitorOn.addEventListener("change", () => {
  487. state.monitor_on = els.monitorOn.checked;
  488. if (state.monitor_on) {
  489. const ctx = ensureMonitorContext();
  490. if (ctx && ctx.state === "suspended") {
  491. void ctx.resume();
  492. }
  493. } else {
  494. stopMonitorPlayback();
  495. }
  496. refreshControlVisibility();
  497. sendSettings();
  498. });
  499. }
  500. if (els.monitorSource) {
  501. els.monitorSource.addEventListener("change", () => {
  502. state.monitor_source = els.monitorSource.value;
  503. sendSettings();
  504. });
  505. }
  506. }
  507. function animate() {
  508. drawWave(els.waveMic1, state.buffers.mic1, "#32b3ff");
  509. drawWave(els.waveMic2, state.buffers.mic2, "#50d27a");
  510. if (state.mode === "beamforming") {
  511. drawWave(els.waveBeam, state.buffers.beam, "#ff6d5a");
  512. }
  513. els.recordTimer.textContent = formatDuration(state.recDuration);
  514. requestAnimationFrame(animate);
  515. }
  516. function bindSocket() {
  517. socket.on("connect", () => {
  518. els.status.textContent = "WebSocket: polaczono";
  519. });
  520. socket.on("disconnect", () => {
  521. els.status.textContent = "WebSocket: rozlaczono";
  522. });
  523. socket.on("status", (payload) => {
  524. if (payload?.settings) {
  525. state.mode = payload.settings.mode;
  526. state.gain_db = payload.settings.gain_db;
  527. state.agc = payload.settings.agc;
  528. state.attack_ms = payload.settings.attack_ms;
  529. state.release_ms = payload.settings.release_ms;
  530. state.noise_suppression = Boolean(payload.settings.noise_suppression);
  531. state.speech_gate = Boolean(payload.settings.speech_gate);
  532. state.hum_filter = Boolean(payload.settings.hum_filter);
  533. state.limiter = Boolean(payload.settings.limiter);
  534. state.beam_clarity = Boolean(payload.settings.beam_clarity);
  535. state.hifi_mode = Boolean(payload.settings.hifi_mode);
  536. state.hifi_mic = payload.settings.hifi_mic || "mic1";
  537. state.angle = payload.settings.angle;
  538. state.auto_beam = Boolean(payload.settings.auto_beam);
  539. state.monitor_on = Boolean(payload.settings.monitor_on);
  540. state.monitor_source = payload.settings.monitor_source || "beam";
  541. state.sample_rate = payload.settings.sample_rate;
  542. syncUiFromState();
  543. }
  544. });
  545. socket.on("audio_data", (payload) => {
  546. const wasRecording = state.recording;
  547. pushWaveData("mic1", payload.mic1 || []);
  548. pushWaveData("mic2", payload.mic2 || []);
  549. pushWaveData("beam", payload.beam || []);
  550. pushWaveData("mono_mix", payload.mono_mix || []);
  551. setVu(els.vuMic1, payload.rms_mic1 || 0);
  552. setVu(els.vuMic2, payload.rms_mic2 || 0);
  553. setVu(els.vuBeam, payload.rms_beam || payload.rms_mono_mix || 0);
  554. state.recording = Boolean(payload.recording);
  555. state.recDuration = Number(payload.rec_duration || 0);
  556. const gateOpen = Boolean(payload.speech_gate_open ?? payload.speech_detected);
  557. state.speech_detected = gateOpen;
  558. state.hifi_mode = Boolean(payload.hifi_mode ?? state.hifi_mode);
  559. state.auto_angle = Number(payload.beam_angle_deg ?? state.auto_angle);
  560. state.monitor_on = Boolean(payload.monitor_on ?? state.monitor_on);
  561. state.monitor_source = payload.monitor_source || state.monitor_source;
  562. if (state.auto_beam) {
  563. state.angle = state.auto_angle;
  564. els.angleValue.textContent = Number(state.angle).toFixed(1);
  565. }
  566. if (els.autoAngleValue) {
  567. els.autoAngleValue.textContent = Number(state.auto_angle).toFixed(1);
  568. }
  569. if (els.speechState) {
  570. els.speechState.textContent = state.hifi_mode
  571. ? "hifi"
  572. : state.speech_detected
  573. ? "mowa/gate"
  574. : "cisza";
  575. }
  576. els.recordBtn.classList.toggle("recording", state.recording);
  577. els.recordBtn.textContent = state.recording ? "STOP" : "RECORD";
  578. if (state.monitor_on && payload.monitor_chunk_b64) {
  579. const sr = Number(payload.monitor_sr || state.sample_rate || 16000);
  580. playMonitorChunk(payload.monitor_chunk_b64, sr);
  581. }
  582. if (wasRecording && !state.recording) {
  583. state.recDuration = 0;
  584. void loadRecordings();
  585. }
  586. });
  587. socket.on("recordings_updated", async () => {
  588. await loadRecordings();
  589. });
  590. socket.on("server_ack", (payload) => {
  591. if (payload?.type === "record_started") {
  592. state.recording = true;
  593. els.recordBtn.classList.add("recording");
  594. els.recordBtn.textContent = "STOP";
  595. void loadRecordings();
  596. }
  597. if (payload?.type === "record_stopped") {
  598. state.recording = false;
  599. els.recordBtn.classList.remove("recording");
  600. els.recordBtn.textContent = "RECORD";
  601. state.recDuration = 0;
  602. void loadRecordings();
  603. }
  604. if (payload?.type === "settings_applied" && payload.settings) {
  605. state.mode = payload.settings.mode;
  606. state.gain_db = payload.settings.gain_db;
  607. state.agc = payload.settings.agc;
  608. state.attack_ms = payload.settings.attack_ms;
  609. state.release_ms = payload.settings.release_ms;
  610. state.noise_suppression = Boolean(payload.settings.noise_suppression);
  611. state.speech_gate = Boolean(payload.settings.speech_gate);
  612. state.hum_filter = Boolean(payload.settings.hum_filter);
  613. state.limiter = Boolean(payload.settings.limiter);
  614. state.beam_clarity = Boolean(payload.settings.beam_clarity);
  615. state.hifi_mode = Boolean(payload.settings.hifi_mode);
  616. state.hifi_mic = payload.settings.hifi_mic || "mic1";
  617. state.angle = payload.settings.angle;
  618. state.auto_beam = Boolean(payload.settings.auto_beam);
  619. state.monitor_on = Boolean(payload.settings.monitor_on);
  620. state.monitor_source = payload.settings.monitor_source || "beam";
  621. state.sample_rate = payload.settings.sample_rate;
  622. refreshControlVisibility();
  623. syncUiFromState();
  624. }
  625. });
  626. socket.on("server_error", (payload) => {
  627. if (payload?.message) {
  628. els.audioStatus.textContent = `Audio: blad (${payload.message})`;
  629. }
  630. });
  631. }
  632. async function init() {
  633. bindControls();
  634. bindSocket();
  635. await loadStatus();
  636. await loadRecordings();
  637. syncUiFromState();
  638. requestAnimationFrame(animate);
  639. }
  640. void init();