diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b0a27..2df94de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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=`, 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 diff --git a/static/app.js b/static/app.js index d306259..6aef021 100644 --- a/static/app.js +++ b/static/app.js @@ -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 @@ -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(). @@ -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, diff --git a/static/highway.js b/static/highway.js index 1913078..a7b0507 100644 --- a/static/highway.js +++ b/static/highway.js @@ -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) { @@ -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)) { @@ -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; @@ -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(); diff --git a/static/v3/index.html b/static/v3/index.html index 666ae0b..3574307 100644 --- a/static/v3/index.html +++ b/static/v3/index.html @@ -1194,6 +1194,10 @@ +
Difficulty