Skip to content
Closed
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Drum-part picker (feedpak 1.17.0 "drums as arrangements").** When a song
carries several drum charts, a **Drum part** selector appears beside the
arrangement switcher (advanced settings) so a player can choose which drummer
to play. Selecting one re-streams that part's tab over the highway WS
(`?drum_part=<id>`, mirroring the arrangement switch); the choice persists
across an arrangement change, and the picker reflects the server's
authoritative part (unknown/absent selection falls back to the primary). The
row hides for single-drum and non-drum songs, so nothing changes there. Builds
on the loader below; no plugin change needed — the drum renderer just draws
whatever tab streams.
- **Multiple drum parts (feedpak 1.17.0 "drums as arrangements").** The sloppak
loader now reads `type: drums` arrangement entries carrying per-arrangement
`drum_tab` file pointers — a song can ship several drum charts (a second
Expand Down
33 changes: 30 additions & 3 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,7 @@ window.feedBack.on('song:ready', () => {
let _arrBusyGen = 0;
let _arrBusyTimeout = null;

async function changeArrangement(index) {
async function changeArrangement(index, drumPart) {
if (currentFilename) {
// Tear down any pending fresh-load credits before switching: the
// no-count-in hold timer would otherwise fire togglePlay() against the
Expand Down Expand Up @@ -1276,11 +1276,38 @@ async function changeArrangement(index) {
_resetSectionPracticeLog();
invalidateParentCount();

window.highway.reconnect(currentFilename, index);
// Carry the selected drum part across the re-stream. An explicit
// `drumPart` (a drum-part switch, from changeDrumPart) wins; otherwise
// preserve the current picker selection so an ARRANGEMENT switch keeps
// the chosen part (drum parts are song-level, not per-arrangement).
const part = drumPart !== undefined
? drumPart
: (document.getElementById('drum-part-select')?.value || '');
window.highway.reconnect(currentFilename, index, part);
window.feedBack.emit('arrangement:changed', { index, filename: currentFilename });
}
}

// Switch which drum part plays (feedpak 1.17.0 "drums as arrangements"). A part
// switch re-streams the same song with a different drum tab — the same
// transition as an arrangement switch — so it delegates to changeArrangement
// with the CURRENT arrangement held and the new part applied. Wired to
// #drum-part-select's onchange; the select is populated + shown by
// highway.js's song_info handler only when the song has 2+ drum parts.
async function changeDrumPart(partId) {
if (!currentFilename) return;
let index = 0;
const si = window.highway && typeof window.highway.getSongInfo === 'function'
? window.highway.getSongInfo() : null;
if (si && typeof si.arrangement_index === 'number' && si.arrangement_index >= 0) {
index = si.arrangement_index;
} else {
const arrSel = document.getElementById('arr-select');
if (arrSel && arrSel.value !== '') index = Number(arrSel.value) || 0;
}
return changeArrangement(index, partId);
}

// Restart the current song from the beginning (or from loop A when an A–B
// loop is armed). Uses the canonical _audioSeek funnel only — never touches
// audio.currentTime directly and never reloads via playSong().
Expand Down Expand Up @@ -2325,7 +2352,7 @@ configureHost({
Object.assign(window, {
_confirmDialog, _getArrangementNamingMode, _libraryLocalFilename, _librarySongArtUrl,
_librarySongId, _onHeaderClick, _onNamingModeChange, _trapFocusInModal,
changeArrangement, checkPluginUpdates, clearLibFilters, clearLoop,
changeArrangement, changeDrumPart, checkPluginUpdates, clearLibFilters, clearLoop,
deleteSelectedLoop, esc, exportDiagnostics, exportSettings, filterFavorites,
filterLibrary, fullRescanLibrary, goFavPage, handleSliderInput,
hideScanBanner, importSettings, loadPlugins, loadSavedLoop,
Expand Down
47 changes: 46 additions & 1 deletion static/highway.js
Original file line number Diff line number Diff line change
Expand Up @@ -2298,6 +2298,31 @@ function createHighway() {
sel.appendChild(opt);
}
}
// Drum-part picker (feedpak 1.17.0 "drums as
// arrangements"): a song can carry several drum
// charts. Populate the picker beside the
// arrangement switcher; show it only when there
// are 2+ parts to choose between. `drum_parts`
// is always present (empty for non-drum songs),
// so a single-drum / no-drum song hides it. The
// currently-streaming part is marked selected by
// the `drum_tab` handler below (authoritative
// `part_id`), so we don't guess here.
{
const dpSel = document.getElementById('drum-part-select');
if (dpSel) {
const parts = Array.isArray(msg.drum_parts) ? msg.drum_parts : [];
dpSel.textContent = '';
for (const p of parts) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name || p.id;
dpSel.appendChild(opt);
}
const dpRow = document.getElementById('v3-drum-part-row');
if (dpRow) dpRow.classList.toggle('hidden', parts.length <= 1);
}
}
}
// Plugin context API — broadcast current song state
if (window.feedBack) {
Expand Down Expand Up @@ -2380,7 +2405,22 @@ function createHighway() {
name: (typeof msg.name === 'string' && msg.name) ? msg.name : 'Drums',
kit: Array.isArray(msg.kit) ? msg.kit : [],
hits: [],
// Which drum part this stream carries (feedpak
// 1.17.0). Present only for multi-part packs;
// null otherwise. Plugins can read it via
// bundle.drumTab.part_id.
part_id: (typeof msg.part_id === 'string' && msg.part_id) ? msg.part_id : null,
};
// Reflect the authoritative streaming part in the
// picker (the server resolves an unknown/absent
// selection to the primary, so this keeps the
// dropdown honest even after a fallback).
if (hwState.drumTab.part_id) {
const dpSel = document.getElementById('drum-part-select');
if (dpSel && dpSel.value !== hwState.drumTab.part_id) {
dpSel.value = hwState.drumTab.part_id;
}
}
break;
case 'drum_hits':
if (hwState.drumTab && Array.isArray(msg.data)) {
Expand Down Expand Up @@ -2773,7 +2813,7 @@ function createHighway() {
localStorage.setItem('showFingerHints', String(hwState._showFingerHints));
},

reconnect(filename, arrangement) {
reconnect(filename, arrangement, drumPart) {
// Close old WS but keep audio + animation running
if (hwState.ws) { hwState.ws.close(); hwState.ws = null; }
hwState.ready = false;
Expand All @@ -2799,6 +2839,11 @@ function createHighway() {
_resetChordRenderState();
const wsParams = new URLSearchParams();
if (arrangement !== undefined) wsParams.set('arrangement', arrangement);
// Multiple drum parts (feedpak 1.17.0 "drums as arrangements"):
// carry the selected part id so the WS streams ITS drum tab. Empty
// / undefined → the primary part (server default), i.e. today's
// one-drum behavior for any pack the picker never touched.
if (drumPart) wsParams.set('drum_part', drumPart);
let namingMode = 'smart';
if (typeof window._getArrangementNamingMode === 'function') {
const v = window._getArrangementNamingMode();
Expand Down
4 changes: 4 additions & 0 deletions static/v3/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,10 @@ <h3>Gameplay Settings</h3>
<button id="arr-default-pin" type="button" onclick="pinCurrentArrangementDefault()" aria-pressed="false" aria-label="Select an arrangement to make it the default" class="v3-pop-pin" title="Select an arrangement to make it the default">☆</button>
</span>
</div>
<div class="v3-pop-row hidden" id="v3-drum-part-row">
<span class="v3-pop-label">Drum part</span>
<select id="drum-part-select" onchange="changeDrumPart(this.value)" class="v3-pop-select max-w-[130px]" title="Which drum chart to play — a song can carry several"></select>
</div>
<div class="v3-pop-row">
<span class="v3-pop-label" id="mastery-slider-label">Difficulty</span>
<span class="flex items-center gap-2">
Expand Down
2 changes: 1 addition & 1 deletion tests/js/highway_chart_transform.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ test('anchor zoom helpers read the staged anchors first', () => {
test('init and reconnect clear the stage but keep the provider', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const initBody = extractBlock(src, 'init(canvasEl, container)');
const reconnectBody = extractBlock(src, 'reconnect(filename, arrangement)');
const reconnectBody = extractBlock(src, 'reconnect(filename, arrangement, drumPart)');
assert.match(initBody, /_clearChartTransformStage\(\);/, 'init clears the stage');
assert.match(reconnectBody, /_clearChartTransformStage\(\);/, 'reconnect clears the stage');
assert.ok(!/init\([\s\S]{0,2000}_xfProvider = null/.test(src.slice(src.indexOf('const api = {'))),
Expand Down