Spaces:
Sleeping
Sleeping
Download scripts/gsharp_ab_probe.mjs from jasonmdong/notepilot: direct link, hf CLI and curl.
- Browser
- Download file 9.59 kB
-
https://huggingface.co/spaces/jasonmdong/notepilot/resolve/main/scripts/gsharp_ab_probe.mjs
- Command line
-
hf download hf://spaces/jasonmdong/notepilot/scripts/gsharp_ab_probe.mjs
-
curl -L -o gsharp_ab_probe.mjs https://huggingface.co/spaces/jasonmdong/notepilot/resolve/main/scripts/gsharp_ab_probe.mjs
9.59 kB
| // Controlled physical-mic A/B probe for the opening G#2 / A♭2. | |
| // Records only a scratch trace and stereo WebM: left = physical microphone, | |
| // right = exact digital accompaniment output. The solo is deliberately not a | |
| // third channel; its MIDI, audio-context time, and condition are logged. | |
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import { parseArgs } from 'node:util'; | |
| import { connectNotePilot } from './mic_loop_support.mjs'; | |
| const { values: args } = parseArgs({ options: { | |
| out: { type: 'string' }, mic: { type: 'string', default: 'default' }, | |
| volume: { type: 'string', default: '0.5' }, 'settle-seconds': { type: 'string', default: '8' }, | |
| repetitions: { type: 'string', default: '4' }, help: { type: 'boolean', default: false }, | |
| } }); | |
| if (args.help) { | |
| console.log('node scripts/gsharp_ab_probe.mjs --out /tmp/gsharp-ab.json [--repetitions 4]'); | |
| process.exit(0); | |
| } | |
| const out = path.resolve(args.out || ''); | |
| const repetitions = Number(args.repetitions), volume = Number(args.volume), settle = Number(args['settle-seconds']); | |
| if (!args.out || fs.existsSync(out) || out.includes(`${path.sep}.accompy_traces${path.sep}`)) throw Error('--out must be a new scratch file outside .accompy_traces'); | |
| if (!Number.isInteger(repetitions) || repetitions < 3 || repetitions > 5 || !(volume >= 0 && volume <= 1) || !(settle >= 0)) throw Error('Use 3–5 repetitions, volume 0–1, and a non-negative settle time'); | |
| const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); | |
| const c = await connectNotePilot(); | |
| let started = false, recording = false; | |
| try { | |
| const before = await c.evaluate('({playing:state.playing,trace:NPTrace.toObject()})'); | |
| if (before.playing) throw Error('End the current performance before running the probe'); | |
| if (before.trace) fs.writeFileSync(out + '.previous.json', JSON.stringify(before.trace), {flag:'wx'}); | |
| await c.send('Page.bringToFront'); | |
| await c.send('Page.reload', {ignoreCache:true}); | |
| for (let i = 0; i < 60; i++) { | |
| if (await c.evaluate('typeof state !== "undefined" && !!state.current').catch(() => false)) break; | |
| if (i === 59) throw Error('Open a score in NotePilot before running this probe'); | |
| await sleep(500); | |
| } | |
| await c.evaluate(`(async () => { | |
| setSelectedPartIndices([1]); | |
| await onMicEngineChange('follower'); | |
| const devices = await navigator.mediaDevices.enumerateDevices(); | |
| const mic = devices.find(d => d.kind === 'audioinput' && (d.deviceId === ${JSON.stringify(args.mic)} || d.label.toLowerCase().includes(${JSON.stringify(args.mic.toLowerCase())}))); | |
| if (!mic) throw Error('Requested microphone was not found'); | |
| _selectedMicId = mic.deviceId; | |
| const select = document.getElementById('mic-select'); select.value = mic.deviceId; | |
| select.dispatchEvent(new Event('change', {bubbles:true})); | |
| await onOutputDeviceChange('solo', 'default'); | |
| await onOutputDeviceChange('accompaniment', 'default'); | |
| // One Web Audio clock is required for the paired recorder. Empty IDs still | |
| // select the system-default speaker rather than a different device. | |
| _outputSinkIds.solo = ''; _outputSinkIds.accompaniment = ''; | |
| setPlaybackVolume('solo', 100); setPlaybackVolume('accompaniment', ${volume * 100}); | |
| setInputMode('mic'); | |
| })()`); | |
| await sleep(3000); | |
| console.log('Calibrating latency, gate and room response (audible)…'); | |
| await c.evaluate('calculateMicLatency()', 120000); | |
| let finished = false; | |
| for (let i = 0; i < 240; i++) { | |
| finished = await c.evaluate('!_micLatencyCalibration.active && !_calibrationBusy && !_rigSweepActive'); | |
| if (finished) break; | |
| await sleep(500); | |
| } | |
| if (!finished) throw Error('Latency, gate, and room-response calibration did not all finish'); | |
| const calibration = await c.evaluate(`({result:document.getElementById('mic-latency-result')?.textContent, | |
| active:_micLatencyCalibration.active,busy:_calibrationBusy,sweep:_rigSweepActive,gate:document.getElementById('noise-gate')?.value})`); | |
| console.log(calibration.result); | |
| console.log(`Settling mic and output for ${settle}s after calibration…`); | |
| await sleep(settle * 1000); | |
| await c.evaluate(`(() => { const b=document.getElementById('bpm-input'); b.value='60'; b.dispatchEvent(new Event('change',{bubbles:true})); state.practiceStartBeat=0; })()`); | |
| await c.evaluate('startPlaying()', 30000); | |
| if (!await c.evaluate('state.playing && !!_scoreFollower && !!_pitchDetector')) throw Error('Follower session did not start'); | |
| // This probe supplies its own known accompaniment. Stop the ordinary clock so | |
| // neither condition receives unlogged score playback. Disabling only the | |
| // test session's watchdog avoids artificial score advances during silence. | |
| await c.evaluate(`(() => { | |
| state.accompanist.stop(); _skipAutoEnabled = false; | |
| const ctx = audioCtx(); | |
| window.__npReferenceBus = ctx.createGain(); | |
| window.__npReferenceRole = 'accompaniment'; | |
| const source = _pitchDetector._source, merger = ctx.createChannelMerger(2), dest = ctx.createMediaStreamDestination(); | |
| source.connect(merger,0,0); window.__npReferenceBus.connect(merger,0,1); merger.connect(dest); | |
| const mime=MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; | |
| const recorder=new MediaRecorder(dest.stream,{mimeType:mime}), chunks=[]; | |
| recorder.ondataavailable=e=>{if(e.data.size) chunks.push(e.data)}; | |
| window.__npPairedCapture={recorder,chunks,merger,dest,mime}; recorder.start(1000); | |
| NPTrace.log('test.captureStart',{sampleRate:ctx.sampleRate,mime}); | |
| NPTrace.meta({testRun:{harness:'gsharp_ab_probe',protocol:'ABBA',repetitions:${repetitions}, | |
| settleMs:${settle * 1000}, calibration:${JSON.stringify(calibration)}, pairedCapture:true, referenceRole:'accompaniment', | |
| micRole:'physical', solo:{midi:44,name:'G#2',hz:103.826}, | |
| accompaniment:[{midi:60,offsetMs:0},{midi:63,offsetMs:500},{midi:68,offsetMs:1000}], | |
| trialSpacingMs:3500, soloOffsetMs:250, outputPath:'web-audio-sample'}}); | |
| })()`); | |
| recording = true; started = true; | |
| // ABBA pairs: each trial repeats the opening accompaniment arpeggio C4, | |
| // D#4, G#4. B adds only G#2 at +250 ms. The follower is reset to score | |
| // index 1 before each trial solely to expose its real G#2 likelihood; its | |
| // watchdog and score-driven accompaniment are disabled above. | |
| const order = Array.from({length:repetitions}, () => ['A','B','B','A']).flat(); | |
| const schedule = await c.evaluate(`(() => { | |
| const ctx=audioCtx(), start=ctx.currentTime+1, spacing=3.5, offset=.25, n=${order.length}; | |
| const order=${JSON.stringify(order)}; | |
| const traceT = at => performanceTimeForAudioContextTime(ctx,at)-NPTrace.current().t0; | |
| for(let i=0;i<n;i++) { | |
| const at=start+i*spacing, condition=order[i], resetAt=Math.max(ctx.currentTime,at-.35); | |
| setTimeout(async()=>{ | |
| if(!state.playing) return; | |
| state.tracker.position=1; await resetScoreFollower(); syncExpectedMicNote(); | |
| NPTrace.log('test.abReset',{trial:i,condition,index:1,expectedMidi:44,atMs:Math.round(traceT(resetAt))}); | |
| },Math.max(0,(resetAt-ctx.currentTime)*1000)); | |
| const acc=[[60,0],[63,.5],[68,1]]; | |
| NPTrace.log('test.abTrial',{trial:i,condition,atMs:Math.round(traceT(at)),soloAtMs:Math.round(traceT(at+offset)), | |
| accompaniment:acc.map(([m,dt])=>({midi:m,atMs:Math.round(traceT(at+dt))}))}); | |
| for(const [m,dt] of acc) { | |
| playWebAudioSampleNote(m,${volume},'piano',{outputRole:'accompaniment',startAt:at+dt,duration:.42,suppressPedalResonance:true}); | |
| _scoreFollower?.accompanimentFired({pitches:[m],atMs:performanceTimeForAudioContextTime(ctx,at+dt),durMs:420,beat:null}); | |
| NPTrace.log('audio.note',{midi:m,name:pitchName(m),role:'accompaniment',instrument:'piano',path:'web-audio-sample',sent:true,velocity:${volume},audioTime:+(at+dt).toFixed(4),probe:true,trial:i,condition}); | |
| } | |
| if(condition==='B') { | |
| playWebAudioSampleNote(44,.6,'piano',{outputRole:'solo',startAt:at+offset,duration:.42,suppressPedalResonance:true}); | |
| NPTrace.log('audio.note',{midi:44,name:'G#2',role:'solo',instrument:'piano',path:'web-audio-sample',sent:true,velocity:.6,audioTime:+(at+offset).toFixed(4),probe:true,trial:i,condition}); | |
| } | |
| } | |
| return {order, durationMs:Math.ceil((start+(n-1)*spacing+3-ctx.currentTime)*1000)}; | |
| })()`); | |
| console.log(JSON.stringify(schedule)); | |
| await sleep(schedule.durationMs); | |
| await c.evaluate(`NPTrace.log('test.abEnd',{order:${JSON.stringify(order)}})`); | |
| } finally { | |
| try { | |
| if (recording) { | |
| const data = await c.evaluate(`(async()=>{const c=window.__npPairedCapture;if(c.recorder.state!=='inactive')await new Promise((ok,bad)=>{c.recorder.onstop=ok;c.recorder.onerror=bad;c.recorder.stop()});const b=new Blob(c.chunks,{type:c.mime});return await new Promise((ok,bad)=>{const r=new FileReader();r.onload=()=>ok(r.result);r.onerror=bad;r.readAsDataURL(b)})})()`,30000); | |
| fs.writeFileSync(out+'.paired.webm',Buffer.from(data.split(',')[1],'base64'),{flag:'wx'}); | |
| console.log('Saved paired mic/accompaniment audio beside trace'); | |
| } | |
| if (started) { | |
| await c.evaluate('_skipAutoEnabled = true; stopPlaying("gsharp-ab-probe")'); | |
| const trace=await c.evaluate('JSON.stringify(NPTrace.toObject())'); | |
| fs.writeFileSync(out,trace,{flag:'wx'}); console.log('Saved',out); | |
| } | |
| } finally { | |
| try { await c.evaluate('window.__npPairedCapture=null;window.__npReferenceBus=null;window.__npReferenceRole=null'); } catch (_) {} | |
| c.close(); | |
| } | |
| } | |