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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
per-bin min/max/RMS cache (~3 ms/bin) and aggregated per pixel column so the
shape stays sharp at any zoom. Same lane, same seek behaviour — no layout or
API change. Pure helper `_buildWaveformPeaks` covered by tests.
- **Point people at Tempo Map when an import drifts from the audio.** Auto-sync can only approximate a *human* performance between sync points, so an imported chart often starts aligned and then drifts (falls behind / gets ahead) — and the #1 confusion in the field is not realizing the **beatmap is editable at all**. The post-import status now names the fix in both cases: per-bar `warp` and the scalar `offset` fallback (GP3/4/5 repeats/jumps, degenerate anchors) each end with *"Drifting from the recording? Open 🎵 Tempo Map to drag the beat grid onto the audio."*, and the toolbar button's tooltip now reads "…fix a chart drifting from the audio — drag the beat grid, edit BPM & time signatures." No behavior change to the import itself; the Tempo Map editor (drag sync points, per-measure BPM + time signature, insert/delete, drum-vs-all ride scope) already existed. Tests: `tests/tempo_map_guidance.test.js` (`_syncAppliedMessagePure`).

### Added
- **Import a Guitar Pro guitar/bass track into the song you're editing —
Expand Down
37 changes: 25 additions & 12 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -8911,17 +8911,12 @@ window.editorDoCreate = async () => {
if (data.error) { status.textContent = 'Error: ' + data.error; btn.disabled = false; return; }

await window.editorApplyCreateResult(data);
// Surface how the sync landed: 'warp' = chart follows the recording
// bar-by-bar; 'offset' = the server fell back to the scalar offset,
// with sync_reason saying why ('repeats' is the only user-actionable
// cause; everything else gets a generic message).
if (typeof setStatus === 'function' && data.sync_applied === 'warp') {
setStatus('Imported with per-bar audio sync.');
} else if (typeof setStatus === 'function' && data.sync_applied === 'offset') {
setStatus(data.sync_reason === 'repeats'
? 'This file uses repeats/jumps, which per-bar sync can’t map yet — applied start offset only.'
: 'Per-bar sync could not be applied to this import — applied start offset only.');
}
// Surface how the sync landed ('warp' = chart follows the recording
// bar-by-bar; 'offset' = server fell back to the scalar offset), and —
// for either — point the user at the Tempo Map editor to fine-tune any
// residual drift by hand. See _syncAppliedMessagePure.
const _syncMsg = _syncAppliedMessagePure(data.sync_applied, data.sync_reason);
if (_syncMsg && typeof setStatus === 'function') setStatus(_syncMsg);
} catch (e) {
status.textContent = 'Import failed: ' + e.message;
btn.disabled = false;
Expand Down Expand Up @@ -11624,6 +11619,24 @@ function _tempoMapHudTextPure(measureCount, width) {
}
return `Tempo Map - ${n} measures - drag poles to retime - right-click sync point: BPM / signature/delete - right-click grid: insert`;
}
// Status shown right after an audio-synced GP import. Names the Tempo Map tool
// so a drifting chart has an obvious, discoverable fix — the #1 confusion in the
// field is not knowing the beatmap is editable at all (auto-sync can only
// approximate a human performance between sync points, so some drift is
// expected and is meant to be fine-tuned by hand).
function _syncAppliedMessagePure(syncApplied, syncReason) {
const tip = ' Drifting from the recording? Open 🎵 Tempo Map to drag the beat grid onto the audio.';
if (syncApplied === 'warp') {
return 'Imported with per-bar audio sync.' + tip;
}
if (syncApplied === 'offset') {
const base = syncReason === 'repeats'
? 'This file uses repeats/jumps, which per-bar sync can’t map yet — applied start offset only.'
: 'Per-bar sync could not be applied to this import — applied start offset only.';
return base + tip;
}
return '';
}
/* @pure:tempo-map-guidance:end */

/* @pure:tempo-sync-inspector:start */
Expand Down Expand Up @@ -11794,7 +11807,7 @@ function _ensureTempoMapButton() {
btn.type = 'button';
btn.textContent = '🎵 Tempo Map';
btn.className = 'px-3 py-1 bg-dark-600 hover:bg-dark-500 rounded text-xs font-medium hidden';
btn.title = 'Open Tempo Map to edit sync points, BPM, and time signatures';
btn.title = 'Open Tempo Map to fix a chart drifting from the audio — drag the beat grid, edit BPM & time signatures';
btn.onclick = () => {
// Finalize any in-progress canvas drag before switching
// modes — commit a moved sync-point / drum drag (don't
Expand Down
26 changes: 25 additions & 1 deletion tests/tempo_map_guidance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ if (!m) {
}

const api = new Function(
'"use strict";' + m[0] + '\nreturn { _tempoMapHudTextPure };'
'"use strict";' + m[0] + '\nreturn { _tempoMapHudTextPure, _syncAppliedMessagePure };'
)();

let pass = 0;
Expand Down Expand Up @@ -46,5 +46,29 @@ t('normalizes invalid measure counts to zero', () => {
assert.ok(api._tempoMapHudTextPure('bad', 960).includes('0 measures'));
});

t('warp import message points at the Tempo Map fine-tune path', () => {
const text = api._syncAppliedMessagePure('warp', null);
assert.ok(text.includes('per-bar audio sync'));
assert.ok(text.includes('Tempo Map'));
assert.ok(/drift/i.test(text));
});

t('offset (repeats) message explains the fallback and points at Tempo Map', () => {
const text = api._syncAppliedMessagePure('offset', 'repeats');
assert.ok(/repeats\/jumps/.test(text));
assert.ok(text.includes('Tempo Map'));
});

t('offset (other) message is generic and points at Tempo Map', () => {
const text = api._syncAppliedMessagePure('offset', 'anchors');
assert.ok(text.includes('could not be applied'));
assert.ok(text.includes('Tempo Map'));
});

t('no message when no audio sync was applied', () => {
assert.strictEqual(api._syncAppliedMessagePure(undefined, undefined), '');
assert.strictEqual(api._syncAppliedMessagePure('', ''), '');
});

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