Skip to content

feat(launch): add a collapsible microphone test#281

Open
meiiie wants to merge 4 commits intowebadderallorg:mainfrom
meiiie:feat/microphone-test-ux
Open

feat(launch): add a collapsible microphone test#281
meiiie wants to merge 4 commits intowebadderallorg:mainfrom
meiiie:feat/microphone-test-ux

Conversation

@meiiie
Copy link
Copy Markdown
Collaborator

@meiiie meiiie commented Apr 19, 2026

Description

Adds a collapsible microphone test to the launch microphone menu so users can quickly record and play back a short microphone sample before starting a capture.

Motivation

Users currently only get a live level meter, which makes it hard to tell whether microphone gain, device selection, or monitoring actually sound right before recording. This change adds a Discord-style quick microphone check without changing the main recording flow.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Related Issue(s)

N/A

Testing Guide

  • Open the launch microphone dropdown
  • Confirm the microphone test stays collapsed by default
  • Click Start Mic Test, speak, then stop the test and verify playback starts automatically
  • Click Play Last Test to verify replay works
  • Switch to another microphone device and confirm only the selected row shows a live meter
  • Start a normal recording with microphone and system audio enabled to verify the main capture flow still behaves as expected

Validation run locally:

  • npx @biomejs/biome check src/hooks/useScreenRecorder.ts src/components/launch/LaunchWindow.tsx src/components/launch/LaunchWindow.module.css src/hooks/useMicrophoneTest.ts src/hooks/microphoneTestMimeType.ts src/hooks/microphoneTestMimeType.test.ts src/i18n/locales/en/launch.json src/i18n/locales/es/launch.json src/i18n/locales/ko/launch.json src/i18n/locales/nl/launch.json src/i18n/locales/zh-CN/launch.json
  • npm run i18n:check
  • npx tsc --noEmit
  • npx vitest --run src/hooks/microphoneTestMimeType.test.ts src/hooks/recordingMimeType.test.ts src/hooks/useScreenRecorder.test.ts

Checklist

  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation.
  • My changes generate no new warnings or errors.
  • New and existing unit tests pass locally with my changes.
  • Any dependent changes have been merged and published in downstream modules.

Summary by CodeRabbit

  • New Features

    • Microphone self-test: record, stop, and play back audio with real-time level metering and an expandable/collapsible test panel showing status, errors, and playback controls.
  • UI / Styling

    • New styles and buttons for the microphone test flow integrated into the microphone dropdown.
  • Localization

    • Added microphone test strings in English, Spanish, Korean, Dutch, and Chinese.
  • Tests

    • Added unit tests for MIME-type selection logic and microphone test helper behavior.
  • Chores

    • Minor screen-recording source selection simplification.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 39d6c07a-6b5e-4c03-8e9e-3ba228b12978

📥 Commits

Reviewing files that changed from the base of the PR and between c155d58 and 97f2108.

📒 Files selected for processing (1)
  • src/components/launch/LaunchWindow.tsx

📝 Walkthrough

Walkthrough

Adds a microphone self-test feature: new useMicrophoneTest hook + helpers and tests, MIME-type selector and tests, mic-test UI inside the launch window with CSS, small screen-recorder constant tweak, and localization strings in five locales.

Changes

Cohort / File(s) Summary
Microphone test core
src/hooks/useMicrophoneTest.ts, src/hooks/microphoneTestMimeType.ts, src/hooks/microphoneTestMimeType.test.ts, src/hooks/useMicrophoneTest.test.ts
New hook implementing capture, metering, recording, playback, session-guarding, cleanup, exported helper handlers, MIME-type selector utility, and unit tests.
Launch UI
src/components/launch/LaunchWindow.tsx
Integrated mic-test panel into mic dropdown; gated audio metering to selected mic; added start/stop/play controls, expand/collapse logic, and minor icon reorder.
Styles
src/components/launch/LaunchWindow.module.css
Added ~14 CSS module classes for mic-test UI (card, header, preview, buttons, status, layout, hover/disabled states).
Localization
src/i18n/locales/en/launch.json, src/i18n/locales/es/launch.json, src/i18n/locales/ko/launch.json, src/i18n/locales/nl/launch.json, src/i18n/locales/zh-CN/launch.json
Added microphone-test UI strings (actions, statuses, error messages) across five locales; Dutch also adds screen/window empty-state labels.
Screen recorder minor
src/hooks/useScreenRecorder.ts
Introduced LINUX_PORTAL_SOURCE constant and simplified source-selection expression (no behavioral change).

