mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 02:14:29 +00:00
feat(v3): promote Audio Engine to a first-class sidebar item
Add the bundled desktop `audio_engine` plugin (native audio I/O, VST
hosting, amp modeling, pitch detection) as a dedicated v3 sidebar entry
in the HOME group, immediately below Settings, via the existing
PROMOTED_PLUGINS mechanism in static/v3/shell.js. Gated on the plugin
being installed (renderPromotedNav checks /api/plugins), so it shows
only in desktop builds. Adds a new `audio` waveform icon; the displayed
label comes from the plugin manifest nav.label ("Audio").
Test: tests/browser/audio-engine-nav.spec.ts (placement after Settings,
inside the HOME group, links to the plugin screen).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8f0625e1f7
commit
ae0857bd97
@@ -23,6 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
(`renderPromotedNav` checks `/api/plugins`), so it appears only when the
|
||||
editor is loaded. The displayed label comes from the plugin's manifest
|
||||
`nav.label`.
|
||||
- **"Audio Engine" promoted to a first-class v3 sidebar item.** The bundled
|
||||
desktop audio plugin (`id: audio_engine` — native audio I/O, VST hosting,
|
||||
amp modeling, pitch detection) now gets its own sidebar entry in the **HOME**
|
||||
group, immediately below **Settings**, via the existing `PROMOTED_PLUGINS`
|
||||
mechanism in `static/v3/shell.js` (new `audio` waveform icon), instead of
|
||||
being reachable only through the generic Plugins gallery. Gated on the plugin
|
||||
actually being installed (`renderPromotedNav` checks `/api/plugins`), so it
|
||||
appears only in desktop builds where the plugin is bundled. The displayed
|
||||
label comes from the plugin's manifest `nav.label` (`"Audio"`). Test:
|
||||
`tests/browser/audio-engine-nav.spec.ts`.
|
||||
- **Guitar Pro → notation importer (`lib/gp2notation.py`)** (feedBack#825 WS4b, epic #828). Piano/keys tracks imported from Guitar Pro (GPIF: `.gpx` GP6 / `.gp` GP7-8) now produce real Sloppak Notation Format data (sloppak-spec §5.3) alongside the `midi = string*24 + fret` guitar wire encoding. `gp2rs_gpx.convert_file` writes a `<stem>.notation.json` sidecar next to each keys arrangement XML (best-effort — a notation bug never breaks the RS-XML conversion), and `gp2notation.attach_notation_to_sloppak()` is the assembly-side helper that renames it into `notation_<id>.json` + adds the per-arrangement `notation:` manifest sub-key. Voice→staff routing salvages the logic from PR #703 (whose `stf` wire-field approach this supersedes): GP voice position 0 → `rh` staff (`G2`), positions ≥ 1 → `lh` (`F4`); a forced-LH track (the merged `Piano LH` partner from `_find_piano_pairs`, or a standalone track named `… LH`) routes everything to `lh` — preserving authored hand crossings instead of inferring hands from pitch. Emits measures with absolute `t` from the bar-indexed tempo map, change-only `ts`/`tempo`/`ks`, and `beat_groups` for compound/irregular meters (6/8 → `[3,3]`, 9/8 → `[3,3,3]`, 5/8 → `[2,3]`, 7/8 → `[2,2,3]` — cf. the feedBack#261 denominator pitfalls); beats carry `dur`/`dot`/`tu`/`rest` from GP rhythms and notes carry absolute `midi` (String+Fret resolves via the string template's concert pitches, Tone+Octave via `(octave+1)*12 + step`) with `tied` continuations kept as real beats (engraving needs the tied notehead — unlike the RS-XML walk, which drops them and extends sustain). Timing reuses the `gp2rs_gpx` machinery (bar-indexed tempo map, per-beat rhythm durations, `_note_midi`) so notation lines up with the RS XML the highway plays — with one deliberate divergence: double dots advance time ×1.75 (vs. the RS-XML walk's single-dot ×1.5 approximation) so a written `dot: 2` agrees with the emitted beat times; sharing the walk itself stays tracked in feedBack#618. Tests: `tests/test_gp2notation.py`.
|
||||
- **Legacy keys → notation lifter (`scripts/lift_keys_notation.py`)** (feedBack#825 WS4c, epic #828). One-time batch converter that lifts existing **directory-form** piano/keys sloppaks from the legacy guitar wire encoding (`midi = s*24 + f`) into real Sloppak Notation Format files (sloppak-spec §5.3). Candidates are arrangements whose name matches `\b(keys|piano|keyboard|synth)\b` (case-insensitive); each gets a `notation_<id>.json` plus the per-arrangement `notation:` manifest sub-key. Measures derive from the song-level `beats` downbeats (`measure >= 0`; `song_timeline.json` preferred, first-arrangement fallback), with per-measure tempo from downbeat spacing (emitted only on a > 1 BPM change). Durations come from the wire sustain (`sus`, legacy `l` alias) when present, else the gap to the next onset in the same hand — quantized to the nearest plain/single-dotted `{1,2,4,8,16,32}` denominator at the local tempo, floored at a 32nd. Hands are split heuristically: onsets within 10 ms form a group; a group spanning > 12 semitones splits at its largest internal interval gap (low side → `lh`), otherwise the whole group goes by mean pitch vs middle C — single-staff output when everything lands on one hand. Idempotent (arrangements already carrying `notation:` are skipped; an orphan `notation_<id>.json` without the manifest key is refused, not overwritten) with `--dry-run` support; every payload is checked via `notation.validate_notation` before write. Honest caveat: the manifest is round-tripped through PyYAML (`safe_load` + `safe_dump(sort_keys=False)`) — key order survives, YAML comments/custom formatting do not (the script warns when comments are present). Zip-form `.sloppak` files are reported and skipped. Tests: `tests/test_lift_keys_notation.py`.
|
||||
- **Notation schema v1 freeze — completeness batch** (feedBack#822, epic #828). Adds the low-hanging-fruit fields ahead of content production: top-level credits `rights`/`lyricist`/`arranger`; measure `pickup` (anacrusis); beat `arp` (arpeggiate), `ferm` (fermata), and **typed grace notes** — `grace: "a"` (acciaccatura, MusicXML `grace/@slash=yes`) / `"p"` (appoggiatura); note `stem` (`"up"`/`"down"` force). Pedal is settled as the existing `spd`/`sph`/`spu` trio with a documented MusicXML `<pedal start|change|stop>` mapping — no separate `ped` field. A new "v1 non-features" spec subsection pins the accepted limitations (microtonal, figured bass, mid-measure key/time/clef changes, `ott`/`barline`/ornaments/`trem`/`glis`) as additive-v1.x territory. `lib/notation.py` gains the `GRACE_TYPES`, `STEM_DIRECTIONS`, and `DYNAMICS` vocabularies; the validator stays permissive by design.
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
{ key: 'virtuoso', screen: 'plugin-virtuoso', label: 'Virtuoso - Practice', group: null, icon: 'target' },
|
||||
{ key: 'rig_builder', screen: 'plugin-rig_builder', label: 'Rig Builder', group: null, icon: 'amp' },
|
||||
{ key: 'editor', screen: 'plugin-editor', label: 'Song Editor', group: null, icon: 'edit' },
|
||||
{ key: 'audio_engine', screen: 'plugin-audio_engine', label: 'Audio', group: null, icon: 'audio' },
|
||||
// Not in the sidebar groups, but routable (profile badge → here).
|
||||
{ key: 'profile', screen: 'v3-profile', label: 'Profile', group: null, icon: 'user' },
|
||||
];
|
||||
@@ -61,6 +62,7 @@
|
||||
{ navKey: 'virtuoso', pluginId: 'virtuoso', slotId: 'v3-nav-virtuoso', anchorAfter: 'feedbarcade' },
|
||||
{ navKey: 'rig_builder', pluginId: 'rig_builder', slotId: 'v3-nav-rig-builder', anchorAfter: 'saved' },
|
||||
{ navKey: 'editor', pluginId: 'editor', slotId: 'v3-nav-editor', anchorAfter: 'songs' },
|
||||
{ navKey: 'audio_engine', pluginId: 'audio_engine', slotId: 'v3-nav-audio-engine', anchorAfter: 'settings' },
|
||||
];
|
||||
const TOPBAR_KEYS = ['home', 'songs', 'plugins', 'settings'];
|
||||
const SIDEBAR_GROUPS = ['HOME', 'LIBRARY'];
|
||||
@@ -82,6 +84,7 @@
|
||||
amp: 'M4 5h16a1 1 0 011 1v12a1 1 0 01-1 1H4a1 1 0 01-1-1V6a1 1 0 011-1zm11 4a3 3 0 100 6 3 3 0 000-6zM6.5 8.5h.01M9 8.5h.01',
|
||||
target: 'M12 3a9 9 0 100 18 9 9 0 000-18zm0 4a5 5 0 100 10 5 5 0 000-10zm0 4a1 1 0 100 2 1 1 0 000-2z',
|
||||
edit: 'M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7M18.5 2.5a2.1 2.1 0 013 3L12 15l-4 1 1-4 9.5-9.5z',
|
||||
audio: 'M11 5L6 9H3v6h3l5 4V5zM15.5 8.5a5 5 0 010 7M19 6a9 9 0 010 12',
|
||||
};
|
||||
function iconSvg(name) {
|
||||
const d = ICONS[name] || ICONS.disc;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// The Audio Engine plugin (`audio_engine`) is bundled only in the desktop
|
||||
// build, so stub /api/plugins to make it present here. Asserts it renders as a
|
||||
// promoted sidebar entry in the v3 "HOME" group, immediately AFTER the Settings
|
||||
// entry, and routes to its plugin screen.
|
||||
const STUB_PLUGINS = [
|
||||
{ id: 'audio_engine', name: 'Audio Engine', nav: { label: 'Audio', screen: 'audio-engine', icon: '🎸' }, status: 'ready', has_screen: false, has_settings: false },
|
||||
];
|
||||
|
||||
test('audio_engine is promoted into the HOME nav group after Settings', async ({ page }) => {
|
||||
await page.route('**/api/plugins', route =>
|
||||
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(STUB_PLUGINS) }));
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('#v3-nav a[data-v3-nav]', { timeout: 15000 });
|
||||
|
||||
// The promoted slot is filled with the Audio Engine link.
|
||||
const aeLink = page.locator('#v3-nav-audio-engine a[data-v3-nav="audio_engine"]');
|
||||
await expect(aeLink).toHaveCount(1, { timeout: 10000 });
|
||||
await expect(aeLink).toContainText('Audio');
|
||||
|
||||
// It sits inside the HOME group, immediately after the Settings entry.
|
||||
const order = await page.evaluate(() => {
|
||||
const nav = document.getElementById('v3-nav');
|
||||
const links = Array.from(nav.querySelectorAll('a[data-v3-nav]'));
|
||||
const keys = links.map(a => a.getAttribute('data-v3-nav'));
|
||||
const settingsIdx = keys.indexOf('settings');
|
||||
const aeIdx = keys.indexOf('audio_engine');
|
||||
// renderSidebar emits one wrapper <div> per group as a direct child of
|
||||
// #v3-nav: [ heading <div>GROUP</div>, items <div>…links…</div> ]. Walk up
|
||||
// from the link to that group wrapper and read its heading (first child) —
|
||||
// structural, so it doesn't depend on the heading's CSS classes.
|
||||
function groupOf(el) {
|
||||
let node = el;
|
||||
while (node && node.parentElement && node.parentElement !== nav) node = node.parentElement;
|
||||
const heading = node && node.firstElementChild;
|
||||
return heading ? heading.textContent.trim() : null;
|
||||
}
|
||||
return { settingsIdx, aeIdx, group: aeIdx >= 0 ? groupOf(links[aeIdx]) : null };
|
||||
});
|
||||
expect(order.settingsIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(order.aeIdx).toBe(order.settingsIdx + 1);
|
||||
expect(order.group).toBe('HOME');
|
||||
|
||||
// It targets the plugin's own screen.
|
||||
await expect(aeLink).toHaveAttribute('href', '#/audio_engine');
|
||||
});
|
||||
Reference in New Issue
Block a user