Extend timing alignment test to three measures

This commit is contained in:
firestar5683
2026-09-19 19:46:40 -07:00
parent a134503c61
commit ae87804add
6 changed files with 16 additions and 13 deletions
@@ -1,7 +1,7 @@
"""Explicitly attended calibration process; no import-time audio output."""
import time
RATE=48000
from operator_output import COUNT, COUNT_IN, INTERVAL, MARKER_OFFSETS
from operator_output import COUNT, COUNT_IN, TEST_COUNT, INTERVAL, MARKER_OFFSETS
class ClickSequence:
"""Prebuilt low-level click PCM; callback only copies memory and timestamps."""
@@ -13,8 +13,8 @@ class ClickSequence:
self.index = 0
self.failed = False
start = 2.0
if count not in (4,COUNT):raise ValueError('Unsupported calibration sequence')
offsets=[start+i*INTERVAL for i in range(4 if count==4 else COUNT_IN)]
if count not in (TEST_COUNT,COUNT):raise ValueError('Unsupported calibration sequence')
offsets=[start+i*INTERVAL for i in range(TEST_COUNT if count==TEST_COUNT else COUNT_IN)]
if count==COUNT:offsets+=list(MARKER_OFFSETS)
self.beats = [round(at * RATE) for at in offsets]
self.pcm = np.zeros((self.beats[-1] + RATE, 2), dtype='float32')
@@ -28,7 +28,7 @@ class ClickSequence:
second = round(.08 * RATE)
marker[second:second + len(tone_t)] = .07 * np.sin(2 * np.pi * 1760 * tone_t) * envelope
for index,beat in enumerate(self.beats):
click = marker if index >= COUNT_IN else downbeat if index % 4 == 0 else beat_click
click = marker if count == COUNT and index >= COUNT_IN else downbeat if index % 4 == 0 else beat_click
self.pcm[beat:beat + len(click)] = click[:, None]
self.stream = sd.OutputStream(device=device, samplerate=RATE, channels=2, dtype='float32', blocksize=480,
callback=self.callback)
@@ -60,7 +60,7 @@ def main():
from pathlib import Path
from bluetooth_output import prepare_output,select_device
from operator_output import real_offroad,selected_output
p=argparse.ArgumentParser();p.add_argument('--address',required=True);p.add_argument('--count',type=int,choices=[4,COUNT],required=True);args=p.parse_args()
p=argparse.ArgumentParser();p.add_argument('--address',required=True);p.add_argument('--count',type=int,choices=[TEST_COUNT,COUNT],required=True);args=p.parse_args()
output=selected_output()
if not real_offroad() or not output or not output['connected'] or output['address']!=args.address:
raise RuntimeError('Park and reconnect the selected Bluetooth speaker before calibration')
+3 -2
View File
@@ -18,6 +18,7 @@ import time
BPM = 100
INTERVAL = 60 / BPM
TEST_COUNT = 12
COUNT_IN = 8
COUNT = 20
MARKER_COUNT = COUNT - COUNT_IN
@@ -268,7 +269,7 @@ class OutputOwner:
token = secrets.token_hex(16)
clicks = {}
try:
sink = self.sink_factory(lambda index, at: clicks.__setitem__(index, at), count=4 if action == 'test' else COUNT)
sink = self.sink_factory(lambda index, at: clicks.__setitem__(index, at), count=TEST_COUNT if action == 'test' else COUNT)
if self.output_provider() != state['output']:
sink.close()
raise ValueError('Output changed before calibration began')
@@ -283,7 +284,7 @@ class OutputOwner:
session_lease.__exit__(None, None, None); operator.__exit__(None, None, None)
raise
threading.Thread(target=self._watch, args=(token,), daemon=True).start()
return dict(ok=True, session=token, interval_ms=round(INTERVAL * 1000), beats=4 if action=='test' else COUNT,
return dict(ok=True, session=token, interval_ms=round(INTERVAL * 1000), beats=TEST_COUNT if action=='test' else COUNT,
count_in=COUNT_IN,bpm=BPM,beats_per_bar=4,method=METHOD,marker_count=MARKER_COUNT,target_taps=MARKER_COUNT,
marker_offsets_ms=[round(at*1000) for at in MARKER_OFFSETS],
instructions='Listen to two bars without tapping. Then tap once at the START of each two-tone marker; wait through the silence. The estimate includes your reaction time.')
+4 -2
View File
@@ -211,10 +211,12 @@ class OutputTests(unittest.TestCase):
stamps=[]
with patch.dict(sys.modules,{'sounddevice':fake}):
from operator_click_process import ClickSequence
sink=ClickSequence(lambda i,at:stamps.append((i,at)),0,count=4)
second=ClickSequence(lambda *_:None,0,count=4)
sink=ClickSequence(lambda i,at:stamps.append((i,at)),0,count=m.TEST_COUNT)
second=ClickSequence(lambda *_:None,0,count=m.TEST_COUNT)
markers=ClickSequence(lambda *_:None,0,count=m.COUNT)
markers_again=ClickSequence(lambda *_:None,0,count=m.COUNT)
self.assertEqual(len(sink.beats), 12)
self.assertTrue(all(b-a == 28800 for a,b in zip(sink.beats,sink.beats[1:])))
self.assertTrue(np.array_equal(sink.pcm,second.pcm))
self.assertTrue(np.array_equal(markers.pcm,markers_again.pcm))
self.assertEqual(len(markers.beats),20)
@@ -36,7 +36,7 @@
</div>
<!-- Snackbar for messages -->
<div id="snackbar_wrapper"></div>
<script type="module" src="/assets/mobile/js/app.js?v=roadscore-calibration-3"></script>
<script type="module" src="/assets/mobile/js/app.js?v=roadscore-calibration-4"></script>
</body>
</html>
@@ -1,4 +1,4 @@
import { RoadScore } from "./views/RoadScore.js?v=calibration-markers-3"
import { RoadScore } from "./views/RoadScore.js?v=calibration-test-4"
import { createApp, h } from "vue"
import { AppShell } from "./components/AppShell.js?v=roadscore-calibration-2"
import { Home } from "./views/Home.js"
@@ -73,8 +73,8 @@ export const RoadScore = {
const first = this.testClicks.find(click => click.beat === 0)
const index = first ? Math.floor((now - first.server_ms) / (60000 / this.bpm)) : -1
this.beatIndex = index
this.beat = index >= 0 && index < this.countIn ? String(index % 4 + 1) : '♪'
this.cue = index < 0 ? 'Getting ready…' : index < this.countIn && !this.testMode ? `Listen · count-in bar ${Math.floor(index / 4) + 1} of 2` : this.testMode ? 'Watch and listen' : 'Tap once when you hear each chime · wait through the gaps'
this.beat = index >= 0 && index < (this.testMode ? 12 : this.countIn) ? String(index % 4 + 1) : '♪'
this.cue = index < 0 ? 'Getting ready…' : index < this.countIn && !this.testMode ? `Listen · count-in bar ${Math.floor(index / 4) + 1} of 2` : this.testMode ? (index < 12 ? `Watch and listen · bar ${Math.floor(index / 4) + 1} of 3` : 'Test complete') : 'Tap once when you hear each chime · wait through the gaps'
this.animation = requestAnimationFrame(() => this.animateBeat())
},
keyTap(event) {