Sequence Diagram

sequenceDiagram
    participant User
    participant Launch as LaunchWindow<br/>Component
    participant Hook as useMicrophoneTest<br/>Hook
    participant Media as Browser Media APIs
    participant Audio as AudioContext &<br/>AnalyserNode
    participant Playback as HTMLAudioElement

    User->>Launch: Click "Start Test"
    Launch->>Hook: startTest()
    Hook->>Media: navigator.mediaDevices.getUserMedia({ audio: { deviceId } })
    Media-->>Hook: MediaStream
    Hook->>Media: new MediaRecorder(stream)

    rect rgba(0, 150, 255, 0.5)
    Note over Hook,Audio: Recording phase (metering + recorder)
    Hook->>Audio: create AudioContext + AnalyserNode + RAF loop
    Audio-->>Hook: level updates
    Launch->>Launch: show AudioLevelMeter
    end

    User->>Launch: Click "Stop Test"
    Launch->>Hook: stopTest()
    Hook->>Media: stop MediaRecorder / stop tracks
    Hook->>Hook: assemble Blob from chunks

    rect rgba(150, 0, 150, 0.5)
    Note over Hook,Playback: Playback phase
    Hook->>Playback: create audio element with blob URL and play()
    Playback-->>Hook: play events / ended / error
    end

    Playback->>Hook: ended
    Hook->>Hook: revoke URL / reset state
    Launch->>User: show "play last" ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • webadderall/Recordly#246: Overlaps LaunchWindow.tsx changes — prior launcher UI edits touch the same component.
  • webadderall/Recordly#269: Related to the Linux "linux-portal" screen-source handling used in useScreenRecorder.
  • webadderall/Recordly#198: Overlaps Dutch localization additions for launch UI keys.

Poem

🐇 I hopped to check a tiny mic,
I recorded a whisper, then gave it a click,
Levels danced, a gentle beat,
Played it back — my carrot-cheerful tweet,
Hooray, the little test worked quick! 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(launch): add a collapsible microphone test' is specific, concise, and accurately describes the main feature addition in the changeset.
Description check ✅ Passed The PR description fully covers all template sections with comprehensive details about the feature, motivation, testing approach, and completed checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@meiiie meiiie marked this pull request as ready for review April 19, 2026 18:20
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/hooks/microphoneTestMimeType.ts (1)

11-15: Optional: guard the default globals for safer standalone use.

MediaRecorder.isTypeSupported(...) and document.createElement("audio") will throw ReferenceError if the utility is ever invoked outside a browser-like environment (e.g., SSR or a non-jsdom test). It's currently gated by useMicrophoneTest's supported check, so this is not an active bug — but a tiny guard would make the function self-sufficient.

♻️ Suggested guard
 	const isTypeSupported =
-		options.isTypeSupported ?? ((type: string) => MediaRecorder.isTypeSupported(type));
+		options.isTypeSupported ??
+		((type: string) =>
+			typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(type));
 	const canPlayType =
 		options.canPlayType ??
