diff --git a/screen.html b/screen.html
index e1736ca6..d9d63f4a 100644
--- a/screen.html
+++ b/screen.html
@@ -49,6 +49,7 @@
0:00 / 0:00
+ M-- --
diff --git a/screen.js b/screen.js
index 7882f4f6..c8f5c710 100644
--- a/screen.js
+++ b/screen.js
@@ -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;
@@ -4690,6 +4700,7 @@ function updateTimeDisplay() {
return m + ':' + String(s).padStart(2, '0');
};
el.textContent = fmt(S.cursorTime) + ' / ' + fmt(S.duration);
+ updateMeasureDisplay();
}
// ════════════════════════════════════════════════════════════════════
@@ -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;
@@ -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
diff --git a/tests/measure_readout.test.js b/tests/measure_readout.test.js
new file mode 100644
index 00000000..259e4f1b
--- /dev/null
+++ b/tests/measure_readout.test.js
@@ -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);
\ No newline at end of file