Repository navigation
Phone generator: refresh the clock while the test signal runs (slewed) - #72
Merged
Merged
Conversation
Shared C++ for the AV Sync calibrator board (docs/calibrator.md, contract v1): - find_sound_onsets: clicks and beeps in a recording (trailing 0.25 ms RMS, 25 % of peak), told apart by 2.7 kHz vs 1.5 kHz energy. - find_flash_onsets: LED flash start from row-band means, on the rolling-shutter row the analyzer uses; readout from the band height or, with full-frame exposure, fitted over the pulses in camera_calibration. - camera_calibration: pair flashes with clicks, robust median, uncertainty. - display_calibration: click then beep D = 100 ms later, offset = gap - D, both swap directions weighted equally. - calibration pattern for all generators: halves swap every 500 ms, beep. Unit tests on synthetic signals (noise, missing pulses, stray sound, rolling shutter with known/unknown readout, slow screen, sensor mismatch). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
PhoneAnalyzer::begin_calibrator keeps 32 row-band means per frame and the microphone, and poll_calibrator reports progress about once a second, then the result (20 pairs or 30 s) or an error. No live report or QR scan while it runs. iOS: beginCalibrator(mode) on the camera view; the camera take first re-locks exposure (frame interval) at the lowest ISO, because the LED floods the lens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
…the QR
generateStyled(..., {mode: 'calibration'}) shows the calibration pattern in
the generator's own screen: the same display link, layers, audio engine and
player node as the test signal. Portrait halves left and right near the
bottom microphone, a red outline of the board, halves swap every 500 ms, a
1.5 kHz beep 100 ms after each swap (scheduled on the host clock minus the
reported output latency). The session is play-and-record on the built-in
speaker; the microphone take is analysed with the shared core
(display_calibration) and the result comes back when the screen closes.
The test signal sends a saved sender calibration in the AVS4 QR
(senderCalibration* options), only on the audio output it was measured on.
Core pattern helpers are now frame based (also 59.94 fps).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Same feature as the phone: the generator can send the calibration pattern instead of the test signal, so a phone with the calibrator board can measure a TV/monitor chain. Left/right halves swap every 500 ms on the grain grid (also interlaced and 59.94), a 1.5 kHz beep 100 ms after each swap at the chirp level, a grey tick at the ends of the pattern line. Live key, also in the Signal API (pattern) and the web form (Picture). tests/tools/calibration_flow + tests/integration/calibration_pattern.py read both flows, check the timing and run a simulated calibrator through display_calibration (1080p50, 1080i25, 1080p59.94: 0.03 ms). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Three steps with small animations: plug in, camera (the calibrator on the rear camera; progress ring of 20 flashes; 'Camera vs mic' result saved as this phone's analyser calibration, method calibrator-pulse, same record and validity as the clap), screen (the generator's calibration pattern; 'Screen vs speaker' saved as this phone's sender calibration, method transponder). The test signal then sends it in the AVS4 QR. A TV/monitor showing the container's calibration pattern can be measured too (result only, not saved). The clap calibration is unchanged. iOS only for now (Android has no generator and no calibrator take yet). Pure logic in src/calibrator.mjs with node tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
docs/calibrator.md (copy of PR #69's contract) gets an 'App side' section and the list of changes from the v1 proposal (none needs a firmware change). calibration.md: user guide 'With the calibrator'. phone-generator.md: calibration mode and the sender calibration in the QR. SPECIFICATION: AVSYNC_PATTERN and the calibrator in 12.3. IMPLEMENTATION_PLAN: entry with the deviations. CHANGELOG, mobile-app README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
… clap gap fill unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Done once in core (Pairer::set_sender_calibration) for the phone session, the container analyzer and the file analyzer. Events keep raw_av_offset_ms and sender_calibration_ms; the phone reading has rawOffsetMs and senderCalibrationMs; /api/v1/measurement has raw_offset_ms and sender_calibration_ms; the web UI Input card shows AV sync raw. Not calibrated: no change. The phone fixture sends a sender calibration at 2 Hz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
…ickname Measure and Results show the raw value and the sender calibration under a corrected result; saved results keep them. The analyser calibration stays subtracted in the app only, so nothing counts twice. Clear calibration asks first and clears the analyser and the sender calibration. A sender's default name is the newest Title from its QR; a set nickname wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
… nickname Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
PhoneGeneratorPlan, the AVS4 QR, render_picture/render_sync_foreground and the calibration pattern functions compile into libavsync-jni as on iOS (generator_jni.cpp mirrors AVSyncGenerator.mm). The renderer's text goes through JNI to Android's text drawing with the bundled DejaVu Sans (GeneratorText.kt), as iOS uses CoreText; the font and icon come from the iOS resource folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Same behaviour and options as the iPhone generator: test picture with the AVS3 fields, sync square and scale, AVS4 QR with test ID, title and sender calibration, stable sender ID, 'Stopped' screen, calibration pattern with the microphone. Frames: Choreographer's expected presentation time (API 33+, else frame start plus two periods). Sound: one AudioTrack stream written in 5 ms blocks; packets are placed to the sample by AudioTrack.getTimestamp on System.nanoTime, the clock Choreographer uses. Module: supportsGenerator, generate, generateStyled, pickGeneratorImage, generatorImageInfo, like iOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
beginCalibrator('camera' | 'display') as on iOS: the camera take re-runs the
auto step and locks at the lowest ISO (or the lowest exposure compensation when
the camera has no manual exposure), then PhoneAnalyzer::begin_calibrator starts;
display mode listens only. Progress and result reports come from
poll_calibrator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
…on, clear both) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Same file on both branches now, so the merge has no conflict here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
… on iOS Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
The test signal stopped itself about 5 minutes after the last clock check. Now the app hands each new check to the running signal (updateGeneratorClock, iOS and Android). Core PhoneGeneratorPlan::retime slews the TAI stamps of events not queued yet toward the new mapping, at most 100 ppm per event; host times never move, so frames stay on the display grid and sound and picture of one event keep one stamp. A check further off than the limit stops the signal with a clear message. The expiry rule moved to core (clock_reference_expired); the app's check cadence moved to clockCheckDue and now checks a minute before the holdover passes the limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Pressing Start before the first NTP check finished kept the signal on device UTC for the whole run. Now the first accepted check switches it to NTP UTC with the same 100 ppm timestamp slew (core retime from the device-UTC mapping). A check further off than the limit leaves it on device UTC without stopping and says so in the status line; a later check within the limit can still switch it. While slewing, the status shows the correction still to go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
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.
Stacked on #71 (feat/android-generator).
Problem
The phone test signal took the NTP clock check once at Start and stopped itself ("Clock reference expired") once that check was 5 minutes old, or sooner once the holdover passed the limit. So it always died about 5 minutes after the last check.
Fix (option 1: refresh while running)
App.js): while the signal runs, every new clock check is passed to it with the new native callupdateGeneratorClock(options). It is the same JS call on iOS and Android, and it takes the same options asgeneratorClockOptions. The check cadence is now one shared rule,clockCheckDue(src/time-reference.mjs), for measuring and for the signal: a check a minute before the 5-minute age limit, or (at most every 30 s) when the holdover would pass the limit within a minute. Before, the app checked only after the limit was passed, which was too late for a running signal.PhoneGeneratorPlan::retime,clock_offset_ns,clock_reference_expired): this code is shared, so iOS and Android behave the same.AvsyncGeneratorController.swift, AndroidAvsyncGenerator.kt):updateClockchecks the new reference, slews fromnextAudio(the first event not queued yet), and keeps it as the current reference. The status line still says "NTP UTC", now as "NTP UTC (check 42 s ago, adjusting +0.8 ms)" (the part of the slew still to go; nothing while there is none).retime, iOS and Android identical). If the device clock is further from the check than the limit, it does not stop: it stays on device UTC and the status says "device UTC (clock check +35.0 ms off, more than the 20 ms limit)". A later check within the limit can still switch it. Once switched, the normal rules apply (refresh, expiry, stop on a big jump).source_reference: phone-utc-scheduled-outputeither way). So an analyser only sees the timestamps slew. The close result hasclockApplied: trueand the check inclockReferencefrom the moment of the switch, not from the end of the slew.PhoneGeneratorPlan(checked by reading the code; no change).Tests
test_timecode.cpp), 2 new cases:node --test mobile-app/src/*.test.mjs tests/web/*.test.cjs: 60 pass, includingclockCheckDue, a 15-minute JS run with and without network, and the first check reaching a signal started before it.:avsync:testDebugUnitTest(newclockCheckMapsToTheHostClock),:avsync:assembleDebugAndroidTest(new instrumentedclockRefreshSlewsStampsNotHostTimes, compiled but not run),:app:assembleRelease: build successful.tests/native/generator-runner.mmhas new retime/expiry checks.Needs the Mac: build, then
npm run test:ios-generatorandnpm run test:ios-native. Changed iOS files:AVSyncGenerator.h,AVSyncGenerator.mm,AvsyncGeneratorController.swift,AvsyncModule.swift,tests/native/generator-runner.mm.Device test
🤖 Generated with Claude Code
https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4