-		((type: string) => document.createElement("audio").canPlayType(type));
+		((type: string) =>
+			typeof document !== "undefined" ? document.createElement("audio").canPlayType(type) : "");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/microphoneTestMimeType.ts` around lines 11 - 15, The default
functions assigned to isTypeSupported and canPlayType should guard the globals
to avoid ReferenceError in non-browser environments: replace the current
defaults so isTypeSupported uses MediaRecorder.isTypeSupported only if typeof
MediaRecorder !== "undefined" and typeof MediaRecorder.isTypeSupported ===
"function", otherwise return a safe fallback (e.g., () => false); likewise make
canPlayType use document.createElement("audio").canPlayType only if typeof
document !== "undefined" and document.createElement is a function, otherwise
return a safe fallback (e.g., () => ""). Update the assignments for
isTypeSupported and canPlayType in microphoneTestMimeType.ts accordingly so the
hook is self-sufficient outside browser-like runtimes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 1471-1493: The collapsed preview subtitle currently uses
microphoneTestHasPlayback to choose between "microphoneTestReady" and
"microphoneTestIdle", which hides the unsupported message when
microphoneTestSupported is false; change the subtitle logic in the mic preview
row to reuse the existing microphoneTestStatusText (or at least check
microphoneTestSupported and show the unsupported branch) so the unsupported
reason is visible even when the start button is disabled; locate the subtitle
rendering in LaunchWindow.tsx (the element referencing microphoneTestHasPlayback
/ microphoneTestReady / microphoneTestIdle) and replace it with a call/condition
that uses microphoneTestStatusText or the unsupported branch while leaving the
button handlers (setMicrophoneTestExpanded, startTest) and disabled prop
(canRunMicrophoneTest) unchanged.

In `@src/hooks/useMicrophoneTest.ts`:
- Around line 176-210: The playback handlers and the play() catch block in
playCurrentSample currently only avoid clearing playbackAudioRef but still call
setStatus/setError even when the audio is stale; update playbackAudio.onended,
playbackAudio.onerror, and the catch handler in playCurrentSample to first check
if playbackAudioRef.current === playbackAudio and early-return if not, so stale
events don't call setStatus/setError or mutate state, and ensure
clearPlaybackAudio/stopTest still set state for the active audio; add a
unit/test that starts a playback, calls stopTest() or starts a second
playCurrentSample() before the first play() promise settles, and asserts the
stale rejection does not overwrite the current state.
- Around line 279-290: After awaiting beginMetering(stream, session) add a
session staleness check (compare the current session reference/id stored in your
hook to the local session captured before the await) and return early if they
differ so you don't call setHasPlayback, setError, setStatus("recording"), or
recorder.start() for a stale session; likewise, in the catch block only call
stopInputStream() if the session at the time of error still matches the original
session (guard stopInputStream() with the same session check) so you don't shut
down a newer session's stream. Ensure you reference the same session variable
used around beginMetering, recorder, stopInputStream, setStatus, setError and
setHasPlayback when implementing the comparisons.

In `@src/i18n/locales/zh-CN/launch.json`:
- Line 37: Update the translation for the key "microphoneTestUnsupported" so it
no longer blames the device; replace the string "此设备不支持麦克风测试。" with wording that
references the current environment or browser (for example, "当前环境或浏览器不支持麦克风测试。")
so it reflects missing runtime APIs driven by useMicrophoneTest rather than
implying a hardware fault.

---

Nitpick comments:
In `@src/hooks/microphoneTestMimeType.ts`:
- Around line 11-15: The default functions assigned to isTypeSupported and
canPlayType should guard the globals to avoid ReferenceError in non-browser
environments: replace the current defaults so isTypeSupported uses
MediaRecorder.isTypeSupported only if typeof MediaRecorder !== "undefined" and
typeof MediaRecorder.isTypeSupported === "function", otherwise return a safe
fallback (e.g., () => false); likewise make canPlayType use
document.createElement("audio").canPlayType only if typeof document !==
"undefined" and document.createElement is a function, otherwise return a safe
fallback (e.g., () => ""). Update the assignments for isTypeSupported and
canPlayType in microphoneTestMimeType.ts accordingly so the hook is
self-sufficient outside browser-like runtimes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46049cc3-ceb5-476d-a1c7-d2ac7033babc

📥 Commits

Reviewing files that changed from the base of the PR and between daf99c0 and acd5599.

📒 Files selected for processing (11)
  • src/components/launch/LaunchWindow.module.css
  • src/components/launch/LaunchWindow.tsx
  • src/hooks/microphoneTestMimeType.test.ts
  • src/hooks/microphoneTestMimeType.ts
  • src/hooks/useMicrophoneTest.ts
  • src/hooks/useScreenRecorder.ts
  • src/i18n/locales/en/launch.json
  • src/i18n/locales/es/launch.json
  • src/i18n/locales/ko/launch.json
  • src/i18n/locales/nl/launch.json
  • src/i18n/locales/zh-CN/launch.json

Comment thread src/components/launch/LaunchWindow.tsx
Comment thread src/hooks/useMicrophoneTest.ts
Comment thread src/hooks/useMicrophoneTest.ts
Comment thread src/i18n/locales/zh-CN/launch.json Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 286-288: The microphone self-test stays enabled whenever
micDropdownOpen is true, allowing it to run during recording/countdown; update
the enable logic for useMicrophoneTest (the enabled prop where micDropdownOpen
&& devices.length > 0 and/or the canRunMicrophoneTest check) to also require
!recording && !countdownActive so the test is disabled during countdown or
active recording, and additionally call whatever UI handler closes the mic
dropdown (e.g., setMicDropdownOpen(false) or closeMicDropdown) when recording or
countdown starts so the test UI cannot remain interactive over live recording
controls.

In `@src/hooks/useMicrophoneTest.ts`:
- Around line 330-336: The onstop handler mutates shared refs
(recorderRef.current and stream via stopInputStream()) before verifying the
stopped recorder belongs to the active session, which can stop a newer session;
change recorder.onstop to first check sessionRef.current === session (and
optionally that recorderRef.current === recorder) and return early if not, only
then clear recorderRef.current and call stopInputStream(); add a regression test
that triggers an old recorder.onstop after a new session becomes current to
ensure the guard prevents stopping the new stream.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a578f620-103a-4f04-89ec-c2a61674ca14

📥 Commits

Reviewing files that changed from the base of the PR and between acd5599 and a999dad.

📒 Files selected for processing (5)
  • src/components/launch/LaunchWindow.tsx
  • src/hooks/microphoneTestMimeType.ts
  • src/hooks/useMicrophoneTest.test.ts
  • src/hooks/useMicrophoneTest.ts
  • src/i18n/locales/zh-CN/launch.json
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/locales/zh-CN/launch.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/microphoneTestMimeType.ts

Comment thread src/components/launch/LaunchWindow.tsx
Comment thread src/hooks/useMicrophoneTest.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/launch/LaunchWindow.tsx (1)

272-342: Mic-test wiring and state machine look solid.

The useMicrophoneTest gating (micDropdownOpen && !recording && !countdownActive && devices.length > 0), the canRunMicrophoneTest check, and the three effects (auto-collapse on dropdown close, auto-close dropdown + collapse on recording/countdown, auto-expand on busy/error) together give a clean state machine. The auto-expand effect's dependency list ([microphoneTestBusy, microphoneTestStatus]) also correctly avoids re-expanding an error card after a manual collapse, since deps don't change between renders while the status is stable.

One optional refactor: microphoneTestStatusText is computed via an IIFE on every render. Wrapping in useMemo (or extracting a pure helper) would make intent clearer and avoid the micro re-work on unrelated renders.

♻️ Suggested tweak
-	const microphoneTestStatusText = (() => {
-		if (!microphoneTestSupported) {
-			return t("recording.microphoneTestUnsupported");
-		}
-
-		switch (microphoneTestStatus) {
-			case "recording":
-				return t("recording.microphoneTestRecording");
-			case "playing":
-				return t("recording.microphoneTestPlaying");
-			case "error":
-				switch (microphoneTestError) {
-					case "permission-denied":
-						return t("recording.microphoneTestPermissionDenied");
-					case "playback-failed":
-						return t("recording.microphoneTestPlaybackFailed");
-					case "capture-failed":
-						return t("recording.microphoneTestCaptureFailed");
-					default:
-						return t("recording.microphoneTestUnsupported");
-				}
-			case "idle":
-			default:
-				return microphoneTestHasPlayback
-					? t("recording.microphoneTestReady")
-					: t("recording.microphoneTestIdle");
-		}
-	})();
+	const microphoneTestStatusText = useMemo(() => {
+		if (!microphoneTestSupported) {
+			return t("recording.microphoneTestUnsupported");
+		}
+		switch (microphoneTestStatus) {
+			case "recording":
+				return t("recording.microphoneTestRecording");
+			case "playing":
+				return t("recording.microphoneTestPlaying");
+			case "error":
+				switch (microphoneTestError) {
+					case "permission-denied":
+						return t("recording.microphoneTestPermissionDenied");
+					case "playback-failed":
+						return t("recording.microphoneTestPlaybackFailed");
+					case "capture-failed":
+						return t("recording.microphoneTestCaptureFailed");
+					default:
+						return t("recording.microphoneTestUnsupported");
+				}
+			default:
+				return microphoneTestHasPlayback
+					? t("recording.microphoneTestReady")
+					: t("recording.microphoneTestIdle");
+		}
+	}, [microphoneTestSupported, microphoneTestStatus, microphoneTestError, microphoneTestHasPlayback, t]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/launch/LaunchWindow.tsx` around lines 272 - 342,
microphoneTestStatusText is recomputed every render via an IIFE; wrap the logic
in a useMemo (or extract a pure helper) to avoid unnecessary work and make
intent clearer. Move the IIFE into React.useMemo and list its dependencies:
microphoneTestSupported, microphoneTestStatus, microphoneTestError,
microphoneTestHasPlayback, and t (the translation fn); keep the same switch
logic and return values so behavior of microphoneTestStatusText,
microphoneTestBusy, canRunMicrophoneTest and the existing effects (which
reference microphoneTestStatus) remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 1444-1461: The primary mic test button is disabled based solely on
canRunMicrophoneTest, which can flip false mid-test and prevent the user from
calling stopTest; update the disabled logic on the button (component using
microphoneTestBusy, canRunMicrophoneTest, onClick handlers startTest/stopTest,
and className micTestButton inside micTestActions) so it remains enabled while
microphoneTestBusy is true (e.g., disabled only when !canRunMicrophoneTest AND
!microphoneTestBusy), keeping the current onClick behavior that calls stopTest
when microphoneTestBusy and startTest otherwise.

---

Nitpick comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 272-342: microphoneTestStatusText is recomputed every render via
an IIFE; wrap the logic in a useMemo (or extract a pure helper) to avoid
unnecessary work and make intent clearer. Move the IIFE into React.useMemo and
list its dependencies: microphoneTestSupported, microphoneTestStatus,
microphoneTestError, microphoneTestHasPlayback, and t (the translation fn); keep
the same switch logic and return values so behavior of microphoneTestStatusText,
microphoneTestBusy, canRunMicrophoneTest and the existing effects (which
reference microphoneTestStatus) remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e1874967-f048-43da-918c-f9819dc6a15b

📥 Commits

Reviewing files that changed from the base of the PR and between a999dad and c155d58.

📒 Files selected for processing (3)
  • src/components/launch/LaunchWindow.tsx
  • src/hooks/useMicrophoneTest.test.ts
  • src/hooks/useMicrophoneTest.ts
✅ Files skipped from review due to trivial changes (2)
  • src/hooks/useMicrophoneTest.test.ts
  • src/hooks/useMicrophoneTest.ts

Comment thread src/components/launch/LaunchWindow.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant