Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions screen.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<svg id="editor-play-icon" class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
</button>
<span id="editor-time-display" class="text-xs text-gray-500 font-mono w-20">0:00 / 0:00</span>
<span id="editor-measure-display" class="text-xs text-gray-500 font-mono w-16" title="Current measure and time signature">M-- --</span>
<!-- Loop selected bars on the 3D highway (drag the bottom beat bar to
select a range first). Returns to this exact edit position on exit. -->
<button id="editor-loop3d-btn" onclick="editorLoopIn3D()" class="px-3 py-1 bg-accent hover:bg-accent-light rounded text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed" disabled title="Select some notes or set a loop region first">▶ Loop in 3D</button>
Expand Down
48 changes: 48 additions & 0 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -4681,6 +4681,16 @@ function updatePlayIcon() {
}
}

function updateMeasureDisplay() {
const el = document.getElementById('editor-measure-display');
if (!el) return;
const selectedIdx = S.tempoMapMode ? S.tempoSel : -1;
const r = _editorMeasureSignatureReadoutPure(S.beats || [], S.cursorTime || 0, selectedIdx);
el.textContent = r.label;
el.title = r.measure === null
? 'No measure grid available'
: `Measure ${r.measure}, time signature ${r.numerator}/${r.denominator}`;
}
function updateTimeDisplay() {
const el = document.getElementById('editor-time-display');
if (!el) return;
Expand All @@ -4690,6 +4700,7 @@ function updateTimeDisplay() {
return m + ':' + String(s).padStart(2, '0');
};
el.textContent = fmt(S.cursorTime) + ' / ' + fmt(S.duration);
updateMeasureDisplay();
}

// ════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -5973,6 +5984,42 @@ function _tempoResolvedMeasureIdx() {
return measures[0].i;
}


/* @pure:measure-readout:start */
function _editorMeasureSignatureReadoutPure(beats, time, selectedIdx) {
if (!Array.isArray(beats) || !beats.length) return { label: 'M-- --', measure: null, numerator: null, denominator: null };
let idx = Number.isInteger(selectedIdx) && selectedIdx >= 0 && selectedIdx < beats.length && beats[selectedIdx] && beats[selectedIdx].measure > 0
? selectedIdx
: -1;
const t = Number.isFinite(Number(time)) ? Number(time) : 0;
if (idx < 0) {
for (let i = 0; i < beats.length; i++) {
const b = beats[i];
if (!b || b.measure <= 0) continue;
if ((Number(b.time) || 0) <= t + 1e-6) idx = i;
else break;
}
}
if (idx < 0) idx = beats.findIndex(b => b && b.measure > 0);
if (idx < 0) return { label: 'M-- --', measure: null, numerator: null, denominator: null };
const downbeat = beats[idx];
let nextIdx = beats.length;
for (let i = idx + 1; i < beats.length; i++) {
if (beats[i] && beats[i].measure > 0) { nextIdx = i; break; }
}
let numerator = Math.max(1, nextIdx - idx);
if (nextIdx === beats.length) {
let prevIdx = -1;
for (let i = idx - 1; i >= 0; i--) {
if (beats[i] && beats[i].measure > 0) { prevIdx = i; break; }
}
if (prevIdx >= 0) numerator = Math.max(1, idx - prevIdx);
}
const den = _tempoNormalizeDenominatorPure(downbeat.den);
const measure = downbeat.measure;
return { label: `M${measure} ${numerator}/${den}`, measure, numerator, denominator: den };
}
/* @pure:measure-readout:end */
function updateBPMDisplay() {
const el = document.getElementById('editor-bpm');
if (!el || S.beats.length < 2) return;
Expand Down Expand Up @@ -6000,6 +6047,7 @@ function updateTempoSigDisplay() {
}
if (numEl) numEl.value = String(_tempoMeasureBeatCount(d));
if (denEl) denEl.value = String(_tempoMeasureDenominator(d));
updateMeasureDisplay();
}

// Defer a `resizeCanvas` until layout has settled — used when a
Expand Down
66 changes: 66 additions & 0 deletions tests/measure_readout.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';
/*
* Measure/signature readout helper tests for screen.js.
*
* Run: node tests/measure_readout.test.js
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');

const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
const ts = src.match(/\/\* @pure:tempo-map-timesig:start \*\/[\s\S]*?\/\* @pure:tempo-map-timesig:end \*\//);
const mr = src.match(/\/\* @pure:measure-readout:start \*\/[\s\S]*?\/\* @pure:measure-readout:end \*\//);
if (!ts || !mr) {
console.error('FAIL: required pure blocks not found in screen.js');
process.exit(1);
}

const api = new Function(
'"use strict";' + ts[0] + '\n' + mr[0] + '\nreturn { _editorMeasureSignatureReadoutPure };'
)();

let pass = 0;
let fail = 0;
function t(name, fn) {
try { fn(); pass++; console.log(' ok ' + name); }
catch (e) { fail++; console.error(' FAIL ' + name + ': ' + e.message); }
}

const beats = [
{ time: 0, measure: 1, den: 4 },
{ time: 1, measure: -1 },
{ time: 2, measure: -1 },
{ time: 3, measure: -1 },
{ time: 4, measure: 2, den: 8 },
{ time: 5, measure: -1 },
{ time: 6, measure: -1 },
{ time: 7, measure: -1 },
{ time: 8, measure: 3 },
{ time: 9, measure: -1 },
{ time: 10, measure: -1 },
{ time: 11, measure: -1 },
];

t('follows the cursor measure in note view', () => {
assert.deepStrictEqual(api._editorMeasureSignatureReadoutPure(beats, 4.25, -1), {
label: 'M2 4/8', measure: 2, numerator: 4, denominator: 8,
});
});

t('prefers selected tempo sync point over cursor time', () => {
assert.strictEqual(api._editorMeasureSignatureReadoutPure(beats, 1.2, 4).label, 'M2 4/8');
});

t('falls back to denominator 4 when no denominator is authored', () => {
assert.strictEqual(api._editorMeasureSignatureReadoutPure(beats, 8.1, -1).label, 'M3 4/4');
});

t('returns empty label shape when there is no downbeat grid', () => {
assert.deepStrictEqual(api._editorMeasureSignatureReadoutPure([{ time: 0, measure: -1 }], 0, -1), {
label: 'M-- --', measure: null, numerator: null, denominator: null,
});
});

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);