macOS app: you-vs-TV attribution, mic-restart robustness, real-window durations - #15
Merged
Merged
Conversation
The audioConfigChanged handler restarted the engine on every .AVAudioEngineConfigurationChange, but (re)starting AVAudioEngine posts that same notification — so it re-triggered itself in a loop, and the engine never ran long enough for SoundAnalysis to detect anything (no laughs, no log entries, while the mic looked live). Guard against it: set suppressConfigChange around our own start/stop and ignore config-change notifications while it's set (released ~1s after the engine settles). Genuine later device changes are still handled once the guard clears. The handler now also touches state only on the main thread. Fixes #14 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
The activity log ruled out the restart-loop theory (only manual restarts), so the detection path itself produces nothing. Two changes: - Configure the SoundAnalysis analyzer BEFORE starting the audio engine (split AudioHub into prepareFormat() + start(format:)), matching the original working order — the analyzer now exists before any buffer arrives, so no early buffers are dropped and analysis reliably starts. Keeps the post-prepare format validation that fixed the cold-start case. - Add a detection heartbeat to the operational log: "first analysis result received" once per stream, then "detection heartbeat: N windows, max laugh score X" every 60 windows. This makes a "no laughs detected" report answerable from the log alone — it distinguishes no-results (analyzer silent) from results-with-low-scores (classifier not hearing laughter) from scores-but-no-episodes (counter issue). Refs #14 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
Field-informed fixes after the v0.2.0 report (the "no laughs" turned out to be a
wedged USB mic, not the app):
- Restart hardening: all (re)start triggers (launch, wake, config-change, manual
resume) now go through a single-flight, rate-limited path with a brief settle
between teardown and re-acquire — the engine can't overlap-restart or
rapid-cycle, which is what can wedge a USB mic.
- Merge overlapping analysis windows: the built-in classifier emits ~3s windows
every ~1.5s, so mergeGap (was 1.0) sat below the hop — every window became its
own laugh, double-counting one laugh and pinning duration at a constant ~3s.
Raised to 2.0 so a laugh spanning windows is one episode with a varied (if
coarse) duration. Documented duration as coarse in the README.
- Match gerund laugh classes: the classifier emits "giggling", but the keyword
was "giggle" (no substring match), so giggles were shunted to context instead
of counted. Keywords are now stems ("giggl", "chuckl", …).
- Trim the detection heartbeat to one "first analysis result received" line per
stream (dropped the noisy periodic beat now that detection is confirmed).
Refs #14
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
Field data showed context-class *suppression* couldn't separate TV laughter from the owner's (both are the "laughter" class). Replace it with a transparent you-vs-TV hypothesis and keep every laugh: - Attribution: the strongest "produced-audio" context class — music, speech, audience, instruments (tvKeywords expanded to cover them) — becomes the episode's tv_signal. tv_signal >= tvThreshold (0.3) → origin "tv", else "me". In the sample data this cleanly split a 40s music/speech/crowd run (TV) from clean high-confidence laughter+giggling+breathing (you). It's a guess, logged with its reason; nothing is discarded. - Two menu-bar counters: "😄 N 📺 M" (you vs TV today). todayCount(origin:) splits the buckets; a reported miss counts as you. Only your laughs blip. - Denser low-confidence logging: enterThreshold 0.3 -> 0.15 (exit 0.2 -> 0.1) so near-misses are logged as candidates too — later "I laughed" feedback can align to a nearby event instead of nothing. - Log origin / tv_signal / origin_reason in the JSONL and the activity log; document the two counters and hypothesis in the README. Removed the old onSuppressed/tvContextRatio gate (superseded by attribution). Refs #7 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
missingbulb
pushed a commit
that referenced
this pull request
Jul 12, 2026
CFBundleShortVersionString 0.2.0 -> 0.2.1 (build 3) to match the release tag, following the #15 merge (attribution, mic-restart robustness, duration fix). Refs #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
missingbulb
added a commit
that referenced
this pull request
Jul 12, 2026
CFBundleShortVersionString 0.2.0 -> 0.2.1 (build 3) to match the release tag, following the #15 merge (attribution, mic-restart robustness, duration fix). Refs #16 Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Field-driven follow-ups after v0.2.0. The reported "no laughs detected" turned out to be a wedged USB webcam mic (a physical reconnect fixed it), not a code bug — but the investigation surfaced several real improvements, and a better direction for TV handling.
You-vs-TV attribution + two counters (#7 rethought). Context-class suppression couldn't separate TV laughter from the owner's — both are the
laughterclass. Replaced with a transparent hypothesis: the strongest produced-audio context class (music / speech / audience / instruments) becomestv_signal;≥ 0.3→origin: "tv", else"me". On the sample data this cleanly split a 40s music/speech/crowd run (TV) from clean high-confidence laughter+giggling+breathing (owner). Menu bar shows😄 N 📺 M; only your laughs blip; nothing is discarded — every laugh is logged withorigin/tv_signal/origin_reason. It's a guess (a real laugh over TV music will misfire); loudness/proximity and eventual laugh-enrollment would sharpen it.Mic-restart robustness (#6). The overhaul added engine stop/start cycles the original never did (wake / config-change / manual resume); aggressive
AVAudioEnginerestarts can wedge a USB mic. All triggers now go through one single-flight, rate-limited path with a settle delay between teardown and re-acquire — no overlap-restart, no rapid-cycle. Plus a guard so our own restart'sAVAudioEngineConfigurationChangecan't self-trigger a loop, and the analyzer is configured before audio starts.Real-window durations, no double-count. SoundAnalysis emits ~3s windows every ~1.5s;
mergeGap(was 1.0, below the hop) made each overlapping window its own laugh — double-counting and pinning duration at ~3s. Raised to 2.0 so one laugh is one episode with a varied (if coarse ~3s) duration; documented the coarseness honestly.Detection quality + logging. Laugh keywords are now stems (
giggl,chuckl, …) so the classifier'sgigglingcounts.enterThresholdlowered (0.3 → 0.15) so near-misses log ascandidates — later "I laughed" feedback can align to a nearby event. Trimmed the diagnostic heartbeat to one "first analysis result received" per stream.Fixes #14. Refs #6, #7.
🤖 Generated with Claude Code