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

### Added

- **Save is now project persistence; Export to Library is publishing.** Importing
files creates only an unsaved editing session. A new project's first Save
chooses a `.feedpak` name and location through the native picker, later saves
reuse it, and no library file is created until the explicit Export to Library
action. In browsers without the native picker, Save downloads a copy but
keeps the project marked unsaved because completion cannot be verified.
Exporting no longer clears unsaved project changes. The editor's own Back
button now uses the Save / Don't Save / Cancel guard; complete shell navigation
and desktop app-exit guarding still require a cancellable host navigation/close contract.

- **The Legacy (EOF) profile is now a faithful port of EOF's pro-guitar keyset,
pinned to the reference.** F1 opens help, Ctrl+1-9 / Ctrl+0 / Ctrl+` set frets
beside the plain digits, the numpad places and jumps bookmarks (Ctrl+Numpad
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
The **Song Editor** plugin for [feedBack](https://github.com/got-feedback/feedback)
(plugin id `editor`) — a DAW-style timeline editor that turns a recording into
a playable feedBack chart: import a tab or start from audio, line the grid up
to the take, author every note, and build a `.feedpak` the whole ecosystem
plays.
to the take, author every note, save the editable `.feedpak` project, and
export it to the targets where it will be played.

![The Song Editor: the multitrack Tracks column at left, the waveform and beat grid up top, and a synced Guitar Pro chart laid out on the string lanes.](assets/guide/workspace.png)

**New here?** The **[User Guide](docs/USER-GUIDE.md)** (also in-app under
Help ▸ User Guide) walks the whole journey — create a project from a
recording, a chart, or both; fit the tempo map; chart the notes; add tracks,
stems, and drums; mix; and build the finished feedpak.
stems, and drums; mix; save the project; and export it to the library.

It is the largest plugin in the ecosystem. If you are here to work on it,
read this top-to-bottom once — the architecture section saves real time.
Expand Down Expand Up @@ -56,8 +56,12 @@ read this top-to-bottom once — the architecture section saves real time.
(candidate positions from the suggest-position resolver, click to assign),
a drum kit / pad strip (GM-mapped, MIDI-monitor capable), and advisory
playability/limb lints that name physical questions without ever blocking.
- **Build** — assembles the finished `.feedpak` through the host's core
libraries, so output packages are compatible everywhere.
- **Save and export are separate** — with the native file picker, Save preserves
the editable `.feedpak` project at the location you choose and reuses that
destination. Browser fallback downloads a copy but keeps the project marked
unsaved because download completion cannot be verified. **Export to Library**
assembles and publishes a playable library copy through the host's core
libraries; export never pretends that unsaved project edits were saved.

## Layout

Expand Down
43 changes: 24 additions & 19 deletions docs/USER-GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Song Editor — User Guide

The **Song Editor** turns a recording into a playable feedBack chart. You line a
grid up to the music, place the notes, mark the tempo and sections, and build a
`.feedpak` the rest of the app can practice against.
grid up to the music, place the notes, mark the tempo and sections, save the
editable `.feedpak` project, and export it where the rest of the app can use it.

This guide is the same content you get in-app from **Help ▸ User Guide**. It
covers the FeedBack shortcut profile (the default). Three other profiles remap
Expand All @@ -23,7 +23,7 @@ six steps, and this guide follows them in order:
3. **Chart the notes** (§4) — place, edit, and mark up every note and technique.
4. **Build out the tracks** (§5, §7) — more parts, keys with hands, drums, studio stems.
5. **Hear it** (§3) — guide claps, the metronome, loops, and the mixer console.
6. **Save and build** (§9) — `Ctrl+S` while you work; **Build** writes the `.feedpak`.
6. **Save and export** (§9) — `Ctrl+S` preserves the project; export publishes it.

---

Expand Down Expand Up @@ -81,8 +81,9 @@ A few sources arrive through their own doors, any time after creation:
- A **GoPlayAlong** sidecar brings bar→time sync points only (no notes) —
useful for lining a grid up fast.

> **Autosave is not a thing.** Nothing touches your library until you **Build**
> (§9). Save early with `Ctrl+S`.
> **Autosave is not a thing.** Importing creates an unsaved project, not a
> library item. Save early with `Ctrl+S`; nothing touches your library until
> you choose **Export to Library** (§9).

---

Expand Down Expand Up @@ -409,18 +410,21 @@ These live in the **Add** menu (Markers) and show in the inspector when selected

---

## 9. Save and build — the finish line

- **`Ctrl+S`** saves your working session. For an exportable packed song, the
first save can ask where to keep an external copy when your browser supports
a file picker; otherwise it saves straight to the library. Later saves reuse
the chosen location.
- **Build feedpak** (the toolbar button, or File ▸ Build feedpak) assembles
the finished **`.feedpak`** through the host's core libraries and writes it
to your library — this is the only step that changes what the rest of the
app sees. The pack carries everything you authored: every arrangement,
the tempo map, sections and phrases, techniques, keys notation with its
hand split, stems, tones, and art.
## 9. Save and export — the finish line

- **`Ctrl+S`** saves the editable `.feedpak` project. A new project asks for a
name and location through the native file picker; later saves reuse that
location. Opening an existing project and pressing Save updates it in place.
Saving never publishes a library copy and never asks you to review export
settings. In browsers without the native picker, Save downloads a copy but
keeps the project marked unsaved because download completion cannot be
verified; the desktop-native save contract is the durable path.
- **Export to Library** (the toolbar button, or File ▸ Export to Library)
assembles the current project through the host's core libraries and publishes
a playable copy to your library. Exporting is separate from saving: it does
not clear unsaved project changes. The exported pack carries every arrangement,
the tempo map, sections and phrases, techniques, keys notation with its
hand split, stems, tones, and art.

**Ready to build?** A quick pre-flight:

Expand All @@ -429,8 +433,9 @@ These live in the **Add** menu (Markers) and show in the inspector when selected
3. The lint flags you care about are addressed (they *advise*, never block).
4. Song details are filled (title, artist, art) — they're the library card.

Build again any time — rebuilding replaces the pack, and your working session
stays editable.
Export again any time to refresh the library copy. For quick edits that do not
need a new export, Save and close—the project remains editable and the library
copy simply stays at its previous version.

**Undo/redo is the safety net.** Every edit — including tempo moves, offset, and
imports — is undoable (`Ctrl+Z` / `Ctrl+Y`). If something looks wrong, undo it.
Expand Down
92 changes: 69 additions & 23 deletions routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4086,6 +4086,17 @@ async def export_editor_session(session_id: str):
session = sessions.get(session_id)
if not session:
return JSONResponse({"error": "No active session"}, 404)
saved_path = session.get("export_path")
if saved_path:
candidate = Path(saved_path).resolve()
if not candidate.is_file():
return JSONResponse(
{"error": "Saved feedpak is not a packed file"}, 409)
return FileResponse(
candidate,
media_type="application/zip",
filename=str(session.get("export_filename") or candidate.name),
)
filename = str(session.get("filename") or "")
dlc_dir = get_dlc_dir()
if not filename or not dlc_dir:
Expand Down Expand Up @@ -8652,17 +8663,21 @@ async def add_arrangement(data: dict):
async def build_song_endpoint(data: dict):
"""Build a song from the current create-mode session.

Always writes a native `.sloppak`. (the native `.archive`/custom-song export
has been removed — the sloppak container carries every arrangement,
including extended-range 7/8-string guitar and 5/6-string bass that
the old note-chart binary format couldn't.)
``destination: "session"`` writes a temporary `.feedpak` for Save or
Save As. Omitting it (or sending ``"library"``) preserves the
historical Export-to-Library behavior for older clients.
"""
session_id = data.get("session_id", "")
session = sessions.get(session_id)
if not session or not session.get("create_mode"):
return JSONResponse({"error": "No active create session"}, 400)
session["last_touched"] = time.time()

destination = data.get("destination", "library")
if destination not in ("library", "session"):
return JSONResponse(
{"error": "destination must be 'library' or 'session'"}, 400)

arrangements_data = data.get("arrangements", [])
beats = data.get("beats", [])
sections = data.get("sections", [])
Expand Down Expand Up @@ -8753,15 +8768,25 @@ def _build_sloppak():
raise RuntimeError(f"Audio track '{track['name']}' is unavailable")
extra_audio_tracks.append({**track, "path": str(track_path)})

dlc_dir = get_dlc_dir()
if not dlc_dir:
raise RuntimeError("DLC folder not configured")

title = meta.get("title", "Untitled")
artist = meta.get("artistName") or meta.get("artist", "Unknown")
safe_t = re.sub(r'[<>:"/\\|?*]', '_', title)
safe_a = re.sub(r'[<>:"/\\|?*]', '_', artist)
output = dlc_dir / f"{safe_t}_{safe_a}.feedpak"
suggested_name = (
f"{safe_t}_{safe_a}.feedpak" if safe_a
else f"{safe_t}.feedpak"
)
if destination == "session":
# One immutable package per save generation: /session/export
# may still be streaming the previous generation while a fast
# second Ctrl+S is assembled.
output = (Path(session["dir"])
/ f"project-save-{time.time_ns()}.feedpak")
else:
dlc_dir = get_dlc_dir()
if not dlc_dir:
raise RuntimeError("DLC folder not configured")
output = dlc_dir / suggested_name
return _write_sloppak_pak(
audio_file=audio_file,
art_path=_safe_storage_asset(art_path),
Expand Down Expand Up @@ -8793,24 +8818,45 @@ def _build_sloppak():
traceback.print_exc()
return JSONResponse({"error": str(e)}, 500)

# Record the built pack on the session so /session/export (the Save As
# external-copy source) can serve it. Builds land at the DLC root, so
# the bare name is the library-relative filename export resolves.
# Nothing else keys off `filename` for a create-mode session: /save is
# gated off above it, and save_as_sloppak only accepts archive
# sessions.
session["filename"] = Path(output_path).name
title = meta.get("title", "Untitled")
artist = meta.get("artistName") or meta.get("artist", "Unknown")
safe_t = re.sub(r'[<>:"/\\|?*]', '_', title)
safe_a = re.sub(r'[<>:"/\\|?*]', '_', artist)
suggested_name = (
f"{safe_t}_{safe_a}.feedpak" if safe_a
else f"{safe_t}.feedpak"
)
previous_export = session.get("export_path")
if destination == "session":
session["export_path"] = str(output_path)
session["export_filename"] = suggested_name
else:
session.pop("export_path", None)
session.pop("export_filename", None)
if previous_export:
previous = Path(previous_export)
if previous != Path(output_path):
try:
previous.unlink(missing_ok=True)
except OSError:
# A response may still hold the old file open (Windows).
# Session teardown removes the containing temp directory.
pass
if destination == "library":
session["filename"] = Path(output_path).name
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
response = {
"success": True,
"path": output_path,
"format": "sloppak",
# Library-relative filename (built paks land at the DLC root),
# mirroring create_sloppak's response: the frontend keys per-song
# editor prefs (e.g. C1 surface memory) by the same filename a
# later load/loadCDLC will use.
"filename": Path(output_path).name,
"destination": destination,
"filename": (Path(output_path).name
if destination == "library" else suggested_name),
}
# Preserve the legacy library-build response without leaking the
# private session temp path used by Save.
if destination == "library":
response["path"] = output_path
return response

# ── Helpers ──────────────────────────────────────────────────────────

Expand Down
12 changes: 6 additions & 6 deletions screen.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<!-- Top bar: back + song info -->
<div class="flex items-center gap-3 px-4 py-2 bg-dark-800 border-b border-gray-800 shrink-0">
<button onclick="showScreen('home')" class="text-gray-500 hover:text-white text-sm flex items-center gap-1">
<button onclick="editorLeaveToHome()" class="text-gray-500 hover:text-white text-sm flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg> Back
Expand Down Expand Up @@ -64,7 +64,7 @@
<button id="editor-save-btn" onclick="editorSave()" class="px-3 py-1 bg-accent hover:bg-accent-light rounded text-xs font-medium" disabled>Save</button>
<button id="editor-replace-audio-btn" onclick="editorShowReplaceAudioModal()" class="px-3 py-1 bg-dark-600 hover:bg-dark-500 rounded text-xs font-medium hidden" title="Replace the audio track">Replace Audio</button>
<button id="editor-shift-audio-btn" onclick="editorPromptAudioShift()" class="px-3 py-1 bg-dark-600 hover:bg-dark-500 rounded text-xs font-medium hidden" title="Slide the recording in time to line it up with the chart — the audio moves, the chart stays put (non-destructive, undoable)">Shift Audio…</button>
<button id="editor-build-btn" onclick="editorBuild()" class="px-3 py-1 bg-purple-800 hover:bg-purple-700 rounded text-xs font-medium hidden" title="Assemble the finished .feedpak package">Build feedpak</button>
<button id="editor-build-btn" onclick="editorBuild()" class="px-3 py-1 bg-purple-800 hover:bg-purple-700 rounded text-xs font-medium hidden" title="Export the current project to the feedBack library">Export to Library</button>
</div>
<!-- Parts -->
<div class="editor-tb" data-tb="parts">
Expand Down Expand Up @@ -1087,7 +1087,7 @@
</div>
<div class="flex-1 overflow-y-auto px-5 py-4 text-sm text-gray-300 leading-relaxed editor-guide-body">
<p class="text-gray-400">Turn a recording into a playable chart: line a grid up to the
music, place the notes, mark the tempo and sections, and build a <code>.feedpak</code>.
music, place the notes, mark the tempo and sections, save the editable <code>.feedpak</code>, and export it when ready.
Keys below are the default <strong>FeedBack</strong> profile — press
<kbd>?</kbd> for the full, profile-aware shortcut list.</p>
<figure>
Expand All @@ -1104,7 +1104,7 @@ <h3>1 · Start a project</h3>
<img src="/api/plugins/editor/assets/guide/import.png" alt="The Create New Arrangement dialog: add files, review every imported track, and select the Guide audio source.">
<figcaption><strong>New…</strong> uses one track table for audio, Guitar Pro, MIDI, and XML. Pick one audio row as the Guide for tempo mapping.</figcaption>
</figure>
<p class="tip">Nothing touches your library until you <strong>Build</strong> (§9). Save early with <kbd>Ctrl</kbd>+<kbd>S</kbd>.</p>
<p class="tip">Nothing touches your library until you <strong>Export to Library</strong> (§9). Save early with <kbd>Ctrl</kbd>+<kbd>S</kbd>.</p>

<h3>2 · The workspace</h3>
<ul>
Expand Down Expand Up @@ -1167,8 +1167,8 @@ <h3>7 · Drums &amp; 8 · Structure</h3>
<figcaption>Adding drums: import a Guitar Pro or MIDI drum track, then edit it on the piece-lane grid.</figcaption>
</figure>

<h3>9 · Save &amp; build</h3>
<p><kbd>Ctrl</kbd>+<kbd>S</kbd> saves your session. <strong>Build</strong> assembles the finished <code>.feedpak</code> into your library — the only step that changes what the rest of the app sees. Build once the grid fits, the notes are placed, and guide-claps-against-the-recording sounds right.</p>
<h3>9 · Save &amp; export</h3>
<p><kbd>Ctrl</kbd>+<kbd>S</kbd> saves the editable <code>.feedpak</code> project at the location you choose. <strong>Export to Library</strong> publishes the current project to the library without replacing Save. Export once the grid fits, the notes are placed, and guide-claps-against-the-recording sounds right.</p>
<p class="tip"><strong>Undo/redo is the safety net</strong> — every edit, including tempo moves, offset, and imports, is undoable (<kbd>Ctrl</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd>). If something looks wrong, undo it.</p>

<p class="text-gray-500 text-[12px] mt-4 pt-3 border-t border-gray-700/60">More help: <kbd>?</kbd> shortcut panel · <kbd>Ctrl</kbd>+<kbd>K</kbd> command palette. Full written copy: <code>docs/USER-GUIDE.md</code> in the plugin repo.</p>
Expand Down
Loading
Loading