|
3 | 3 | Every agent working on this repository MUST append a dated entry describing |
4 | 4 | their changes after finishing work. |
5 | 5 |
|
| 6 | +## 2026-09-03 — TTS voice listing provider-aware + Settings cleanup (Start TTS, Speech & Translation, mic-selector, Test My Microphone) |
| 7 | + |
| 8 | +- **TTS voice listing is now provider-aware.** `listVoices(development, provider)` |
| 9 | + in `src/main/services/tts/voices.ts` returns the catalog matching the active |
| 10 | + synthesizer: `TTS_PROVIDER=say` → only macOS/system voices (Azure ids are |
| 11 | + never listed because `say` cannot speak them); `TTS_PROVIDER=azure` → Azure |
| 12 | + voices always + macOS system voices in dev; `mock` → Azure only; unknown → |
| 13 | + none. New pure helper `resolveTtsProviderName()` reads `TTS_PROVIDER` from the |
| 14 | + env (default `mock`) and is unit-tested. |
| 15 | +- **`tts:list-voices` IPC threads the resolved provider** into `listVoices` and |
| 16 | + returns it in `ListVoicesResult.provider` (`packages/shared/index.ts`), |
| 17 | + so the catalog the UI shows always matches the runtime synthesizer. |
| 18 | +- **`resolveTtsVoiceId()` is provider-aware too**: with `TTS_PROVIDER=say` a |
| 19 | + persisted Azure id is rejected (falls back to the `say` provider default) |
| 20 | + because `say` cannot synthesize it; azure/mock keeps the existing |
| 21 | + `normalizeSelectedVoiceId()` behavior. This prevents listing an Azure voice |
| 22 | + under `say` or trying to speak an Azure voice with `say`. |
| 23 | +- **Removed the redundant "Start TTS" / "Stop TTS" button** in Settings → Voice. |
| 24 | + Audited: it only enabled `translationActive`, was duplicated by the |
| 25 | + SessionManager during a meeting, and overlapped with the existing **Test Voice** |
| 26 | + button (independent, self-terminating `tts:test`). `TtsPanel` now keeps voice |
| 27 | + selection + Test Voice + status/current-text display. `handleTtsStart` and the |
| 28 | + `onTtsStart`/`onTtsStop` props were removed end-to-end. |
| 29 | +- **Removed the "Speech & Translation" Settings section** and its dead manual |
| 30 | + control surface: `SttPanel.tsx` and `TranslationPanel.tsx` were deleted; the |
| 31 | + `speech` nav entry, and the `SttPanel`/`TranslationPanel` renders and their |
| 32 | + props were removed from `SettingsScreen.tsx`. Dead `handleSttStart` / |
| 33 | + `handleSttStop` and the `onSttStart`/`onSttStop`/`onTranslationStart`/ |
| 34 | + `onTranslationStop` props were removed from `App.tsx`. The shared `stt`/ |
| 35 | + `translation` hooks and the meeting pipeline are untouched (STT/translation |
| 36 | + still run via `/ Start Meeting`). |
| 37 | +- **Fixed `MicSelector` sound-check playback** ("couldn't hear recorded sound"): |
| 38 | + the `<audio>` element (`HTMLAudioElement`) cannot decode **any** audio format |
| 39 | + in this AVMedia Chromium build (conclusive: WAV, webm, and via |
| 40 | + `createMediaElementSource` all yield `networkState=3` / |
| 41 | + `MEDIA_ERR_SRC_NOT_SUPPORTED`). Rewrote the capture and playback to use the |
| 42 | + repo's own raw-PCM path: the mic stream is tapped with a second |
| 43 | + `AudioContext` + `ScriptProcessorNode` at 24 kHz, accumulating sequential, |
| 44 | + non-overlapping PCM frames into a Float32 array via event-driven capture. |
| 45 | + Playback uses `AudioContext.createBuffer(source).connect(destination).start()` |
| 46 | + (the same pattern `useAudioOutput` uses, proven audible in this build). Pause/ |
| 47 | + resume tracks elapsed frames via a poll timer and re-creates a sub-buffer from |
| 48 | + the paused offset on resume. Duplicate `handleStreamEnd` calls (LiveWaveform |
| 49 | + cleanup) are guarded by a `scriptProcessorRef !== null` check. |
| 50 | + `src/renderer/components/MicSelector.tsx`. |
| 51 | + |
| 52 | +- **Fixed `MicSelector` sound-check audio quality** (distorted/laggy playback): |
| 53 | + the previous implementation used `AnalyserNode.getFloatTimeDomainData()` polled |
| 54 | + via `setInterval(30ms)` to capture PCM. This is fundamentally broken for |
| 55 | + sequential capture: `getFloatTimeDomainData` returns a sliding window of the |
| 56 | + last `fftSize` samples, not sequential non-overlapping chunks. At 24 kHz with |
| 57 | + `fftSize=1024`, the analyser refreshes every ~42 ms, but 30 ms polls land |
| 58 | + between frame boundaries — causing overlapping reads (~300 duplicate samples per |
| 59 | + read), which produces pitch-shifted, distorted playback. CDP-verified: |
| 60 | + `getFloatTimeDomainData` at 5 ms apart returns 100% identical data (analyser |
| 61 | + hasn't refreshed), at 42 ms apart returns 0% overlap. Replaced with |
| 62 | + `ScriptProcessorNode` (`createScriptProcessor(4096, 1, 1)`) which fires |
| 63 | + `onaudioprocess` once per buffer with guaranteed sequential, non-overlapping |
| 64 | + `Float32Array` chunks. CDP timing: 3 s recording → ~2.79 s playback (delta is |
| 65 | + ScriptProcessorNode startup + React teardown latency, not sample-rate error). |
| 66 | + `src/renderer/components/MicSelector.tsx`. |
| 67 | + |
| 68 | +- **Fixed the `MicSelector` sound-check Play → Pause flow.** The `LiveWaveform` |
| 69 | + was rendered with `key={checkState}`, which remounted the component on every |
| 70 | + state change (`idle → recording → recorded → playing`). Each remount |
| 71 | + re-ran the microphone setup/teardown effect — tearing the stream down, |
| 72 | + re-fetching the mic, and churning `audio.onended`/playback state so the |
| 73 | + `playing` state wouldn't hold and the Play→Pause transition failed. |
| 74 | + Removed the `key` so a single stable `LiveWaveform` instance persists; it now |
| 75 | + clears its own history on teardown (`active=false`), and recording |
| 76 | + stop/`onStreamEnd` sealing works once. `src/renderer/components/MicSelector.tsx`. |
| 77 | + |
| 78 | +- **Replaced the microphone UI with an ElevenLabs-styled `MicSelector`** |
| 79 | + (`src/renderer/components/MicSelector.tsx`). The ElevenLabs registry is |
| 80 | + rate-limited (HTTP 429 — `npx @elevenlabs/cli@latest components add |
| 81 | + mic-selector` and the `shadcn` fallback both fail), so — consistent with how |
| 82 | + `VoicePicker` and the vendored `LiveWaveform` were handled — a local component |
| 83 | + mirrors the ElevenLabs DevLab `mic-selector` **sound-check card**: a live |
| 84 | + `LiveWaveform` (barWidth 3, scrolling, fade edges), a microphone device |
| 85 | + dropdown (popover with mic glyph + check), and a record / pause / play / trash |
| 86 | + control row plus a mute toggle. Record/play is a device test only: the |
| 87 | + `LiveWaveform` opens ONE stream and hands it to a `MediaRecorder` via |
| 88 | + `onStreamReady`, so capture is never duplicated with the meeting pipeline |
| 89 | + (which still owns real recording). Wired through the existing `useMicrophone` |
| 90 | + device state (`micDevices`/`selectedDeviceId`/`permission`/`error`/ |
| 91 | + `onSelectDevice`); the `level` prop and `AudioLevelMeter` were dropped. |
| 92 | + Old `MicrophonePanel.tsx` and its now-unconsumed `onMicStart`/`onMicStop` |
| 93 | + props were deleted. |
| 94 | +- **Removed the "Test My Microphone" diagnostics button** (dead — it re-ran |
| 95 | + `microphone.start`, duplicating the meeting path) and its `onDeviceTest` |
| 96 | + prop. Diagnostics now lists mic / TTS / audio-output / current-stage status |
| 97 | + (STT and Translation rows removed with the Speech & Translation section). |
| 98 | +- **Validation**: `npm run type-check` clean; `npm test` **88/88** (5 new: |
| 99 | + `resolveTtsProviderName`, per-provider `listVoices` for azure/mock/say/ |
| 100 | + unknown); `npm run build` OK (`dist/renderer/index.html` present); `npm run |
| 101 | + format:check` clean; `npm run lint` **0 errors** (13 pre-existing warnings |
| 102 | + unchanged, incl. hook/`no-unused-vars` in App/useSetup/tests). |
| 103 | + |
6 | 104 | ## 2026-09-03 — Voice picker restyled to ElevenLabs UI + search by gender/country/source |
7 | 105 |
|
8 | 106 | - **Rewrote `VoicePicker.tsx` to mirror the ElevenLabs UI `voice-picker` |
|
0 commit comments