📜 Changelog

Full release history - every version, feature, and bug fix.

No matching releases found.

v1.8.2

2026-06-14 Latest
Local AI, MCP Tools & Editor Polish
  • Run MidiPilot locally and free with Ollama. MidiPilot now has a first-class Ollama (local) provider - point it at your own Ollama install for a 100% local, private, no-API-key, no-cost copilot. (The provider first shipped quietly in 1.8.1.1; this release documents it and adds the polish below.) You install Ollama and pull a model yourself - no harder than getting a cloud API key - and the manual walks through it.
  • More capable MCP / Agent tools. Two new tools - get_selection (read the current selection with per-event indices) and remove_track - plus clearer error messages for malformed tool calls. Both the in-app Agent and external MCP clients can now act on the selection directly (e.g. "delete every second selected note") and remove tracks.
  • Modern grid snap. The Magnet tool can now hard-snap notes to the nearest grid line - including when drawing - like a DAW, with the previous behaviour kept as a "Legacy" option.

✨ Added Added

  • Modern grid snap (snap-to-grid when drawing notes). The Magnet tool now offers two behaviours, switchable under *Settings -> Additional MIDI Settings*: Modern (default) hard-snaps to the nearest grid line - including when drawing new notes, like a DAW - while Legacy keeps the old magnetic pull that only engaged within a few pixels of a grid line. Applies consistently to drawing, moving and resizing (all share one snap path). Works together with the Note Duration setting: the note's start snaps to the grid, its length follows the chosen duration.
  • MCP server hardening + two new tools (shared with Agent Mode). The MCP tool set and the in-app Agent share the same definitions, so both gained:
  • get_selection - returns the user's currently selected events with full details and a 0-based index per event (the index delete_events_by_index expects). MCP clients previously only saw a selection *count*, never the events, so they couldn't act on specific selected notes; now they can.
  • remove_track - deletes a track and its events (guards the file's last track), the missing counterpart to create_track.
  • Argument validation: tool calls with a missing required parameter now return a clear "missing required parameter(s): X" error instead of a confusing tool-specific failure (e.g. a misnamed move_events_to_track argument used to report "Invalid target track index").
  • Ollama (local) provider. Selectable in MidiPilot AI settings and in the chat-footer provider combo; auto-fills the base URL http://localhost:11434/v1 and needs no API key. Tool-capable models drive Agent Mode locally. Documented in the manual (MidiPilot -> Supported Providers -> Local AI with Ollama) and the README provider table.
  • Installed-model picker for Ollama. The model refresh now queries Ollama's native /api/tags (instead of the OpenAI-compat /v1/models, which returns only model ids), so the dropdown lists your installed models with size badges (e.g. qwen3.6:latest (36.0B, 22.0 GB)), flags tool-capable models for Agent Mode, and skips embedding-only models.
  • Ollama in Manage Favourites and Prompt Profiles. The favourites picker now has an Ollama tab, and Prompt Profiles can target Ollama models, so per-provider favourites and per-model system prompts work for local models too.

🔧 Fixed Fixed / Changed

  • Model picker is no longer accidentally editable. The MidiPilot footer model selector was an editable combo, so clicking into it and typing replaced the label - and with the new size badges that label is "name (8.0B, 9.6 GB)", not the model id - which could be sent as a bogus model name. It is now read-only: pick from the list (long names elide with the size badge dropping first; the full name is in the dropdown and tooltip). Custom model names are still entered in Settings.
  • Agent Mode can delete selected events by index. Added a delete_events_by_index tool (also exposed over the MCP server) that deletes specific events from the current selection by their 0-based index - e.g. "delete every second selected note" via indices [1,3,5,...]. This closes a parity gap: Simple Mode already had this, but the Agent only had range-based deletion, so it had to approximate selection-relative deletes by rewriting tick ranges - which capable models handled but smaller/local models often could not complete.
  • Reasoning-effort control for local models. MidiPilot now sends a reasoning-effort value to Ollama on /v1, honouring the Thinking toggle (and defaulting to "low" when Thinking is off). Test Connection sends the same so it stays quick for any selected model.
  • Clear "Ollama not reachable" message. A failed local connection used to say "check your internet connection" (which does not apply to localhost). It now states that the Ollama server isn't reachable and how to start it.

v1.8.1.1

2026-06-04
Hotfix: hardware-acceleration crash
  • Crash on right-click with hardware acceleration enabled. With Settings -> Performance -> Hardware acceleration turned on, right-clicking the piano roll with no events selected (e.g. while using the Measure tool to mark bars) crashed the editor instantly. Disabling hardware acceleration was the only workaround. Fixed - the context menu now behaves identically in both rendering modes.

🔧 Fixed Fixed

  • Stack overflow when opening the piano-roll context menu under OpenGL - in hardware-accelerated mode the matrix is drawn by an OpenGLMatrixWidget wrapper that forwards events to a hidden internal MatrixWidget. When the right-click produced no menu (nothing selected, as with the Measure tool), the internal widget left the QContextMenuEvent un-accepted, so Qt propagated it back up to the wrapper, which forwarded it down again - an unbounded loop that overflowed the stack (0xc00000fd). The wrapper now accepts the event after forwarding, and the no-menu paths in MatrixWidget::contextMenuEvent accept it too, so the event can no longer bounce between parent and child. Software-rendering mode was never affected.

v1.8.1

2026-06-03
Bugfixes & Hardening
  • Commodore 64 / SID fixes. The SF2 ⟷ EMU engine switch no longer crashes when flipped during playback; exporting a C64 tune in SoundFont mode now renders the real C64 timbres instead of a crackle; Emulation is only offered when a .sid is actually open (SoundFont mode still works on any MIDI); and loading a different file (e.g. a Guitar Pro song) after a SID no longer keeps the C64 SoundFont playing - it falls back to your General MIDI SoundFont (auto-loaded from the soundfonts folder if needed), or the system GS Wavetable.
  • Authentic SID audio export. With the C64 Emulation engine active, exporting audio now renders the *original .sid* through the cycle-accurate libsidplayfp engine - the real chip sound, in WAV / OGG / FLAC / MP3 - instead of the converted MIDI, mirroring what you hear on playback. No SoundFont needed and nothing extra to pick (SoundFont mode and other files export as before).
  • Code-review hardening (deferred 1.8.0 findings, re-verified and fixed). A batch of confirmed robustness bugs: an out-of-bounds read on a truncated .gpx, a recorded-but-unfinished-note leak, a MusicXML duration overflow on huge values, an auto-update that could brick the install if extraction failed, two FFXIV/Guitar-Pro UI/parse edge cases, and three playback-thread data races (the FFXIV bard-note timing, the FFXIV equalizer table, and the SID renderer) now guarded.
  • Smaller download. The Windows build no longer ships Qt's FFmpeg media backend (~15 MB): playback uses the native audio backend and audio export runs through FluidSynth/libsndfile + LAME, so nothing needs it. The portable ZIP is correspondingly lighter.

✨ Added Added

  • Authentic SID audio export - when the C64 Emulation engine is active with a .sid loaded, audio export renders the real tune through the libsidplayfp engine (sidfp::Renderer) instead of the converted MIDI, so the exported audio matches Emulation playback rather than the SoundFont conversion. Chosen automatically (no source picker - if EMU is the live engine, the export is the SID, like every other format); SoundFont mode and non-SID files export the converted MIDI as before. Supports WAV / OGG / FLAC (via the bundled libsndfile, loaded dynamically) and MP3 (temp WAV → LAME), requires no SoundFont, and renders the whole tune (per-measure/selection ranges are disabled for this path - a SID has no random access). The mono LAME path was also fixed so mono sources no longer encode as stuttering/scratchy MP3.

🔧 Fixed Fixed

  • C64 engine switch crashed during playback - flipping SF2 ⟷ EMU (or the C64 button) while a tune played tore down the FluidSynth synth / MIDI output under the player thread (use-after-free, 0xc0000005). The transport is now stopped before any engine handover; the same guard was applied to FFXIV SoundFont mode.
  • C64 SoundFont export played the wrong sound - exporting audio in C64 SoundFont mode rendered raw GM / crackle instead of the C64 waveform timbres, because the offline export callback didn't apply the C64 program remap (and wasn't installed in C64 mode at all). Export now matches live playback.
  • Emulation offered without a SID - the SF2 ⟷ EMU switch and the Settings radio now lock Emulation unless a .sid is the open file (Emulation plays the original .sid; SoundFont mode is unaffected and still works on any MIDI).
  • Stale C64/FFXIV SoundFont after switching files - loading a non-SID file (e.g. Guitar Pro) while a C64/FFXIV SoundFont was active left that SoundFont playing the new song; with no special mode active the engine now falls back to a General MIDI SoundFont (auto-loaded from the soundfonts folder if one is present), or the system GS Wavetable.
  • [CORE-002] GPX out-of-bounds read - a truncated/crafted .gpx could read past the BCFZ decompression buffer (crash). The bit-stream reader is now bounds-checked.
  • [CORE-004/005/006] Playback-thread data races - three latent races where the player/audio thread and the GUI thread touched shared state without a lock: the FFXIV bard-accurate min-note-length bookkeeping (its deferred note-off fires on the GUI thread while the player thread processes notes), the FFXIV equalizer's per-program table, and the SID renderer's voice-mute state. All three are now serialised with a mutex. (Could surface as a rare stuck/cut note in bard playback, a crash while dragging an equalizer slider during playback, or a glitch when muting a voice mid-SID-tune.)
  • [CORE-007] Recording leak + stale pairing - stopping a recording with a held note leaked the orphan note-on and left a stale entry in the note-pairing map (could mis-pair a later recording); the orphans are now freed.
  • [CORE-008] MusicXML duration overflow - an unusually large <duration> could overflow 32-bit math and corrupt the timeline; it is computed in 64-bit now.
  • [CORE-009] Auto-update could brick the install - a failed or timed-out ZIP extraction was treated as success, potentially leaving no runnable executable. The updater now restores the previous EXE and aborts on any extraction failure.
  • [CORE-010] FFXIV equalizer button - stayed enabled after a declined FFXIV-mode enable; it is now disabled with the mode.
  • [CORE-011] Guitar Pro repeat parse - a corrupt repeat-alternative byte caused an undefined-behaviour bit shift; the value is now clamped.
  • [CORE-012] Tempo-map null guard - a defensive null check in MidiFile::tick ran one step after the dereference it was meant to protect; reordered.
  • Disabled checkboxes / radio buttons looked active - every theme dimmed only the little indicator, not the label text, so a disabled option (e.g. an export range that doesn't apply) stayed full-brightness. Disabled checkbox / radio-button text is now muted in all themes.

🔄 Changed Changed

  • Trimmed the Windows download (~15 MB). Dropped Qt's unused FFmpeg media backend (ffmpegmediaplugin.dll) from the package - MidiEditor uses QAudioSink (native Windows audio backend) plus FluidSynth/libsndfile + LAME for export, and never QMediaPlayer/QMediaRecorder, so the ffmpeg backend was dead weight. SID playback (SF2 + Emulation) and WAV/OGG/FLAC/MP3 export are unaffected.

v1.8.0

2026-06-01
Commodore 64 / SID Support
  • Commodore 64 / SID support (Phase 42). Open a .sid tune like any other file and edit it as MIDI, and hear it the way it was meant to sound. The importer turns a SID's three voices into a multi-track MIDI (one track per voice plus a dedicated percussion track) with note durations, velocities and arpeggios reconstructed from the chip's register stream.
  • Two ways to play a C64 tune, one toolbar button. A new C64 button plays the tune in whichever mode you pick under Settings → MIDI I/O → Commodore 64 / SID:
  • SoundFont - plays the *converted MIDI* through a Commodore 64 SoundFont so it uses real C64 waveform timbres (pulse, sawtooth, triangle, noise) instead of General MIDI instruments. The C64 SoundFont downloads itself on first use (like the FFXIV font) if you don't already have one.
  • Emulation - plays the *original .sid* through the cycle-accurate libsidplayfp engine for authentic chip audio. It is driven by the normal transport: Play starts from the cursor, Stop / Pause and seeking work as usual, the piano-roll cursor follows the music in real-time sync, muting a channel or a track silences the matching SID voice live, and playback stops at the end of the note roll instead of looping forever.
  • Pick the engine the easy way. The first time you turn C64 mode on, a one-time prompt asks whether to use SoundFont or Emulation and remembers it; the C64 SoundFont (~11 MB) is fetched in the background so either engine works instantly later. A retro SF2 ⟷ EMU toolbar toggle (shown only while C64 mode is active) flips the engine without opening Settings - the toggle, the prompt and the Settings radio all stay in sync.
  • Authentic RSID import. Interrupt-installing tunes (Arkanoid, RoboCop, Great Giana Sisters, …) that the lightweight importer can't follow are now imported accurately through the libsidplayfp engine. SID tunes loop forever, so a musical loop detector trims the import to intro + one loop, with a configurable fallback length (Settings) when no clean loop is found.
  • MusicXML export (Phase 43). File → Export MusicXML… writes the current song as a MusicXML score, openable in MuseScore, Finale, Sibelius and Dorico. Since MIDI stores no notation, the exporter *reconstructs* it: per-track parts, measures from the time signature, note values + dots, rests, ties across barlines, chords, clefs, and enharmonic spelling from the key. v1 is a single voice per part on a 1/16 grid (notes/rhythm/instruments/tempo/key correct and openable everywhere - not publication-perfect engraving); the .mxl (compressed) container and Guitar Pro export are planned follow-ups.
  • Retro cursor-time display (Phase 41). An opt-in toolbar widget shows the edit-cursor time - or the live playback time, media-player style - as retro seven-segment digits on a dark bezel. Left-click cycles the readout: position, total length, remaining (countdown), tempo (BPM), and musical bar.beat + time signature. Right-click cycles the LED colour theme (Amber, Blue, Green, Sakura, Mono, Red). Adaptive MM:SS, widening to H:MM:SS past an hour. Added / positioned via Customize Toolbar like the MIDI Visualizer and loaded by default; the chosen readout and colour both persist across restarts.
  • Fixes from a code-review pass (pre-existing bugs). A toolbar widget could crash after you removed it via Customize Toolbar or switched theme (the widget was freed on toolbar rebuild but its pointer left dangling), and FFXIV SoundFont Mode could leave your MIDI output switched to the built-in FluidSynth if you declined the SoundFont download. Both fixed - see *Fixed* below.

✨ Added New Features

  • Phase 42 - SID import - .sid is now an Open format. A Qt-free converter core under src/converter/Sid/ parses the PSID/RSID container (SidFile), and for PSID tunes a from-scratch cycle-stepped 6502 emulator (Mos6502) runs the tune's init + play routines at the C64 frame rate (SidCapture, 50 Hz PAL / 60 Hz NTSC) and snapshots the $D400-$D418 register file every frame. SidReconstruct turns those frames into notes: gate bit → note on/off, oscillator frequency → MIDI pitch (via the tune's clock), ADSR sustain → velocity and the release nibble → a modelled note-release tail, mid-note pitch changes while gated → new notes (arpeggios). Hard-restart artifacts (TEST-bit frames, 1-frame blips) and ring-modulation frames (inharmonic pitch) are filtered so they don't spawn phantom notes. A voice's multiplexed noise drum hits route to a dedicated 4th SID Percussion stream. SidMidiWriter emits a format-1 SMF (conductor + three voice tracks + percussion, voice → MIDI channel) which is loaded straight into the editor.
  • Authentic RSID import via libsidplayfp - RSID tunes that install their own interrupt player need full C64 hardware emulation, so they are imported through the vendored libsidplayfp 3.0.1 cycle-accurate engine: the engine plays the tune while an adapter captures the per-frame SID register writes, which then feed the same loop-detection → reconstruction → SMF pipeline. PSID tunes keep using the lightweight built-in emulator (fast). The slower RSID render shows an animated progress dialog.
  • Loop detection - SID tunes have no intrinsic length (they loop forever). SidCapture::detectLoopEnd derives a per-frame musical signature (gate + coarse pitch + waveform class across the three voices, with tolerance for LFO/vibrato jitter), finds the smallest repeating period at the tail and trims the capture to intro + one loop. When no clean loop is found the importer falls back to a configurable window (default 240 s).
  • C64 SoundFont Mode - a toolbar toggle that plays the *converted MIDI* with authentic C64 timbres. It remaps the importer's lead/percussion programs onto a C64 SoundFont's waveform presets (pulse / sawtooth / triangle / noise), isolating the C64 SoundFont while active and restoring the previous (General MIDI) selection when switched off (C64SoundFontHelper, mirroring the FFXIV SoundFont mode). FluidSynthEngine tracks each channel's pre-remap program so toggling the mode live re-voices every channel instantly instead of crackling on a single noise preset. Enabling the mode without a C64 SoundFont installed offers an auto-download (Commodore_64.sf2, added to the SoundFont download catalog) and switches the MIDI output to FluidSynth, exactly like FFXIV SoundFont Mode.
  • Emulation-mode polish - the MIDI Visualizer and the retro time display now animate during authentic SID playback too (they are normally fed by the MIDI player thread, which doesn't run in Emulation mode; both are now driven from the SID position). Switching the engine radio in Settings while C64 is active hands the active state to the chosen engine immediately. The time display now defaults to the Blue LED theme.
  • Authentic SID playback (Emulation mode) - plays the original .sid through libsidplayfp's engine to a QAudioSink. It is fully transport-controlled: arming is the C64 button (glow = armed), then Play renders from the cursor position, Stop / Pause stop (Pause parks the cursor), and the matrix playback cursor follows the audio in exact real-time sync. Channel mute and track mute mirror onto the three SID voices live (muting a channel, or muting every track that drives a voice, silences that voice; hiding is purely visual). Playback auto-stops at the end of the note roll.
  • One button, mode in Settings - a single C64 toolbar button triggers whichever engine the Settings → MIDI I/O → Commodore 64 / SID radio selects (SoundFont or Emulation), plus a *default SID import length* spinbox for the no-loop fallback. Switching the engine while C64 is active hands the active state over to the chosen engine immediately. The button is dark-mode-readable: the original colour Commodore logo on a light chip with an accent glow when active, a legible silhouette when off.
  • Engine picker, toolbar switch & SoundFont prefetch - a new C64Mode helper is the single source of truth for the engine choice (the Settings radios, the toolbar switch, the first-use picker and the C64 button all route through it, so the handover logic lives in one place and they stay in sync via a modeChanged notifier). On first C64 use a one-time QMessageBox picker (SoundFont / Emulation) is shown and persisted (C64/modeChosen); choosing Emulation also kicks off a silent background download of Commodore_64.sf2 into the soundfonts folder (announced in the picker) so a later switch to SoundFont is instant. New code-drawn C64ModeSwitchWidget (a glowing retro SF2 ⟷ EMU rocker) registers as the c64_mode_switch toolbar action and toggles its own QToolBar action visibility so it only appears while C64 mode is active.

📝 Notes Implementation Notes

  • libsidplayfp 3.0.1 vendored under third_party/libsidplayfp/ (the self-contained *sidlite* SID emulator, ROM-free) and built as a static library, with thin Qt-free adapters (SidFpCapture for import register-capture, SidFpPlayer for PCM playback). No new runtime DLL beyond Qt's multimedia plugins.
  • Real-time audio pacing - the playback renderer carries any per-call frame surplus forward in a leftover buffer instead of discarding it, so no emulated SID-time is ever skipped and the audio plays at exact real time (keeping the real-time cursor locked to the music).

🟡 Medium Test Harness

  • 5 new SID unit tests - test_sid_file, test_sid_cpu, test_sid_capture, test_sid_reconstruct, test_sid_midiwriter cover the container parser, the 6502 emulator, the capture loop + loop detection, the note reconstruction heuristics, and the SMF writer. With the MusicXML-export test (below) the full suite is 49/49 green.

🟡 Medium Files (Phase 42)

  • src/converter/Sid/ *(new)* - SidFile, Mos6502, SidCapture, SidReconstruct, SidMidiWriter, SidImporter.
  • third_party/libsidplayfp/ *(new, vendored)* - libsidplayfp 3.0.1 + adapter/SidFpCapture.{h,cpp} + adapter/SidFpPlayer.{h,cpp} + CMake static-lib target.
  • src/midi/SidAudioPlayer.{h,cpp} *(new)* - transport-controlled authentic playback (QAudioSink pull-mode), voice muting, real-time position.
  • src/gui/C64ToggleWidget.{h,cpp}, src/gui/C64SoundFontHelper.{h,cpp} *(new)* - toolbar button + SoundFont-mode orchestration.
  • src/gui/MidiSettingsWidget.{h,cpp} - the Commodore 64 / SID settings block (engine radio + default import length + immediate engine handover).
  • src/midi/FluidSynthEngine.{h,cpp} - C64 SoundFont mode + per-channel program remap.
  • src/gui/MainWindow.{h,cpp}, src/gui/MatrixWidget.cpp, CMakeLists.txt - .sid Open dispatch + progress dialog, transport routing to SID, cursor follow, channel/track → voice mute, Qt6::Multimedia + sidplayfp link.

✨ Added Added - MusicXML Export (Phase 43)

  • MIDI → notation engraver - a new score::MidiToScore core (src/converter/Score/) reconstructs notation from flat MIDI: builds the measure grid from the time signature, quantises to a 1/16 + dotted grid, fills rests so every measure is complete, splits notes across barlines into tied notes, groups same-onset notes into chords, picks a clef from each part's median pitch, and spells pitches enharmonically from the key signature (sharps for sharp keys, flats for flat keys). Output is a score::Score IR. The engraver is split into a pure buildScore(ScoreInput) (unit-tested) and a MidiFile-coupled extractInput()/build().
  • MusicXML writer - MusicXmlWriter (src/converter/MusicXml/) serialises the Score IR to MusicXML 4.0 score-partwise via QXmlStreamWriter: part-list with MIDI instrument hints, per-measure <attributes> (divisions/key/time/clef, emitted only where they change), notes/rests with <type>/<dot>/<tie>/<tied>, <chord/>, and <sound tempo>/metronome directions.
  • UI - File → Export MusicXML… (MainWindow::exportMusicXml), enabled whenever a file is open, writing a plain-text .musicxml. One writer covers the whole notation ecosystem (MuseScore/Finale/Sibelius/Dorico all import MusicXML), so a dedicated MuseScore writer is unnecessary.
  • Tests - test_musicxml_export (new) covers note-value mapping, dotted values, ties across barlines, rest filling, chords, enharmonic spelling in sharp/flat keys, and the writer's MusicXML fragments. The engraver core links without the MidiFile tree, so the test stays narrow.

✨ Added Added - Retro Cursor Time Display (Phase 41)

  • Phase 41 - Cursor Time Display - new TimeDisplayWidget (src/gui/), a self-sizing toolbar widget with a custom-painted seven-segment renderer: anti-aliased beveled segments, a soft per-segment glow over dim "ghost" segments, a colon that blinks during playback, and a rounded dark bezel with a subtle sheen. Time source is MidiFile::msOfTick(cursorTick()) while idle (refreshed on cursorPositionChanged) and PlayerThread::timeMsChanged during playback (snapped back to the cursor on playerStopped) - the same signals that already drive the lyric timeline / voice lane. Five readouts cycle on left-click (POS / LEN / REM / BPM / BAR); BAR shows bar.beat num/den from measure() + meterAt(), BPM from the last TempoChangeEvent at/before the tick. Right-click cycles six LED colour palettes. Both the readout mode and the colour persist in QSettings (View/timeDisplayMode, View/timeDisplayTheme).
  • Toolbar integration - registered as the time_display action in LayoutSettingsWidget (available-actions list + every default layout) and built on demand in all four toolbar-build paths, exactly like the MIDI Visualizer. A migration step in MainWindow injects it into existing saved toolbars so it appears (enabled) without a Reset to Default.

🟡 Medium Files Modified

  • src/gui/TimeDisplayWidget.{h,cpp} *(new)*, src/gui/TimeDisplayFormat.h *(new)* - the widget + its pure, unit-tested format/mode helpers.
  • src/gui/LayoutSettingsWidget.cpp - time_display added to the available-actions list and all default toolbar layouts.
  • src/gui/MainWindow.{h,cpp} - widget member + time_display action registration; on-demand instantiation in all toolbar-build paths with playback / cursor signal wiring; setFile() rebind; play() / record() reconnect; migration of existing saved layouts.
  • tests/test_time_display_format.cpp *(new)*, tests/CMakeLists.txt - the format test executable.
  • Planning/02_ROADMAP.md - Phase 41 specification.
  • manual/cursor-time-display.html *(new)* - dedicated manual page (with screenshots + a themes/modes GIF); manual/navigation.js + manual/docs-index.html - sidebar entry (Editor group) + section card.

🔧 Fixed Fixed

  • Toolbar widget crash on rebuild - removing a toolbar widget (Cursor Time, MIDI Visualizer, Lyric Visualizer, FFXIV voice gauge, the C64/FFXIV/MCP toggles) via Customize Toolbar, or switching theme, then opening a file or stopping playback could crash: the widgets are destroyed when their toolbar is rebuilt, but only the MIDI-Visualizer pointer was being cleared, leaving the others dangling. All on-demand toolbar pointers are now cleared on every rebuild (MainWindow::nullOnDemandToolbarWidgets).
  • FFXIV SoundFont Mode left the MIDI output switched on cancel - turning on FFXIV SoundFont Mode without the bard SoundFont installed and then declining the download dialog left the MIDI output switched to the built-in FluidSynth (and persisted) even though the mode wasn't enabled. The previous output device is now restored when the enable is aborted (matching the C64 SoundFont mode behaviour).
  • Saving an imported file could overwrite the original (data loss) - pressing Save (Ctrl+S) on a file opened from an import-only format (SID, Guitar Pro, MML/3MLE, MusicXML, MuseScore) wrote a Standard MIDI file straight over the original on disk, since MidiFile::save() always emits MIDI bytes. The result destroyed the source file and left a .sid/.gp5/... that no longer re-imported. Save now detects an import-only path and redirects to Save As (pre-filled with .mid) after a one-time notice, so the original is never clobbered; once saved as .mid, Save works normally. (The list of import-only formats is now a single shared helper, also used by the collaboration shared-copy path, which had the same workaround but was missing .sid.)

v1.7.2

2026-05-25
Show Mode, In-Session Chat & Follow-the-Host
  • Show Mode is now a first-class session type. When you start a LAN or WAN session a mode picker asks Edit (everyone can edit, original behaviour) or Show (one peer at a time holds "the hat" = editing rights). Use cases: tutorial streams, MIDI code review, classroom teaching, AI walkthroughs. Strict request-and-approve hat passing, host-side hunk validation against the current presenter, and a 30 s heartbeat deadline after which the host can reclaim the hat from a silent presenter.
  • In-session chat side-channel - a "Chat" tab in the Collaboration sidebar that's only visible while a Live session is active. Lightweight: plain UTF-8 text, host-side 4 KB cap + 200 ms-per-sender rate limit, 500-message in-memory scrollback, no persistence. Unread badge + tab-text blink when messages arrive on another tab. Same wire transport as MIDI hunks (no new connection).
  • Follow-the-host viewport sync - viewers see exactly what the presenter is looking at, regardless of window size or zoom. Local matrix auto-fits to the presenter's visible region (~5% padding) so a viewer with a different resolution doesn't snap to the top of the piano roll. Track + channel visibility, edit cursor, active tool name, and the presenter's selected notes all mirror onto every viewer's screen. Playback start / stop is also synced - when the presenter presses Play or Stop, every viewer's player follows at the presenter's cursor position (each peer through their own audio output).
  • Show-Mode UI lock for viewers - the matrix accepts no tool input, MidiPilot's input field is disabled with an explanatory placeholder, the MCP server rejects tools/call with a structured error (read-only midi://* resources stay reachable), and the Edit / Tools / Midi menus + the toolbar widget are greyed out. View-only Playback / Help / chat / scroll / Piano-key preview stay live.
  • Version-mismatch warnings - every hello and sessionWelcome frame now carries an appVersion field. v1.7.2 hosts detect older joiners (pre-1.7.2 → missing field) and surface a modal popup naming the peer + the features the older build won't see. v1.7.2 joiners detect older hosts via the missing sessionWelcome frame and show the symmetric warning. Future-version mismatches (1.7.2 ↔ 1.8.0 etc.) appear as a delayed status-bar tip on both sides. Lesson learned: every wire protocol should carry a version field from v1.0 - the v1.7.0 omission means v1.7.0 / v1.7.1 are forever the "blind generation".
  • Hat-pass UI - new menu entries *Pass the hat to...*, *Yield the hat* (gives the hat back to the host without picking a specific successor), *Request the hat* (viewers), and *Take the hat* (host privilege after the silence deadline). Incoming hat requests show as a modal accept/decline prompt on the presenter; failed transfers (target peer disconnected) bounce back as a status-bar toast. Status-bar carries a 🎩 PRESENTING or 👀 Watching <name> indicator while in Show mode.
  • Mid-session mode toggle (host-only) - new Collab menu entry *Switch to Show mode* / *Switch to Edit mode* (shortcut Ctrl+Shift+M) lets the host flip the active session between modes without leaving and re-starting. Use case: two peers co-editing in Edit mode, one wants to demo something briefly - host toggles into Show, becomes the presenter, demos / passes the hat as needed, then toggles back to Edit and everyone resumes co-editing. The Show-mode UI lock (matrix, MidiPilot, MCP, menus, toolbar) engages and disengages live on every peer as the host flips.
  • Test harness extended - test_session_mode (46 cases) covers the entire Show-Mode + chat + version-mismatch + viewState + playback + mode-switch + yield-hat wire vocabulary, including UTF-8 multibyte chat payloads and legacy-frame fallbacks for every frame type. Full suite: 42/42 green (41 from v1.7.1 + the new SessionMode test executable).

✨ Added New Features

  • Phase 9.9 - Show Mode (§15.2) - session-wide editing-rights model added to LanLiveSession. New LiveSession::Mode { Edit, Show } enum (header-only src/collab/SessionMode.h so it's unit-testable without dragging the LanLiveSession dep tree in). Mode is locked at session start - the host picks via a mode-picker QMessageBox before startHosting{,Wan} runs, the joiner adopts via a new sessionWelcome frame that's the host's first response to the joiner's hello. Strict hat-passing: only the current presenter can broadcast hatTransferred, host-side validator rejects any hunks frame from a non-presenter machineId with an unauthorisedHunkDropped diagnostic signal. Three frame types added: requestHat, hatTransferred(reason: transfer | host-takeover), hatRejected. Race-resolution rule per §15.2: if a requestHat arrives at the host AFTER the requester became the presenter (transfer crossed on the wire), the request is silently dropped. Host-takeover privilege is sender-locked (fromPeer == nullptr check) so a remote peer can't ship {reason: "host-takeover"} to escalate.
  • Phase 9.9c - Show-Mode viewer lock (§15.2) - in Show mode + viewer state: MatrixWidget::setEditingLocked(true) suppresses tool-driven mouse press/move/release (piano-key preview + scrolling stay live, per design); MidiPilotWidget::setShowModeLocked(true) disables the input field with an explanatory placeholder (chat history stays visible so AI walkthroughs are readable); McpServer::handleToolsCall returns a structured "Local editor is in Show Mode (viewer)..." error; the Edit / Tools / Midi menus + the entire toolbar widget are disabled via menuAction()->setEnabled(false) and _toolbarWidget->setEnabled(false). Playback menu stays enabled so Space-bar play/pause works for local-only preview.
  • Phase 9.9d - Hat-pass UI - new actions in the Collab menu: *Pass the hat to...* (presenter-only; opens a QInputDialog::getItem listing currently-connected peers with their display names from connectedPeerInfo()), *Yield the hat* (presenter-only, non-host; sends a yieldHat frame that the host validates against _presenterMachineId and turns into a regular hatTransferred(host, reason="yield") broadcast), *Request the hat* (viewer-only; sends a requestHat frame, status-bar acknowledges), *Take the hat* (host-only; visible when hostCanTakeHat() == true, i.e. the presenter is no longer in the connected peer list). Presenter receives a modal Accept/Decline prompt on incoming requests. Transfer to a disconnected target returns a hatRejected frame surfaced as a status-bar toast. The refreshCollabMenu lambda dynamically shows/hides each action based on isPresenter() / role / hostCanTakeHat().
  • Phase 9.9e - Show-mode status indicators - the existing _statusLiveSessionLabel now appends 🎩 PRESENTING (presenter) or 👀 Watching <name> (viewer) while in Show mode. Successful hat transfers + the host-takeover variant emit transient status-bar toasts ("Hat is now held by Alice", "Host took the hat (presenter was silent)"). All driven from the new LanLiveSession::sessionModeChanged signal so the indicator updates AFTER sessionWelcome has been processed (the original joined signal fires too early, when _mode is still the Edit default).
  • Phase 9.9f - Follow-the-host viewState - added during testing. New viewState wire frame sent by the presenter and re-broadcast by the host. Throttled to one frame per 250 ms in LanLiveSession (_viewStateThrottle QTimer coalesces a rapid drag-scroll burst into a single broadcast). Payload: viewport (startMs / startLine / scaleX / scaleY / focusEndMs + focusEndLine), track + channel visibility arrays, presenter's cursorTick, active tool's toolTip() display name, and an array of selected-event identity tuples (tick, channel, line, type). Viewer applies via a new MatrixWidget::fitToFocus(startMs, endMs, startLine, endLine) method that derives the LOCAL scaleX/scaleY needed to fit the presenter's visible region into the viewer's pixel geometry with ~5 % padding, then centres the focus. This sidesteps the clamp-to-top problem that 1:1 scroll mirroring hit whenever the viewer's window was wider than the presenter's. Visibility + selection apply through new silent setters (MidiTrack::setHiddenSilent, MidiChannel::setVisibleSilent which correctly updates the ChannelVisibilityManager singleton, Selection::setSelectionSilent) so the viewer's undo history stays clean. Backward-compat: the focus-extents fields are -1 when absent, and the decoder falls back to the original 1:1 scroll mirroring.
  • Phase 9.9h - Mid-session mode toggle - new sessionModeSwitch {mode, presenterMachineId} wire frame; host-only LanLiveSession::switchSessionMode(SessionMode) API + matching client-side handleClientSessionModeSwitch that updates _mode + _presenterMachineId and emits sessionModeChanged so the existing GUI lock machinery flips synchronously. MainWindow's Collab menu gains a single toggle action whose text reads "Switch to Show mode" or "Switch to Edit mode" depending on the current state; visible only when hosting. Edit → Show makes the host the initial presenter; Show → Edit clears the presenter pointer and releases every viewer's lock.
  • Phase 9.9g - Playback trigger sync - new playback {action: "start"|"stop", tickPosition: N} wire frame. Presenter's Play / Stop click → broadcast to every viewer; viewer seeks to the carried tick and triggers its own MidiPlayer::play() / stop(). Server-side validator gates on sender == presenterMachineId (same rule as hunks + viewState). _applyingRemotePlayback re-entry flag in MainWindow stops the viewer's local play/stop from re-broadcasting back to the presenter. Deliberately a one-shot trigger: no wall-clock alignment, no continuous re-sync, no playhead-position broadcast (initial latency 50-200 ms and longer-playback drift accepted as good-enough for tutorial / demo use). Each peer plays through whatever audio output is configured locally; an un-configured viewer gets the editor's existing "configure your MIDI output" dialog on first trigger.
  • Phase 9.11 - In-session chat side-channel (§15.3) - new chat wire frame: {sender, displayName, text, timestamp}. Host validates 4 KB UTF-8 cap + 200 ms-per-sender rate limit (per-machineId table in _lastChatMsBySender), then re-broadcasts via broadcastExcept so the sender doesn't see their own message echoed (already appended optimistically locally). New src/gui/collab/CollabChatWidget.{h,cpp} (QTextEdit scrollback + QLineEdit input + Send button); 500-message in-memory cap with the oldest block trimmed when exceeded; per-sender colour styling (own messages in accent blue, others in neutral); local-timezone HH:mm timestamp formatting; defensive HTML-escape on every message text so nothing in the wire can inject formatting. Tab added to lowerTabWidget; visible only while in an active session, auto-cleared on session end (no persistence per §15.3). Unread badge ("Chat (3)") on the tab title when the user is on another tab, plus a 600 ms / glyph-toggle blink for visual emphasis (width-stable across themes). Wire-format unit-tested for UTF-8 round-trip, type-field shape, decode of a frame with missing reason field, and the kChatTextMaxBytes / kChatRateLimitMsPerSender constants.
  • v1.7.2 §15.4 - Version-mismatch detection - appVersion field added to both hello (joiner → host) and sessionWelcome (host → joiner) frames; the encoder reads it from the MIDIEDITOR_RELEASE_VERSION_STRING_DEF compile constant. Three signals: LanLiveSession::peerVersionMismatch(peerName, peerMachineId, peerVer, ourVer), hostVersionMismatch(hostVer, ourVer), legacyHostDetected(). The "legacy host" path triggers when ANY frame other than sessionWelcome arrives first on a freshly-joined client (v1.7.2+ hosts always ship sessionWelcome as their first reply to hello, so absence is a reliable pre-1.7.2 signal). MainWindow surfaces the warnings as modal QMessageBox::information popups for the blind-generation case (peer has no appVersion field, can't see our warnings either) and as status-bar tips with a 600 ms delay for matched-but-different versions (delay lets the immediate post-hello statusMessage emits land first so our warning isn't instantly overwritten). The lesson is now documented in a personal memory: every wire protocol should carry a version field from v1.0.

🟡 Medium Improvements

  • Mode-aware sync tick (§15.2) - in Show mode + viewer state, onSyncTick early-returns without broadcasting any local hunks, BUT still advances _lastSyncedSnapshot / _lastBroadcastEndTick. Otherwise a viewer's accumulated local edits during the lock period would dump as one giant catch-up diff on the next hat-take. Combined with the existing host-side hunk validator, this is defence-in-depth: even if a buggy / older viewer's matrix lock fails, their broadcasts are dropped at both the sender and the receiver.
  • WAN auto-reconnect preserves Show-mode selection - the existing startHostingWan retry path captured the file but not the mode, so a network blip on a Show-mode session silently downgraded to Edit on reconnect. The retry lambda now captures SessionMode retryMode = _mode before leaveSession() and re-passes it through.
  • connectedPeerInfo() accessor - returns (machineId, displayName) pairs for the host's currently-connected peers. Used by the *Pass the hat to...* picker (QInputDialog::getItem over the labels) so the host doesn't have to type a machineId, plus by the status-bar 👀 Watching <name> lookup when the presenter is a remote peer.
  • Tool::setToolChangedCallback - lightweight static-callback hook in src/tool/Tool.cpp so the GUI layer (specifically MainWindow) can trigger a viewState broadcast whenever the local tool changes, without polluting the tool module with QObject / GUI dependencies. Plain function pointer; MainWindow registers a static lambda that calls broadcastLocalViewState().
  • MatrixWidget::applyZoom, currentScaleX/Y, visibleEndMs/Line, fitToFocus - public zoom-state accessors + the fit-to-focus geometry calculator. applyZoom runs calcSizes() so subsequent scroll calls see the new scale; fitToFocus clamps required scale to [0.05, 20.0] (X) and [0.10, 10.0] (Y) so an extreme presenter zoom can't push the viewer into an unusable state.
  • Stale-v1.7.1 leftover fixed in Planning/09_COLLABORATION.md - the "post-MVP" subsection still said "the v1.7.1 ship is just mode + hat-passing + UI lock"; corrected to v1.7.2 to match the actual ship.

🟡 Medium Test Harness

  • TEST-1.7.2-001 - test_session_mode extended to 46 cases (was 14 in v1.7.1):
  • SessionMode enum - wire-string round-trip, case-insensitive parsing, unknown-value fallback to Edit (forward-compatibility), null-out-pointer safety in the decoder.
  • sessionWelcome JSON - type field presence, mode + presenter round-trip, empty-presenter handling, decode of frame with missing fields (defaults to Edit), null-pointer decode, appVersion present-when-supplied / omitted-when-empty / round-trip / legacy-decode-as-empty-string.
  • Hat-pass frames - requestHat field shape + decode round-trip, hatTransferred field shape + empty-reason-defaults-to-transfer + host-takeover reason + decode of missing-reason (legacy → "transfer"), hatRejected shape + decode.
  • Chat frame - type + field shape, UTF-8 multibyte round-trip (German Umlaute + CJK + emoji), multi-line text round-trip, wire-serialization end-to-end, kChatTextMaxBytes / kChatRateLimitMsPerSender constants pinned at 4096 / 200 so a future regression would fail the build.
  • viewState frame - type + sender shape, focus-extents round-trip, legacy-absent-fields decode to safe defaults (focusEndMs/Line = -1, scaleX/Y = 1.0), selection tuple list round-trip, empty selection omitted from wire to save bytes.
  • playback frame - start + stop shape with carried tick position, decode round-trip, absent-tickPosition decodes to -1 sentinel, null-out-pointer safety.
  • sessionModeSwitch frame - host-only mid-session toggle: type + fields, Edit clears presenter pointer, decode round-trip.
  • yieldHat frame - empty-payload type marker (host infers yielder from peer-link machineId).

🟡 Medium Files Modified

  • CMakeLists.txt - version bumped to 1.7.2.
  • README.md - version bumped to 1.7.2.
  • src/collab/SessionMode.h *(new)* - header-only LiveSession::Mode enum + wire encode/decode for sessionWelcome, requestHat, hatTransferred, hatRejected, yieldHat, chat, viewState (incl. the ViewportState struct and SelectedEventId tuple), playback, and sessionModeSwitch. Pure JSON helpers, no QObject - keeps the unit-test build minimal.
  • src/collab/LanLiveSession.{h,cpp} - public API additions: mode(), presenterMachineId(), isPresenter(), connectedPeerInfo(), requestHat(), transferHatTo(), hostTakeHat(), yieldHat(), hostCanTakeHat(), sendChatMessage(), broadcastViewState(), broadcastPlayback(), switchSessionMode(). New signals: sessionModeChanged, hatRequested, hatTransferred, hatTransferRejected, unauthorisedHunkDropped, chatMessageReceived, chatMessageDropped, viewStateReceived, playbackTriggerReceived, peerVersionMismatch, hostVersionMismatch, legacyHostDetected. New private handlers + a _viewStateThrottle QTimer + _lastChatMsBySender rate-limit table. encodeHello + encodeSessionWelcome now carry appVersion. startHosting{,Wan} take an optional SessionMode parameter (default Edit).
  • src/gui/collab/CollabChatWidget.{h,cpp} *(new)* - the chat panel widget.
  • src/gui/MatrixWidget.{h,cpp} - new setEditingLocked, currentScaleX/Y, applyZoom, visibleEndMs/Line, fitToFocus. Mouse press / move / release suppressed when _editingLocked is true. The lock co-exists with the existing enabled flag (which gates everything including piano-key preview).
  • src/gui/MidiPilotWidget.{h,cpp} - new setShowModeLocked(bool) toggles the input field's enable-state and placeholder text; the four other setEnabled(true) re-enable sites (request finished / errored, model-incapable, agent finished / errored) now respect _showModeLocked.
  • src/gui/MainWindow.{h,cpp} - mode picker QMessageBox before startHosting{,Wan}; the applyShowModeLock lambda toggles matrix lock + MidiPilot lock + the Edit/Tools/Midi menus + the toolbar; status-bar 🎩 / 👀 indicator; *Pass the hat to* / *Request the hat* / *Take the hat* menu actions; hat-request modal + transfer-rejected toast; Chat tab integration in lowerTabWidget with unread badge + theme-agnostic / glyph-toggle blink (width-stable, works in QSS-themed builds where setTabTextColor has no effect); version-mismatch warnings (modal for blind generation, delayed status-bar for version-diff); broadcastLocalViewState + applyRemoteViewState for the follow-the-host machinery, wired to scrollChanged / actionFinished / cursorPositionChanged / sessionModeChanged / the Tool::setToolChangedCallback hook. play() / stop() broadcast the playback trigger (presenter-only), playbackTriggerReceived connects to a viewer-side handler that seeks + starts / stops the local player with an _applyingRemotePlayback re-entry guard.
  • src/ai/McpServer.cpp - handleToolsCall returns a structured "Local editor is in Show Mode (viewer); editing is locked..." error when the local peer is a viewer.
  • src/midi/MidiTrack.{h,cpp} - new setHiddenSilent(bool).
  • src/midi/MidiChannel.{h,cpp} - new setVisibleSilent(bool) that ALSO updates the ChannelVisibilityManager singleton (not just the _visible mirror).
  • src/tool/Selection.{h,cpp} - new setSelectionSilent(QList<MidiEvent*>) that bypasses the protocol-step recording.
  • src/tool/Tool.{h,cpp} - new setToolChangedCallback(ToolChangedFn) static hook.
  • tests/CMakeLists.txt, tests/test_session_mode.cpp *(new)* - the wire-vocabulary test executable.
  • Planning/02_ROADMAP.md - the public collab overview: sub-phase status table flipped to ✅ shipped (1.7.2) for 9.9 Show Mode and 9.11 Chat, plus new rows for follow-the-host (9.9f), playback-trigger sync (9.9g), the mid-session mode toggle (9.9h) and the §15.4 version handshake; the "What's next" section rewritten to reflect the v1.7.2 ship. This is the tracked doc to consult for the collab roadmap. (The detailed §15.2 / §15.3 design notes live in Planning/09_COLLABORATION.md, a local-only / gitignored design doc - same convention as 06_TEST_CASES.md - so the inline 09_COLLABORATION links resolve only in the author's working tree.)
  • manual/collab-show-mode.html *(new)* + manual/collab-chat.html *(new)* - dedicated pages for the two new features, with placeholder slots replaced by real screenshots once captured. manual/collaboration.html overview gained a "Session features" section linking to both. manual/collab-lan.html + manual/collab-wan.html got a small callout pointing at the mode picker + chat. manual/docs-index.html gained section-cards for both. manual/navigation.js "Collaboration" sidebar group now includes Show Mode + In-session Chat. manual/index.html "What's New" rewritten for v1.7.2. dedash.py applied across the whole manual (78 em / en dash replacements in 5 files for consistent ASCII hyphens).

v1.7.1

2026-05-14
Bugfix: MidiPilot Stability & v1.7.0 Polish
  • Fixed a MidiPilot crash when starting a new chat while a request was still running - clicking *New Chat* (or being forced to, because a stalled Simple-mode request left the input disabled with no Stop button) deleted the live chat widgets out from under the in-flight streaming/agent callbacks, causing a use-after-free hang-then-crash. New Chat now aborts the running request cleanly first and never deletes widgets mid-callback (BUG-MIDIPILOT-001).
  • Simple mode now has a Stop button - previously only Agent mode could abort an in-flight request; a stalled Simple-mode streaming response had no recovery path except New Chat (which then hit the crash above). Simple mode now mirrors the Agent send→stop button swap, and a stale internal "agent running" flag can no longer permanently swallow a Simple-mode response (BUG-MIDIPILOT-001).
  • MidiPilot empty/unconfigured state is properly centred - before any API key is set, the *Welcome to MidiPilot* panel used to stick to the top with the input field floating in the middle of a big empty gap. It now occupies the chat area and is vertically centred, matching the configured layout (UI-MIDIPILOT-EMPTY-001).
  • "Agent (PR)" mode is no longer shown by default - the experimental AI-as-PR-creator mode (apply-then-review) overlapped with Protocol undo (Ctrl+Z already reverts an entire agent run) and was visible even with collaboration disabled. It is now behind a compile-time flag (default off); the code path stays intact for future re-evaluation (BUG-COLLAB-041).
  • Corrected the WAN pairing-code keyspace math in the manual - collab-cloudflare.html claimed 28⁴ ≈ 614 000 codes; the actual alphabet is 31 characters (A-Z minus I/L/O, plus 2-9) → 31⁴ ≈ 923 000 (BUG-COLLAB-036).
  • Removed a non-existent "Trace" logging level from the docs - the v1.7.0 changelog / README / What's New described five levels ending in *Trace*; the real ladder is Off / Errors / Warnings / Info / Debug. Docs now match the code (BUG-COLLAB-040).
  • Hardened a latent Windows compile-break - <windows.h> was included inside an anonymous namespace in src/main.cpp, which would break the NO_CONSOLE_MODE build the moment anyone enabled it (Win32 typedefs landed in anon-namespace scope). Moved above the namespace (BUG-COLLAB-035).
  • Release packaging robustness - the CI workflow's LICENSE/README/CHANGELOG copy step now has Test-Path guards mirroring release.bat, so a missing top-level file degrades gracefully instead of hard-failing the packaging step under PowerShell's stop-on-error default (BUG-COLLAB-038).
  • Two new unit-test executables - test_ice_config (11 cases) and test_rtc_signaling_token (18 cases) close the highest-value remaining collab-module coverage gaps; full suite is 41/41 green (TEST-1.7.1-001).

🔧 Fixed Bug Fixes

  • Fixed MidiPilot use-after-free crash on New-Chat-while-running (BUG-MIDIPILOT-001) - reported from daily field use: after start, MidiPilot looked busy in Simple mode but the play button never swapped to Stop, the input field was disabled, and there was no way to abort. Clicking *New Chat* to recover hung the app and crashed it. Root cause was three compounding defects in src/gui/MidiPilotWidget.cpp: (A) onNewChat() unconditionally cleared _conversationHistory/_entries and deleteLater()'d every chat-bubble widget with no running-state guard - while a request was live the streaming/step callbacks still held pointers to _thoughtLabel, the streaming bubble and _agentStepsWidget, so deleting them was a use-after-free; (B) Simple mode never showed a Stop button (Stop was Agent-mode-only), so a stalled streaming response had no recovery path; (C) onResponseReceived/onErrorOccurred early-returned on if (_isAgentRunning) - a stale-true flag (missed terminal agent signal) silently swallowed a genuine Simple-mode response and left the input disabled forever, and AiClient::cancelRequest() is intentionally silent (AgentRunner relies on that to avoid a double errorOccurred) so a Simple-mode cancel never self-healed the UI. Fix: a new MidiPilotWidget::abortActiveRequest() helper (agent → AgentRunner::cancel() which self-resets; simple → silent cancelRequest() + explicit UI reset); onNewChat() now bails early when busy - aborts the request, shows "Stopped the running request - click New Chat again to clear", and returns without touching any widget; Simple mode performs the same send→stop button swap as Agent and the _stopButton handler routes through abortActiveRequest() for both modes; the _isAgentRunning guards in onResponseReceived/onErrorOccurred now cross-check _agentRunner->isRunning() and recover from a stale flag instead of dropping the signal. The crash path (New Chat while running) is now structurally impossible. The original *trigger* (why the first Simple request stalled with no terminal signal - likely a provider-side cold-start stall) is not definitively root-caused, but the dead-end is now a recoverable state. Regression risk contained: agent flow self-reset behaviour is unchanged.
  • Fixed "Agent (PR)" mode being shown even with collaboration disabled (BUG-COLLAB-041) - the mode item in the MidiPilot dropdown was gated only by the compile-time MIDIEDITOR_COLLAB_ENABLED (default on), never by the runtime *Settings → Collaboration → Enable collaboration features* master toggle, so it shipped visible regardless. Combined with user feedback that the apply-then-review UX duplicates Protocol's existing Ctrl+Z undo, the mode is now behind a MIDIPILOT_EXPERIMENTAL_AGENT_PR compile-time flag (default 0). The downstream snapshot/diff/PrReviewDialog code path is left compiled in so a developer can flip the flag and rebuild to re-evaluate without re-implementing anything. No QSettings hook, no UI - genuinely hidden, not a discoverable runtime toggle.
  • Fixed <windows.h> included inside an anonymous namespace (BUG-COLLAB-035) - in src/main.cpp the Win32 include sat inside namespace { … }, putting HINSTANCE/LPSTR/WINAPI/EXCEPTION_POINTERS into anonymous-namespace scope. It compiled today only because NO_CONSOLE_MODE is off; enabling that documented build flag would have failed to find the typedefs for the global-scope WinMain. Moved the include above the namespace; the now-redundant second include inside the NO_CONSOLE_MODE block is a header-guarded no-op kept as a clarifying stub. Latent build-break, never fired in a shipped configuration.
  • Fixed wrong pairing-code keyspace math in the Cloudflare manual (BUG-COLLAB-036) - manual/collab-cloudflare.html and Planning/10_DOCUMENTATION.md claimed 28⁴ ≈ 614 000. The rendezvous.js alphabet excludes only 0/O/1/I/L, leaving 31 characters (A-Z minus I/L/O = 23, plus 2-9 = 8), so the real keyspace is 31⁴ ≈ 923 000. Documentation-only; the worker code was always correct.
  • Fixed unreleased-version caption in the logging manual (BUG-COLLAB-037) - manual/logging.html captioned the settings screenshot "Settings → Logging in v1.7.1" while the page shipped in v1.7.0. Corrected to v1.7.0 (the release the logging tab actually shipped in).
  • Removed the non-existent "Trace" logging level from the docs (BUG-COLLAB-040) - the v1.7.0 changelog summary + LOGGING-001 entry, README.md (Features table + Logging section) and manual/index.html (What's New) all described a five-level ladder ending in *Trace*. LoggingConfig::Level has no Trace - it is Off / Errors / Warnings / Info / Debug. All four surfaces now state the real ladder; the size-callout wording was softened from "yellow at Debug, red at Trace" to "escalates with the level" so it stays accurate if thresholds shift. manual/logging.html itself was already correct (its "four severities" + "five levels" split is intentional).
  • Hardened CI release packaging (BUG-COLLAB-038) - .github/workflows/ci.yml copied LICENSE/README.md/CHANGELOG.md into the artifact with bare Copy-Item calls. Under GitHub Actions' default $ErrorActionPreference='stop' for pwsh, a missing top-level file would hard-fail the packaging step - asymmetric with release.bat, which guards the same copies with if exist. Each Copy-Item is now wrapped in if (Test-Path …).
  • Clarified the CMAKE_POLICY_VERSION_MINIMUM scope comment (BUG-COLLAB-039) - the comment in CMakeLists.txt claimed the set(CMAKE_POLICY_VERSION_MINIMUM 3.5) (needed so CMake 4.x accepts libdatachannel's pre-3.5 cmake_minimum_required) was "scoped to this block". CMake has no block scope for if/endif; it is directory-scoped and would leak into the later add_subdirectory(tests). The comment now states this correctly and an explicit unset(CMAKE_POLICY_VERSION_MINIMUM) is issued right after FetchContent_MakeAvailable(libdatachannel) so the loosened policy floor does not bleed into the test tree. Behaviour was already correct in practice (the test CMakeLists has no cmake_minimum_required); this is hygiene + accuracy.

🟡 Medium Improvements

  • UI-MIDIPILOT-EMPTY-001 - MidiPilot empty/unconfigured panel layout - when no API key is configured, the *Welcome to MidiPilot* + *Open Settings* panel previously sat at the top of the sidebar with the (disabled) input field floating in the middle of a large empty gap, because the hidden chat-scroll area's stretch was unclaimed and the input field's default Expanding size policy absorbed the slack. The setup panel now takes the chat area's place (same stretch=1) with inner stretches centring the title/description/button, so the empty state mirrors the configured layout - input/status pinned to the bottom, welcome content vertically centred.

🟡 Medium Test Harness

  • TEST-1.7.1-001 - Two new collab-module test executables - tests/test_ice_config.cpp (11 Qt Test methods: Google STUN-defaults sanity + multi-port coverage, load() fallback for unset / empty / whitespace-only / all-comment values, multi-entry save→load round-trip, blank-line + #-comment stripping in the parser, empty-save reverts to defaults; sandboxed via QStandardPaths::setTestModeEnabled). tests/test_rtc_signaling_token.cpp (18 methods: looksLikeToken prefix-match incl. negative/empty cases, encode prefix shape, offer vs answer distinctness, round-trip preserving SDP + sessionId for both kinds incl. a realistic ICE-candidate SDP, compression shrinks repetitive payloads to <50 % of raw size, and the full decode error matrix: empty / missing-prefix / missing-sessionId / missing-kind / unknown-kind / empty-base64 / corrupted-base64 + errorOut population). Both build with -DMIDIEDITOR_WEBRTC_ENABLED and Qt6::Core only - no libdatachannel link surface. Full suite: 41/41 green.

🟡 Medium Files Modified

  • CMakeLists.txt - version bumped to 1.7.1; unset(CMAKE_POLICY_VERSION_MINIMUM) after the libdatachannel FetchContent + corrected scope comment (BUG-COLLAB-039).
  • src/main.cpp - <windows.h> moved above the anonymous namespace; redundant in-namespace include replaced with a header-guard explanatory stub (BUG-COLLAB-035).
  • src/gui/MidiPilotWidget.h / .cpp - new abortActiveRequest() helper; onNewChat() running-guard; Simple-mode send→stop button swap; _stopButton handler routed through the helper; stale-_isAgentRunning cross-check in onResponseReceived/onErrorOccurred; empty-state setup-panel layout (stretch + inner centring); Agent (PR) item behind MIDIPILOT_EXPERIMENTAL_AGENT_PR compile flag (BUG-MIDIPILOT-001, BUG-COLLAB-041, UI-MIDIPILOT-EMPTY-001).
  • .github/workflows/ci.yml - Test-Path guards around the LICENSE/README/CHANGELOG copies (BUG-COLLAB-038).
  • manual/collab-cloudflare.html, manual/logging.html, manual/index.html, Planning/10_DOCUMENTATION.md - keyspace math, version caption, and "Trace"-level documentation corrections (BUG-COLLAB-036/037/040).
  • README.md - Logging Settings row + Logging section reworded to the real Off → Debug ladder (BUG-COLLAB-040).
  • tests/test_ice_config.cpp, tests/test_rtc_signaling_token.cpp, tests/CMakeLists.txt - two new test executables registered with the Windows PATH-fix block (TEST-1.7.1-001).
  • Planning/03_bugs.md - Round-4 verification fix-status + the BUG-MIDIPILOT-001 field report.

v1.7.0

2026-05-10
Collaboration: PRs, Discord, LAN & WAN Live Sessions
  • Real-time multi-peer Live Sessions over WAN *(Phase 9.8)* - host an editing session, share a 4-character pairing code, and up to 8 joiners can co-edit a single MIDI file in real-time over WebRTC + DTLS, peer-to-peer after a one-shot Cloudflare-hosted rendezvous handshake. Joiner-initiated protocol (every joiner gets its own offer/answer pair), production-tested on 4 PCs at 1.8 KB/s host-out peak. Auto-reconnect on transport failure, ghost-peer protection via 10 s heartbeat / 30 s silence-deadline, and offline-edit recovery as a single state-diff on rejoin (COLLAB-WAN-001).
  • LAN Live Sessions *(Phase 9.5)* - same real-time co-editing over the local network with zero external dependencies. Multicast peer discovery, TCP transport, automatic full-file transfer to joiners on connect, and a manual-IP fallback for networks where multicast is blocked. Sessions show up in the *Join LAN Live Session…* dialog with one click (COLLAB-LAN-001).
  • Async PR workflow with smart-paste tokens *(Phase 9.1)* - *Edit → Create PR…* packages the current diff against a chosen parent into a self-contained token. Collaborators paste it into their editor with Ctrl+V and the *Review PR…* dialog opens with per-hunk cherry-pick. Identical to a code-review loop, no GitHub account needed, no server (COLLAB-PR-001).
  • Discord-channel notifications for PRs *(Phase 9.2)* - drop a webhook URL into *Settings → Collaboration → Discord webhook*; *Create PR* then posts a rich embed plus the smart-paste token to the channel, with a per-share *Don't post this one* opt-out for private one-offs (COLLAB-DISCORD-001).
  • State-diff sync, not an op-log - Live Sessions broadcast a hash-keyed snapshot diff every sync tick instead of streaming individual operations. Whatever a peer edited while disconnected lands as one catch-up diff on rejoin - no missed events, no replay-order conflicts. Free property: a forced network drop during edits round-trips cleanly to the host as soon as the channel reopens (COLLAB-DIFF-001).
  • Connection Test diagnostic - two-stage health probe (/health GET against the rendezvous URL + an in-process two-transport DTLS loopback) returns a quality grade in under six seconds. Surfaces the silent-firewall-block class of failures on Windows that previously looked like *"the browser works but the app times out"* (COLLAB-DIAG-001).
  • Returning-peer reconciliation - when a peer rejoins a session whose history they already have, MidiEditor offers a *Fast-forward* bundle of just the commits since the fork point instead of re-transferring the whole file. The new *Welcome Back* dialog summarises the divergence and lets the peer choose Fast-forward / Full reload / Cancel (COLLAB-FF-001).
  • New logging subsystem with dedicated Settings tab and manual page - five radio-selectable levels (Off / Errors / Warnings / Info / Debug) with a live preview of exactly which log lines you'll see at each level, a size-callout warning that escalates as the level climbs, 10 MB file rotation with three numbered backups (40 MB total budget), per-category overrides for advanced troubleshooting, and an *Open log file* shortcut. The new manual/logging.html walks through every level with sample output and "when to attach a log to an issue" steps (LOGGING-001, DOC-LOGGING-001).
  • Lock side panels during playback - opt-in *Settings → System & Performance → Playback* toggle that restores the legacy "panels disabled during playback" behaviour for users who prefer fewer accidental edits while listening. Default off - panels stay fully interactive (PLAYBACK-LOCK-001).
  • Comprehensive Collaboration manual section - eight new manual pages cover the full workflow: a top-level *Collaboration* overview with mode-selector cards, plus dedicated PR / Discord / LAN / WAN / Settings / Cloudflare-self-host pages with synchronised SVG animations of each protocol's handshake, OBS recordings of two PCs editing simultaneously, and a Cloudflare-Worker walkthrough that goes from blank dashboard to deployed worker in a single screenshot strip (DOC-COLLAB-001).
  • Six new test executables - test_midi_diff, test_pr_bundle, test_drum_kit_preset, test_gp_binary_reader, test_mml_midi_writer, test_three_mle_parser add deterministic coverage for the collaboration diff pipeline, the Guitar Pro binary reader, the MML round-trip, and the 3MLE parser; the largest single-release expansion of the test harness so far (TEST-1.7-001).
  • Cloudflare rendezvous worker shipped under cloudflare/ - the small stateless KV-backed Worker that swaps SDP blobs between WAN peers ships in-tree with wrangler.toml, a deploy walkthrough in cloudflare/README.md, and v3 of the joiner-initiated multi-peer protocol baked in. Self-host it on the free tier in under five minutes if you don't want to use the shared Happy Tunes endpoint (CLOUDFLARE-001).

✨ Added New Features - Collaboration core

  • COLLAB-PR-001 - Asynchronous PR workflow with smart-paste tokens (Phase 9.1) - new top-level *Edit → Create PR…* and *Edit → Review PR…* commands plus a CollabService singleton that owns a per-file .midiedit-collab.json sidecar tracking the local commit history (parentHash chain, per-hunk MidiDiff payloads, knownPeers list). The PR token is a base64 + zlib-compressed self-contained bundle (PrBundle) carrying the parent SHA, a list of MidiDiff hunks, the author's display name + machine UUID, and an optional cover message. Pasting it triggers MainWindow::paste() to detect the prefix, open PrReviewDialog with a per-hunk cherry-pick checklist, apply selected hunks via PrApply inside one Protocol action (single Ctrl+Z undoes the whole import), and append the merged commit to the local history. The token format trims trailing/leading whitespace and surrounding triple/single backticks before parsing so Discord-pasted tokens "just work".
  • COLLAB-DISCORD-001 - Discord webhook integration for PR distribution (Phase 9.2) - new *Settings → Collaboration → Discord webhook* field plus an Also post to Discord checkbox in PrCreateDialog. When the URL is set and the checkbox is on, *Create PR* posts a Discord rich embed (file name, parent commit short-hash, author display name, hunk count) followed by a code-fenced smart-paste token via WebhookClient. A per-share *Don't post this one* opt-out is provided for private one-offs. Webhook URL is validated against the https://discord.com/api/webhooks/<id>/<token> schema; a missing or malformed URL silently disables the checkbox.
  • COLLAB-LAN-001 - LAN Live Sessions (Phase 9.5) - new *Edit → Start LAN Live Session…* and *Edit → Join LAN Live Session…* commands wire the host editor to LanLiveSession::startHosting() (multicast announcement on 239.255.42.99:45301, TCP listener on a random ephemeral port, full-file transfer to each joiner on connect) and the joining editor to a session-list dialog backed by LanDiscovery. Sync uses the same hash-keyed MidiSnapshot + MidiDiff pipeline as the PR workflow, broadcast every 1.5 s when the local snapshot hash changes. Joiners receive the host's file on connect, apply incoming hunks through PrApply inside per-message Protocol actions, and rebroadcast their own diffs to the host. Manual-IP fallback for blocked-multicast networks: hosts copy a lan://<ip>:<port> URL from the start dialog, joiners paste it into the join dialog. Mode is non-invasive - disabling collab via the master toggle in Settings hides every menu entry, and -DMIDIEDITOR_ENABLE_COLLAB=OFF produces a binary with zero Collab* symbols.
  • COLLAB-WAN-001 - Multi-peer WAN Live Sessions over WebRTC + Cloudflare rendezvous (Phase 9.8) - Phase 9.8's joiner-initiated protocol scales the original 1:1 WAN session to up to 8 joiners per host. Each joiner is the WebRTC initiator (creates its own offer SDP), the host is the responder (creates an answer per incoming offer), and the Cloudflare Worker holds an explicit joiner-index:CODE key plus per-joiner joiner-offer:CODE:<id> and host-answer:CODE:<id> slots. Host polls /joiner-offers every 2 s and processes new offers in arrival order. The WebRtcLiveServer owns one WebRtcTransport per joiner (QHash<QString, WebRtcTransport*>); each transport has its own DTLS credentials and SCTP data channel, so every peer's MIDI traffic is encrypted end-to-end and never traverses Cloudflare after the handshake. SDPs are bytewise tagged with the joinerId and DTLS fingerprints, so signalling forgery is structurally caught at handshake time. The host pre-flights /health against the rendezvous URL before publishing the session and aborts cleanly with a "rendezvous unreachable" message if it doesn't resolve. Peer count is capped at MAX_PEERS_PER_SESSION = 8 server-side; a full session returns HTTP 503 with a clear "session is full" message to the joiner. Production-tested 2026-05-08 on a 1-host + 3-joiner WAN session at 1.8 KB/s peak host-out - comfortable headroom on a 50 Mbit upstream.
  • COLLAB-DIFF-001 - MidiSnapshot + MidiDiff + PrApply state-diff pipeline - the live-sync engine doesn't stream MIDI events as they happen; it diffs the current snapshot against the last-broadcast snapshot every 1.5 s tick. MidiSnapshot::fromFile() extracts a sorted list of every event's identity tuple (channel, tick, type, key/control/program), MidiHash Blake2b's it for the wire-level dedup key, and MidiDiff::between(prev, next) produces a list of add / remove / modify hunks against tuple identity. PrApply::apply() re-finds matching events in the receiver's file by tuple (no fragile pointer or insertion-order tracking) and applies the hunks inside one Protocol::startNewAction block. Free property: any number of edits made while disconnected become exactly one catch-up diff on rejoin, no replay ordering needed and no possibility of double-application. Verified 2026-05-08: deleting notes on a joiner during a forced WAN drop propagated atomically to the host the moment LIVE returned.
  • COLLAB-FF-001 - Returning-peer reconciliation with fast-forward bundles (Phase 9.5g) - when a peer rejoins a session whose parentHash they already carry in their .midiedit-collab.json sidecar, HistoryReconciliation::commitsSinceFork() extracts just the commits since the divergence point and the host transfers a Fast-forward bundle (a list of PrBundle hunks, gzipped) instead of the full file. WelcomeBackDialog summarises the divergence (commits behind, last-known author, file-size delta) and lets the peer choose *Fast-forward* / *Full reload* / *Cancel*. Far-diverged peers (ancestorHash not found) fall back to a full file transfer with an inline "Unrelated histories - full reload required" notice.
  • COLLAB-DIAG-001 - Connection Test diagnostic - new *Test connection* button in *Settings → Collaboration → WAN Rendezvous*. Stage 1 issues a GET /health against the configured rendezvous URL and checks for a v3-protocol response. Stage 2 spins up an in-process two-WebRtcTransport DTLS loopback with host-only ICE candidates (no STUN), reports round-trip latency, and grades the result A/B/C. Run-time is bounded at six seconds total. Surfaces the *silent firewall block on unsigned EXEs* class of Windows failures that previously looked like "browser-OK + Qt-app-times-out" by reporting a stage-1 success and a stage-2 timeout.
  • COLLAB-IDENTITY-001 - Display name + machine UUID (Phase 9.1d) - new CollabIdentity::machineUuid() derives a stable installation ID once on first run from the system username + hostname + a QUuid::createUuid() salt, stored next to QSettings. Combined with the user-editable display name in *Settings → Collaboration → Display name*, this lets multiple PCs with the same display name still get distinct authorship in commit history without any registration flow. Used by every feature that emits an authored event: PrBundle author field, WebRtcStartDialog "host name" field, LanDiscovery announcement payload, History-tab commit attribution. A re-installed editor on the same machine keeps the same UUID so the user's commits stay attributed to them.
  • COLLAB-HISTORY-001 - Per-file collab history + History tab - new CollabHistoryWidget (always visible alongside the existing Tracks / Channels / Event / Protocol tabs when a sidecar exists) lists every commit in the local history with author, timestamp, hunk count, parent hash short-form, and a per-row "Use as parent for new PR" action. Sidecar is written through QSaveFile for atomic on-disk updates and tolerant field parsing for forward compatibility. History compaction is intentionally out of scope for v1.7; sidecars stay growable.
  • COLLAB-AUTORECONNECT-001 - Auto-reconnect on transport failure (WAN) - new *Settings → Collaboration → Auto-reconnect on transport failure* checkbox (default off) plus a *Max retry attempts* slider (1-5, default 2). When enabled, a Live session whose WebRTC channel dies restarts the rendezvous flow with the same code, ~2 s back-off between attempts. Each peer's reconnect is independent (one joiner's failed retry doesn't kick the others), and a sequence-counter check inside the retry lambda guarantees that pressing *Leave* between failure and the back-off cancels the pending restart cleanly. Caveat surfaced in the manual: won't recover when the host restarts and gets a new code - joiners enter the new code manually in that case.
  • COLLAB-WORK-ON-COPY-001 - Hosting safety: work-on-copy default - when *Settings → Collaboration → Work on a copy when hosting* is on (default), starting a Live session writes <name>_shared.mid into Documents/MidiEditor_AI/shared/ and switches the editor to that copy. The original file on disk is untouched until the user opts to save back manually. Non-MIDI source formats (Guitar Pro .gp3/.gp4/.gp5/..., MML, MusicXML, MuseScore - all import-only formats) automatically rewrite the shared-copy suffix to .mid so the re-open flow doesn't try to re-import MIDI bytes through the wrong importer; the original on disk stays in its native format.

✨ Added New Features - Logging

  • LOGGING-001 - Logging subsystem with dedicated Settings tab and manual page - new LoggingConfig singleton owns a five-level ladder (Off / Errors / Warnings / Info / Debug - Off being the no-output level and the rest mapping directly onto Qt's four built-in severities), per-category Qt logging-rule overrides (midieditor.collab.*, midieditor.gui.*, midieditor.midi.*, midieditor.ai.*), and a 10 MB-rotated file logger (midieditor_ai.log + three numbered backups, 40 MB total disk budget). The Settings dialog grew a new *Logging* tab built around five radio buttons (Off untinted, Errors red, Warnings orange, Info blue, Debug gray) with a four-cell cumulative severity bar to the right of each row, a live preview text-block showing exactly which sample log lines you'll see at the selected level, and a size-callout box that escalates from green through yellow to red as the level climbs, with an estimated MB/hour figure. *Open log file* opens the rotated log next to MidiEditorAI.exe in the OS file manager. *Verbose collab logging* checkbox is an overlay that promotes only the midieditor.collab.* categories to debug - handy for reproducing a session bug without flipping the global level. The Collaboration tab keeps a smaller mirror of the same controls so users debugging a collab issue don't have to switch tabs. New manual/logging.html walks through each level with sample output, size estimates per session-hour, and step-by-step "when to attach a log to an issue" guidance.
  • PLAYBACK-LOCK-001 - Lock side panels during playback (opt-in) - new *Settings → System & Performance → Playback → Lock side panels during playback* toggle (default off, key playback/lock_panels). When on, the Tracks / Channels / Event / Protocol tabs are disabled for the entire duration of playback and recording - restoring the legacy v1.4.1-and-earlier behaviour for users who prefer fewer accidental edits while listening. Default-off because the v1.4.2 redesign that made panels stay interactive during playback has been the documented behaviour for a year and is what users now expect.

🟡 Medium Documentation

  • DOC-COLLAB-001 - Eight-page Collaboration manual section - new manual/collaboration.html overview with a fixed 2x2 mode-selector grid (PR + Discord on top, LAN + WAN below) and one-line "when to use" callouts; new manual/collab-pr.html walking through Create PR / Review PR with per-hunk cherry-pick screenshots; new manual/collab-discord.html linking to the official Discord webhook documentation plus the embed format we post; new manual/collab-lan.html with multicast vs manual-IP screenshots, an animated SVG of the LAN handshake (host announce → joiner discovery → TCP file-transfer → state-diff sync), troubleshooting copy for blocked-multicast networks, and OBS recordings of two PCs editing simultaneously; new manual/collab-wan.html with a matching two-track SVG animation of the WAN flow (orange Cloudflare handshake on top, green direct DTLS once the data channel opens), Connection Test screencap (collab_connection_test.webm), full troubleshooting section for the silent-firewall-block class of failures; new manual/collab-cloudflare.html self-host walkthrough that goes from blank Cloudflare dashboard → KV namespace bound → worker deployed in a single screenshot strip, with a 12 s SVG animation visualising every step of the v3 protocol synchronised against the step-list; new manual/collab-settings.html reference for every toggle on the Collaboration tab - Identity, Hosting safety, WAN Rendezvous, Discord webhook, Logging - including the Connection Test video and per-setting *Default* / *When to change* notes. The manual sidebar nav (navigation.js) gained a *Collaboration* group in the Editor section so all eight pages are one click apart.
  • DOC-LOGGING-001 - Dedicated logging manual page - new manual/logging.html covers the five severity levels with sample log lines per level (colour-coded <span class="lvl-CRI/WRN/INF/DBG/TRC">), the cumulative level-stack model, green/yellow/red size-callout boxes with estimated MB/hour at each level, the rotation policy (10 MB cap × 3 backups = 40 MB total), per-category overrides for advanced users, and a "when to attach a log to an issue" checklist. Linked from the new Logging Settings tab via *Read the dedicated Logging manual page* and from the Collaboration overview via *Verbose collab logging*.
  • DOC-PLAYBACK-001 - Playback page refresh - manual/playback.html gained a *Live Side Panels During Playback* section documenting the opt-in lock toggle (PLAYBACK-LOCK-001) with a screenshot of the new Settings → System & Performance → Playback panel, and the previous HTML4 align="right" image floats were converted to centered block-level images so the page renders cleanly on narrow viewports.
  • DOC-FOOTER-001 - Manual footer alignment - manual/site.css now centers the GitHub-repo logo + text combo in the .footer-links flex row with align-items: center; so the icon-bearing link doesn't drop below the baseline of the text-only links on every manual page.

🟡 Medium Test Harness

  • TEST-1.7-001 - Six new test executables - tests/test_midi_diff.cpp (snapshot diff round-trip, hash equality, modify-vs-add-vs-remove classification, track-field round-trip across all 10 event types), tests/test_pr_bundle.cpp (token build + parse, prefix detection, base64 + zlib round-trip, malformed token rejection, whitespace-stripping pre-parse), tests/test_drum_kit_preset.cpp (FFXIV percussion preset coverage map, edge-case GM keys), tests/test_gp_binary_reader.cpp (GP3/GP4/GP5 chunk parse, byte-order, fixture-driven note recovery), tests/test_mml_midi_writer.cpp (MML round-trip including dotted notes and ties), tests/test_three_mle_parser.cpp (3MLE channel header parse + chord-stack lowering). All six wired into tests/CMakeLists.txt with the WIN32 PATH-injection block so Qt6Test.dll resolves under ctest.

🟡 Medium Cloudflare Worker

  • CLOUDFLARE-001 - Rendezvous Worker shipped in-tree under cloudflare/ - cloudflare/rendezvous.js (v3 protocol, joiner-initiated multi-peer, explicit joiner-index:CODE key for KV list()-consistency, soft MAX_PEERS_PER_SESSION = 8 cap, 5-minute TTL), cloudflare/wrangler.toml (KV binding scaffolding), and cloudflare/README.md (web-UI deploy walkthrough + CLI deploy via wrangler + free-tier capacity table). The default rendezvous URL in the editor points at the shared Happy Tunes endpoint https://midi-rdv.happytunesai.workers.dev/; users who don't want to share that endpoint can self-host on the Cloudflare free tier in under five minutes following the in-tree README. SDPs contain public IP + port info; the worker never logs them, and they expire after five minutes.

📝 Notes Architecture / Technical Notes

  • -DMIDIEDITOR_ENABLE_COLLAB=OFF clean opt-out - the entire collab subsystem is gated behind a single CMake option and a single master toggle in Settings. Building with the option off produces a binary with zero references to any Collab*, Lan*, WebRtc*, or Pr* symbol; every menu entry, settings tab, and dialog disappears at compile time. This is a non-invasive guarantee per 09_COLLABORATION §4 so users uninterested in collab pay no binary-size, no startup-time, and no attack-surface cost.
  • Single Protocol action per incoming hunk batch - PrApply::apply() always runs inside one Protocol::startNewAction("Live edit from <author>") block, so a single Ctrl+Z undoes a remote peer's whole sync tick atomically. Same for the "fast-forward bundle on rejoin" path: an entire catch-up diff unwinds with one undo.
  • Cloudflare KV list() eventual-consistency workaround - KV's list() operation has unbounded eventual-consistency lag (observed ~14+ s before a freshly put()'d key appears in list() results from the same PoP). The v3 protocol routes around this by maintaining an explicit joiner-index:CODE key written + read with get() / put() (read-your-writes consistent within a single PoP), eliminating the 30+ second startup delay we observed in real WAN sessions on v2.

✨ Added Files Added - Collaboration core (src/collab/)

  • CollabService.{h,cpp} - singleton owning the per-file sidecar, commit history, and the active LiveSession handle.
  • CollabIdentity.{h,cpp} - stable display-name + machine-UUID derivation, stored next to QSettings.
  • CollabHistoryFile.{h,cpp} - atomic .midiedit-collab.json reader/writer using QSaveFile, tolerant field parsing.
  • MidiSnapshot.{h,cpp} - per-file event-tuple snapshot extraction (deterministic, hash-stable).
  • MidiHash.{h,cpp} - Blake2b digest of a MidiSnapshot for the wire-level dedup key.
  • MidiDiff.{h,cpp} - diff (prev, next) snapshots into add / remove / modify hunks with stable identity tuples.
  • PrApply.{h,cpp} - apply a hunk list to a target file inside one Protocol action; defensive trackIdx = 0 fallback for older-build hunks missing the track field.
  • PrBundle.{h,cpp} - base64 + zlib smart-paste token build/parse, looksLikeToken detection, prefix-stripping.
  • HistoryReconciliation.{h,cpp} - fork-point lookup + commitsSinceFork() slice extraction for fast-forward bundles.
  • LanDiscovery.{h,cpp} - multicast announce + listen on 239.255.42.99:45301 with multi-interface IPv4 handling.
  • LanTransport.{h,cpp} - TCP transport for LAN sessions with size cap and per-iteration null guards.
  • LanLiveSession.{h,cpp} - host/joiner state machine, snapshot-tick broadcaster, heartbeat timer (10 s ping / 30 s deadline), and machineId-based ghost-peer dedup on the hello handshake.
  • IPeerLink.h - peer-link interface with peerMachineId() / setPeerMachineId() / lastSeenMs() / touchLastSeen().
  • ILiveServer.h / ILiveClient.h - abstract roles; LAN and WAN have parallel implementations.
  • WebRtcTransport.{h,cpp} - libdatachannel wrapper with std::atomic<bool> _alive callback-vs-dtor race guard.
  • WebRtcLiveServer.{h,cpp} - host-side multi-transport server (QHash<QString, WebRtcTransport*>, joiner-poll loop).
  • WebRtcLiveClient.{h,cpp} - joiner-side initiator that creates the offer SDP and posts it to the rendezvous Worker.
  • RtcRendezvousClient.{h,cpp} - REST client for the Cloudflare Worker (POST /session, POST /joiner-offer, GET /host-answer/<id>).
  • RtcSignalingToken.{h,cpp} - fallback inline-token format for environments without a rendezvous URL.
  • IceConfig.{h,cpp} - ICE servers + gathering-timeout knobs.
  • WanConnectionTest.{h,cpp} - two-stage /health + DTLS-loopback diagnostic with quality grading.
  • WebRtcSmokeTest.{h,cpp} - in-process bring-up smoke test (development scaffold, not a public feature).
  • WebhookClient.{h,cpp} - Discord webhook embed builder + POST.

✨ Added Files Added - Collaboration UI (src/gui/collab/)

  • CollabSettingsWidget.{h,cpp} - *Settings → Collaboration* tab (Identity / Hosting safety / WAN Rendezvous / Discord webhook / Logging).
  • CollabHistoryWidget.{h,cpp} - *History* tab next to Tracks / Channels.
  • LanLiveStartDialog.{h,cpp} / LanLiveJoinDialog.{h,cpp} - host and join flows for LAN.
  • WebRtcStartDialog.{h,cpp} / WebRtcJoinDialog.{h,cpp} - host and join flows for WAN with the 4-character pairing code.
  • PrCreateDialog.{h,cpp} / PrReviewDialog.{h,cpp} - async PR creation and per-hunk cherry-pick.
  • ReturningPeerDialog.{h,cpp} / WelcomeBackDialog.{h,cpp} - fast-forward vs full-reload choice on rejoin.

✨ Added Files Added - Logging

  • src/LoggingConfig.{h,cpp} - central logging singleton, per-category rule synthesis, file rotation.
  • src/gui/LoggingSettingsWidget.{h,cpp} - radio-button + colour-bar level picker with live preview and size callout.

✨ Added Files Added - Manual

  • manual/collaboration.html, manual/collab-pr.html, manual/collab-discord.html, manual/collab-lan.html, manual/collab-wan.html, manual/collab-cloudflare.html, manual/collab-settings.html, manual/logging.html - see *Documentation* above for content breakdown.
  • manual/screenshots/collaboration_settings.png, collaboration_menu.png, collaboration_tab.png, Create-PR_dialog.png, Review-PR.png, DiscordWebhookURL.png, discord_pr_message.png, Cloudflare_worker.png, cloudflare_KV_menu.png, cloudflare_add_binding.png, cloudflare_add_binding_menu.png, cloudflare_add_binding_variable_kv_namespace.png, cloudflare_code_offers.png, cloudflare_collabsettings_own_url.png, cloudflare_deploy_hello_world.png, cloudflare_edit_code_hello_world.png, cloudflare_example_url.png, cloudflare_example_worker_kv_namespace_workflow.png, cloudflare_hello_world.png, cloudflare_save_code.png, cloudflare_worker_kv_create.png, collab_join_lan_session.png, collab_lan_session_midi_download_request.png, collab_live_lan_client.png, collab_live_lan_host.png, live_lan_session.png, playback_side_panel_lock.png, settings_logging.png - manual screenshots.
  • manual/screenshots/collab_connection_test.webm, live_LAN_pc1.webm, live_LAN_pc2.webm, live_WAN_pc_1.webm, live_WAN_pc2.webm - OBS-recorded clips embedded in the LAN, WAN, and Settings pages.

✨ Added Files Added - Tests

  • tests/test_midi_diff.cpp, tests/test_pr_bundle.cpp, tests/test_drum_kit_preset.cpp, tests/test_gp_binary_reader.cpp, tests/test_mml_midi_writer.cpp, tests/test_three_mle_parser.cpp - see *Test Harness* above for coverage detail.

✨ Added Files Added - Cloudflare

  • cloudflare/rendezvous.js - Cloudflare Worker source (v3 joiner-initiated protocol).
  • cloudflare/wrangler.toml - KV binding scaffold for self-host.
  • cloudflare/README.md - web-UI + CLI deploy walkthrough, free-tier capacity table.

🟡 Medium Files Modified

  • CMakeLists.txt - version bumped to 1.7.0; MIDIEDITOR_ENABLE_COLLAB build option added with the corresponding target_compile_definitions gates; new test executables registered.
  • src/main.cpp - LoggingConfig::initialize() and category rule application happen before any QObject is constructed so startup is logged at the user-selected level.
  • src/midi/MidiFile.{h,cpp} - exposes the immutable iteration views needed by MidiSnapshot::fromFile() and emits a fileChanged() signal that LanLiveSession uses to drive the snapshot-tick poll.
  • src/ai/MidiEventSerializer.{h,cpp} - every event-type serializer (note, cc, pitch_bend, program_change, tempo, time_sig, key_sig, text, chan_pressure, key_pressure) now consistently round-trips the track field with a null-pointer guard, so MidiDiff matches events across builds correctly and the receiver-side PrApply can route hunks to the correct track.
  • src/gui/MainWindow.{h,cpp} - top-level menu wiring for *Edit → Create PR…* / *Review PR…* / *Start LAN Live Session…* / *Join LAN Live Session…* / *Start WAN Live Session…* / *Join WAN Live Session…*; paste() now detects smart-paste tokens via PrBundle::looksLikeToken (whitespace-trimmed and backtick-stripped) and routes them through PrReviewDialog; prepareHostFile() writes the work-on-copy with the correct .mid suffix when the source is import-only; MidiPilotWidget integration unchanged for non-collab users.
  • src/gui/MatrixWidget.{h,cpp} - paint-path additions for the optional remote-cursor overlay used by Live Sessions (collab-only - invisible outside an active session).
  • src/gui/MidiPilotWidget.{h,cpp} - minor wiring to coexist with the Collaboration sidebar tab.
  • src/gui/SettingsDialog.cpp - registers the new *Collaboration* and *Logging* tabs; *System & Performance* gained the *Lock side panels during playback* checkbox.
  • manual/playback.html - added *Live Side Panels During Playback* lock-toggle section; converted HTML4 align="right" image floats to centered block-level images so the dialog screenshot no longer overflows into the footer.
  • manual/navigation.js - *Collaboration* group inserted into the Editor section of docGroups.
  • manual/site.css - .footer-links gained align-items: center; so the GitHub-repo icon sits on the same baseline as text-only links across every manual page.
  • manual/docs-index.html - new *Collaboration* card linking to collaboration.html plus a *Logging* card linking to logging.html.
  • tests/CMakeLists.txt - registered the six new test executables with the WIN32 PATH-injection block.

v1.6.1

2026-05-03
Hotfix: rebrand polish, FFXIV onboarding & SoundFont Equalizer
  • FFXIV SoundFont Equalizer (Phase 39) - per-instrument volume mixer with live preview - new modal dialog under *Tools → FFXIV SoundFont Equalizer…* and *Settings → MIDI I/O → FluidSynth → Open FFXIV SoundFont Equalizer…* with one row per FFXIV bard preset (Lute, Harp, Piano, Flute, Trumpet, GM Drum Kit, electric guitars, …), 0-200 % gain slider + mute + per-row Preview button, master gain, search, and unlimited user presets persisted to QSettings (FFXIV-EQ-001).
  • MidiEditor AI brand theme is the new default - fresh installs boot into the brand theme instead of *Follow OS*; existing users keep their selection. The *Appearance* settings dropdown now labels the entry MidiEditor AI (Default) so it is unambiguous (THEME-DEFAULT-001).
  • Optional startup update check (upstream 366a92f) - new *Settings → Performance → Updates → Check for updates at startup* checkbox, default on, key updater/check_on_startup, useful for offline / privacy-sensitive installs (UPDATER-OPTOUT-001).
  • Sharper piano roll & protocol icons at 125 / 150 % scaling (upstream 8997ad7) - MatrixWidget and ProtocolWidget now allocate their cached pixmaps at the device pixel ratio; closes upstream issue #53 (RENDER-DPR-001).
  • Edit cursor + playback marker in the FFXIV Voices lane - the bottom voice-load chart now mirrors MatrixWidget's vertical edit cursor and overlays the live MidiPlayer::timeMs() position in brand cyan during playback (UX-VOICE-LANE-001).
  • FFXIV Voice Lane auto-shows / auto-hides with FFXIV SoundFont Mode - new *View → Auto-show FFXIV Voice Lane with FFXIV SoundFont Mode* toggle (default on) makes the voice-load lane appear automatically when you flip FFXIV Mode on (toolbar XIV button or settings checkbox) and hide again when you flip it off, so you only see the chart where you actually need it. The existing *Show FFXIV Voice Lane* toggle is kept and now means "always show" - either switch alone is enough to make the lane visible. With both off the lane stays hidden until opted back in (UX-VOICE-LANE-002).
  • Two-row toolbar with the full curated MidiEditor AI layout is the new default - fresh installs now boot with Customize Toolbar already enabled, double-row mode on, and *every* tool from the curated layout visible (editing / AI / FFXIV on row 1, transport / view / status widgets on row 2) instead of the cramped, stripped-down single row. New users see the whole feature set up front and can hide individual entries afterwards from *Settings → Customize Toolbar*. Existing users keep whatever layout they already have (TOOLBAR-DEFAULT-001).
  • Stale midieditor.ico shipped next to v1.6.0 .exe - the loose 362 KB legacy ICO has been replaced in-place with the new branded logo so installers and self-update flows pick up the correct icon (B-FIELD-001).
  • FFXIV SoundFont onboarding silently dropped the downloaded SoundFont - enabling FFXIV Mode on a clean install downloaded FF14-c3c6-fixed.sf2 but never registered it, never switched the MIDI output to FluidSynth, and dropped any *other* SoundFont fetched in the same dialog session; the helper now force-initialises the engine, switches the output, registers every downloaded path, and persists the new state immediately (B-FIELD-002).
  • Crash when deleting the only Tempo / TimeSignature event (upstream 21fe86b crash slice) - MainWindow::deleteSelectedEvents() now refuses to remove the final tick-0 anchor on channels 17 / 18 while still removing every other selected meta event (B-UPSTREAM-CRASH-001).
  • FFXIV Channel Fixer dropped unmatched percussion off channel 9 (upstream a35f1ee) - Rebuild mode no longer reassigns a generic "Drums" / "Perc" track away from channel 9 when the majority of its notes already live there (B-FFXIV-PERC-001).

✨ Added New Features

  • FFXIV-EQ-001 - FFXIV SoundFont Equalizer (Phase 39) - new per-instrument volume mixer for the FFXIV bard SoundFont. The dialog is reachable from two equivalent entry points (both auto-enabled when FFXIV SoundFont Mode is on, disabled otherwise): the *Tools* menu and the new *Open FFXIV SoundFont Equalizer…* button in *Settings → MIDI I/O → FluidSynth*. Each of the 28 rows (drum kit first, then 27 melodic / percussion / guitar presets in canonical FFXIV order) carries a 0-200 % gain slider, numeric spinbox, mute checkbox, per-row reset, and a ▶ Preview button that auditions the change with a short C-D-E-G arpeggio (or kick / snare / hat / crash for the GM Drum Kit row). The header strip exposes a preset combo (built-in *FFXIV Default* plus user presets), *Save As…* / *Delete* / *Reset to Built-in*, a master gain slider, and a search filter. *Apply* persists to the active preset, *Cancel* reverts to the snapshot taken on construction. Audio path: a new singleton FfxivEqualizerService owns the active mix and emits mixChanged() on every slider tick; FluidSynthEngine connects this signal during initialize() and pushes the combined (bard + EQ) GEN_ATTENUATION per channel via the new applyFfxivEqualizerAttenuation() helper, so slider edits affect the very next note in both live playback and offline export. A muted slot is encoded as a sentinel attenuation past the audible range (1440 cB) so silenced presets stay silent in rendered WAV / FLAC / OGG / MP3 too. The preview channel applies EQ unconditionally - even when FFXIV Mode is off - so the audition truly reflects the slider position. Persistence keys: FFXIV/equalizerActivePreset (string) plus FFXIV/equalizerPresets/<name>/master and FFXIV/equalizerPresets/<name>/programs/<int> (QStringList["gainStr","muted01"]). The built-in *FFXIV Default* preset cannot be overwritten or deleted; *Save As…* always creates a new user preset, and deleting the active user preset gracefully falls back to the built-in. Test coverage: 13 Qt-Test methods in tests/test_ffxiv_equalizer_service.cpp (unknown-program fallback, master-gain scaling, preset round-trip, built-in protection, drum-kit row ordering, …) - all green under ctest.
  • UPDATER-OPTOUT-001 - Optional startup update check *(upstream 366a92f)* - new Settings → Performance → Updates → Check for updates at startup checkbox (default on, key updater/check_on_startup). When unchecked, MidiEditor AI no longer pings the GitHub release API on launch - useful for offline / air-gapped / privacy-sensitive installations. Default behaviour is unchanged for existing users.
  • UX-VOICE-LANE-001 - Edit cursor and playback marker in the FFXIV Voices lane - the bottom FFXIV Voices chart now mirrors the vertical playback / edit cursor that the piano roll above it shows, so it is finally obvious *where* in the song you are when scanning the voice-load graph. The edit cursor (MidiFile::cursorTick()) is drawn as a thin gray line with a small triangle at the top to match MatrixWidget; while playback is running, the live MidiPlayer::timeMs() position is overlaid in brand cyan. The lane redraws on every cursorPositionChanged() for static moves and on MidiPlayer::playerThread()::timeMsChanged(int) during playback - reconnected per play() / record() because Windows recreates PlayerThread each call.

🟡 Medium Improvements

  • THEME-DEFAULT-001 - MidiEditor AI brand theme is the new default for fresh installs - first-run installs now boot into the brand theme (deep navy base, brand cyan focus rings, violet AI/MidiPilot accents) introduced in 1.6.0 instead of *Follow OS*. Existing users keep whichever theme they had selected - Appearance::loadSettings() only applies the new default when the theme QSettings key is missing, and that key is written back on every save, so anyone who has launched any previous version already has an explicit value. Change in src/gui/Appearance.cpp: settings->value("theme", ThemeSystem)settings->value("theme", ThemeBrand). The *Settings → Appearance → Theme* dropdown entry is now labelled MidiEditor AI (Default) so the default is unambiguous.
  • RENDER-DPR-001 - Sharper piano roll and protocol icons at fractional scaling *(upstream 8997ad7, closes upstream issue #53)* - the cached MatrixWidget back-buffer is now allocated at the actual device pixel ratio (size() * dpr) and tagged with setDevicePixelRatio(dpr) instead of being sized in logical pixels and upscaled by Qt at paint time. The same DPR-aware path is now used for the protocol step icons. The visible difference is biggest at 125 % / 150 % Windows scaling, where the piano roll and protocol thumbnails were previously slightly blurry. No behaviour change at 100 % / 200 %.
  • TOOLBAR-DEFAULT-001 - Curated two-row toolbar with all tools visible is the new default for fresh installs - first-run installs now boot with toolbar_customize_enabled = true, toolbar_two_row_mode = true, and the curated default order baked into src/gui/LayoutSettingsWidget.cpp (getDefaultToolbarOrder(), getDefaultToolbarEnabledActions() now returns the full order so every tool is enabled out of the box, getDefaultToolbarRowDistribution()). Row 1 hosts editing tools, AI / FFXIV controls (MidiPilot, MCP, FFXIV SoundFont Mode); row 2 hosts transport, zoom, MIDI I/O and the status widgets (FFXIV Voice Gauge, MIDI Visualizer, Lyric Visualizer). The previously-hidden entries (select_box, glue_all_channels, move_*, size_change, transpose_*, thru, panic) are now visible by default too - users can hide individual entries from *Settings → Customize Toolbar*. Existing users keep whichever layout they had - Appearance::loadSettings() only applies the new defaults when toolbar_two_row_mode / toolbar_customize_enabled / toolbar_action_order / toolbar_enabled_actions are missing, and those keys are written back on every save, so anyone who has launched any previous version already has explicit values.
  • UX-VOICE-LANE-002 - FFXIV Voice Lane auto-shows / auto-hides with FFXIV SoundFont Mode - the FFXIV Voice Lane (the voice-load chart beneath the velocity lane) is now driven by two cooperating switches under the *View* menu, combined via OR: (1) the existing Show FFXIV Voice Lane toggle (View/showVoiceLane, default off) keeps the lane permanently visible regardless of FFXIV mode; (2) the new Auto-show FFXIV Voice Lane with FFXIV SoundFont Mode toggle (View/voiceLaneAutoFollowFfxiv, default on) shows the lane while FFXIV SoundFont Mode is active and hides it when the user flips FFXIV mode off (toolbar XIV button or settings checkbox). With both off the lane stays hidden until opted back in. Implementation: a single new helper MainWindow::updateVoiceLaneVisibility() evaluates alwaysShow || (autoFollow && ffxivOn) and is called from (a) the lane area's initial setup in setupActions(), (b) both View-menu action lambdas, and (c) a new connect(FluidSynthEngine::ffxivSoundFontModeChanged, ...) slot in createMenubar(). Default behaviour for fresh installs: the lane appears the moment FFXIV Mode is enabled and disappears the moment it's disabled - exactly when it's useful. Existing users with View/showVoiceLane = true keep their always-on behaviour because OR.

🔧 Fixed Bug Fixes

  • Fixed stale midieditor.ico shipped next to v1.6.0 .exe (B-FIELD-001) - the release ZIP and the CI packaging step both copied the legacy 362 KB run_environment/midieditor.ico next to MidiEditorAI.exe, even though the embedded .rc resource (and every other surface) had already been swapped to the new Midieditor-ai_logo.ico in Phase 37.1. The loose run_environment/midieditor.ico has been replaced in-place with the new branded logo so existing packaging scripts keep working unchanged and existing 1.5.x / 1.6.0 installs end up with the correct icon after the next in-place update. Regression since v1.6.0.
  • Fixed FFXIV SoundFont onboarding silently dropping the downloaded SoundFont (B-FIELD-002) - enabling FFXIV SoundFont Mode for the first time on a clean install correctly downloaded FF14-c3c6-fixed.sf2, but the file never appeared in the FluidSynth SoundFont list and was not heard during playback - so FFXIV Mode silently did nothing audible and *Settings → Midi I/O* showed an empty SoundFont stack. Three layered causes: (1) on a clean install FluidSynthEngine was not yet initialised, so loadSoundFont() short-circuited with -1 and never registered the path; (2) the active MIDI output stayed on the previous port (typically *Microsoft GS Wavetable Synth*), so even if the SF had loaded, audio would have bypassed FluidSynth; (3) the FFXIV onboarding download lambda only registered the FFXIV file - any *other* SoundFont the user downloaded in the same dialog session (e.g. GeneralUser GS) was silently dropped. FfxivSoundFontHelper::requestEnable() now switches the MIDI output to *FluidSynth (Built-in Synthesizer)* and force-initialises the engine first, then registers every downloaded SoundFont via addPendingSoundFontPaths() + loadSoundFont() (belt-and-suspenders so the path survives a re-init), persists the engine state via saveSettings() immediately, and finally surfaces a one-line confirmation dialog. The same pre-init / no-persist gap was also patched in MidiSettingsWidget::showDownloadSoundFontDialog() so the Settings → Download Default… entry behaves identically. Regression since v1.6.0.
  • Fixed crash when deleting the only Tempo / TimeSignature event (B-UPSTREAM-CRASH-001) *(upstream 21fe86b crash-fix slice)* - selecting and deleting the very last Tempo Change (channel 17) or Time Signature (channel 18) event when it lived at tick 0 and was the only event on its meta-channel crashed the editor at the next playback / save attempt because MidiFile invariants assume at least one anchor at tick 0. MainWindow::deleteSelectedEvents() now refuses to remove that final anchor while still removing every other tempo / timesig event the user selected, matching upstream behaviour. Long-standing bug; never reproduced before because typical files always carry multiple tempo events.
  • Fixed FFXIV Channel Fixer dropping unmatched percussion off channel 9 (B-FFXIV-PERC-001) *(upstream a35f1ee, adapted to our diverged Tier-2 / Tier-3 fork)* - in Rebuild mode, a track whose name didn't match an FFXIV instrument (e.g. a generic "Drums" / "Perc" track or an unnamed percussion track imported from a DAW) was reassigned to its track-index channel, destroying the GM percussion mapping. The fixer now scans an unmatched track's NoteOn distribution: if the majority of its notes already live on channel 9 it stays on channel 9, otherwise the existing numeric assignment applies unchanged. Helper added in src/ai/FFXIVChannelFixer.cpp (dominantNoteChannel()); Tier-3 (Preserve) is unaffected because it already honours assignedChannel(). Long-standing bug; surfaced after upstream's a35f1ee triage.

🟡 Medium Test Harness

  • tests/test_ffxiv_equalizer_service.cpp - 13 new Qt Test methods covering the new FFXIV equalizer service: unknownProgramReturnsMasterGain, builtinDefaultsHavePiano, setProgramGainEmitsMixChanged, mutedSlotReturnsZero, masterGainScalesAllPrograms, savePresetRoundtripPersistsValues, deletingActivePresetFallsBackToBuiltin, cannotDeleteBuiltinPreset, cannotSaveOverBuiltinPreset, allPresetNamesAlwaysIncludesBuiltin, knownInstrumentsContainsDrumKitFirst. Uses QStandardPaths::setTestModeEnabled(true) so QSettings is fully isolated from the user profile. Wired into tests/CMakeLists.txt with the WIN32 PATH-injection block so Qt6Test.dll resolves under ctest.

🟡 Medium Documentation

  • manual/menu-tools.html - new row in the Tools-menu reference describing the FFXIV SoundFont Equalizer entry, with the toolmenu screenshot.
  • manual/soundfont.html - new #ffxiv-equalizer section covering the dialog (entry points, per-row controls, header / footer, playback hook, persistence), plus a hero shot of the dialog and a side-by-side of both menu entry points.
  • DOC-SHORTCUTS-001 - manual/shortcuts.html (NEW) - dedicated keyboard-shortcut reference page covering all 78 actions registered in MainWindow::createMenubar (_defaultShortcuts[id]), grouped into 14 sections: File, Edit & Selection, Navigation, Tools (F1-F11 + Ctrl+F1/F2/F3), Editing operations (glue, scissors, delete-overlaps, convert pitch-bend, explode-chords, split-channels, strum, magnet), Tweak (Ctrl+1…5 target picker + small/medium/large nudge), Note Duration Presets (Alt+1…7 / Alt+Shift+1…8 for 1/1…1/64 plus all tuplets), Transpose & alignment, Quantize, MIDI/FFXIV/AI tools (MidiPilot, Lyric Timeline, SRT import), View & zoom, Playback, MIDI panic, Move-selection-to-track (Shift+0…9), and a Mouse Modifiers reference (Ctrl/Shift box-select, Ctrl+wheel zoom). Every row carries the action ID so users can rebind under *Settings → Keybinds*. Linked from docs-index.html (new ⌨️ *Keyboard Shortcuts* section card) and added to the Docs dropdown of all 23 manual pages that carry the standard nav-dropdown.
  • DOC-NAV-001 - Manual navigation refactored from a 21-button horizontal bar into a categorised left sidebar - the long horizontal .doc-subnav pill bar that sat under the site nav and held *every* manual page side-by-side became unreadable once the page count crossed 20. manual/navigation.js now ships a docGroups tree (Getting Started · Editor · AI & Automation · FFXIV · Files & Formats · Appearance) and renders an <aside class="doc-sidebar"> left rail injected into every manual page that loads the script. On screens ≥ 1200 px the sidebar is permanently docked at 240 px wide and body padding is shifted to make room; on narrower screens it collapses behind a hamburger toggle (just below the site nav) that opens an overlaid drawer with backdrop, Escape-to-close, and auto-close after a link click. Active page is highlighted with the brand cyan→violet accent and aria-current="page". New CSS lives in manual/site.css under *DOC SIDEBAR*; the legacy .doc-subnav selector and its <div> injection were removed. Any cached pages that still contain a .doc-subnav element are removed at runtime so the two bars never coexist.
  • DOC-MANUAL-REFRESH-001 - midieditor-ai.de visual refresh - follow-up polish on top of the new sidebar nav: every manual page now inherits the brand --gradient-hero background plus the subtle cyan grid overlay from the index hero (fixed via body::before in manual/site.css), the floating sidebar card sits in a 20 px inset with rounded corners + soft shadow, and the prose column was widened by ~15 % (960 → 1104 px max-width) so long-form pages aren't visually squeezed by the rail. The .toc-shortcuts "On this page" list in manual/shortcuts.html is now an auto-fill button grid (14 emoji-iconed push-button cards with brand-cyan hover border + lift). Tempo Conversion was relocated from the FFXIV group to the Editor group in navigation.js because it's a general MIDI tool, not FFXIV-specific. manual/midi-overview.html was rewritten from a plain <ul> of paragraphs into a card-based primer: brand hero, 5 stat tiles (16 channels · 128 notes · 128 velocities · 128 GM programs · drum ch 10), a Tracks-vs-Channels two-panel callout, an 11-card event grid (added the missing Aftertouch + GM-drum-map facts, renamed Key/Channel Pressure → Aftertouch, added Lyric meta-event), refreshed I/O section mentioning FluidSynth + RTP-MIDI + USB-MIDI, and a *Learn more* panel linking to midi.org, the official spec, GM sound set, MIDI 2.0, Wikipedia, plus internal cross-links. Old anchor IDs (tracksandchannels, events, io) preserved.

🟡 Medium Files Modified

  • src/midi/FfxivEqualizerService.h / .cpp - NEW. Singleton mixer service: built-in instrument table (28 rows), gainFor(program, isDrum), mute, master gain, preset CRUD (Save As / Delete / Reset / Revert), QSettings persistence. Emits mixChanged() on every mutating call.
  • src/midi/FluidSynthEngine.h / .cpp - added playPreviewArpeggio(program, isDrum), applyFfxivEqualizerAttenuation(channel), ffxivEqualizerCb(gain), and the static _exportCurrentProgram[16]. Wired FfxivEqualizerService::mixChanged in initialize() so slider edits push GEN_ATTENUATION instantly. Live sendMidiData PC handler and offline exportPlaybackCallback both now route through applyFfxivEqualizerAttenuation().
  • src/gui/FfxivEqualizerDialog.h / .cpp - NEW. Modal dialog with 28 rows (slider + spinbox + mute + reset + ▶ Preview), preset header (combo + Save As / Delete / Reset to Built-in), master gain, search filter, Apply / Cancel.
  • src/gui/MidiSettingsWidget.h / .cpp - added the new Open FFXIV SoundFont Equalizer… button in the FluidSynth settings group (gated on the FFXIV mode checkbox) plus the onOpenFfxivEqualizer() slot and the FfxivEqualizerDialog.h include.
  • src/gui/MainWindow.h / .cpp - added the Tools → FFXIV SoundFont Equalizer… menu entry, the matching openFfxivEqualizer() slot, and the FfxivEqualizerService::loadFromSettings() startup hook.
  • src/gui/AppearanceSettingsWidget.cpp - Theme dropdown entry renamed to MidiEditor AI (Default) so the default is unambiguous.
  • src/gui/Appearance.cpp - first-run theme default flipped to ThemeBrand (THEME-DEFAULT-001); _toolbarTwoRowMode and _toolbarCustomizeEnabled defaults flipped to true (TOOLBAR-DEFAULT-001).
  • src/gui/LayoutSettingsWidget.cpp - getDefaultToolbarOrder(), getDefaultToolbarEnabledActions(), getDefaultToolbarRowDistribution() rewritten to emit the curated 2-row layout (TOOLBAR-DEFAULT-001).
  • src/gui/MainWindow.cpp - guard around the 2-second startup updater QTimer::singleShot plus the tempo/timesig anchor guard in deleteSelectedEvents(). New _voiceLaneAutoFollowAction (View menu) + central updateVoiceLaneVisibility() helper + FluidSynthEngine::ffxivSoundFontModeChanged connection driving the OR-combined visibility rule (UX-VOICE-LANE-002).
  • src/gui/MainWindow.h - added _voiceLaneAutoFollowAction and updateVoiceLaneVisibility() declarations (UX-VOICE-LANE-002).
  • src/gui/PerformanceSettingsWidget.cpp - new *Updates* group with the Check for updates at startup checkbox.
  • src/gui/MatrixWidget.cpp / src/gui/ProtocolWidget.cpp - DPR-aware back-buffer allocation (RENDER-DPR-001).
  • src/gui/FfxivVoiceLaneWidget.h / .cpp - edit-cursor + live playback overlay (UX-VOICE-LANE-001).
  • src/gui/FfxivSoundFontHelper.cpp - output-port switch + force-init + every-path registration in the FFXIV onboarding flow (B-FIELD-002).
  • src/ai/FFXIVChannelFixer.cpp - new dominantNoteChannel() helper preserving CH9 percussion in Rebuild mode (B-FFXIV-PERC-001).
  • tests/test_ffxiv_equalizer_service.cpp - NEW. 13 Qt Test methods covering the equalizer service (see Test Harness above).
  • tests/CMakeLists.txt - registered the new test executable plus the WIN32 set_tests_properties(... PATH=${_qt_bin_dir}; ...) entry so Qt6Test.dll resolves under ctest.
  • manual/menu-tools.html - new FFXIV SoundFont Equalizer row in the Tools-menu reference table.
  • manual/soundfont.html - new #ffxiv-equalizer section with screenshots of the dialog and both entry points.
  • manual/screenshots/FFXIV-SoundFont-Equalizer.png, FFXIV-SoundFont-Equalizer_midi_menu.png, FFXIV-SoundFont-Equalizer-toolmenu.png - NEW manual screenshots.
  • run_environment/midieditor.ico - replaced legacy ICO with the new branded logo (B-FIELD-001).
  • manual/navigation.js - flat docPages array replaced with grouped docGroups tree; renders an <aside class="doc-sidebar"> rail with a hamburger toggle + backdrop drawer for small screens (DOC-NAV-001).
  • manual/site.css - .doc-subnav block removed; new *DOC SIDEBAR* block (.doc-sidebar, .doc-sidebar-toggle, .doc-sidebar-backdrop, group / link styling, ≥1200 px docked-rail body shift, ≤1199 px drawer mode) (DOC-NAV-001). Body now uses var(--gradient-hero) + a fixed cyan grid body::before overlay, the docked sidebar became a floating card (20 px inset · rounded · soft shadow), and the prose column max-width grew to 1104 px (DOC-MANUAL-REFRESH-001).
  • manual/navigation.js - Tempo Conversion moved from the FFXIV group into the Editor group of docGroups (DOC-MANUAL-REFRESH-001).
  • manual/shortcuts.html - .toc-shortcuts "On this page" list converted from a 2-column <ul> into a .toc-grid of 14 emoji-iconed push-button cards with brand-cyan hover (DOC-MANUAL-REFRESH-001).
  • manual/midi-overview.html - full rewrite: brand hero, 5-tile stats strip, Tracks-vs-Channels two-panel callout, 11-card event grid (added Aftertouch + GM drum-map + Lyric meta-event), refreshed I/O section, and a *Learn more* panel linking out to midi.org, the spec, GM, MIDI 2.0 and Wikipedia (DOC-MANUAL-REFRESH-001).

v1.6.0

2026-05-02
Voice Limiter, Tempo Conversion & Paste Special
  • FFXIV voice-limiter awareness (Phase 32) - read-only analyze_voice_load AI tool plus the MidiPilot → Tools → Analyze Voice Load menu surface a per-tick voice peak, the 16-voice ceiling, and per-channel rate hotspots so dense compositions can be audited *before* an in-game performance drops notes.
  • Time-preserving tempo conversion (Phase 33) - new Tools → Tempo Tools → Convert Tempo, Preserve Duration… dialog plus right-click entry points on the channel/track list. Scales every event's tick position by target_bpm / source_bpm and rewrites the tempo meta in one atomic, undoable operation, so a 90 BPM vocal lines up with a 180 BPM project at the same real-time speed.
  • Paste Special - cross-instance track/channel assignment (Phase 34) - copying a multi-track arrangement from another MidiEditor instance and pasting into a new file no longer collapses everything onto the current edit track. The new Edit → Paste Special… dialog offers three modes: *Create new tracks per source* (default), *Preserve source track + channel mapping (1:1)*, and *Paste to current edit track + channel (legacy)*. Track creation is part of the same protocol action so a single Ctrl+Z undoes the paste *and* the new tracks together. Ctrl+V automatically opens this dialog whenever cross-instance clipboard data is present.
  • Copy to Track / Copy to Channel (Phase 36) - new Tools → Copy events to track… and Tools → Copy events to channel… entries duplicate the current selection 1:1 onto another track / channel while leaving the originals in place. The new copies become the active selection so an immediate *Octave Up* / velocity edit only touches the duplicates.

🟡 Medium Phase 32 - FFXIV Voice Limiter / Awareness

  • VOICE-001 - analyze_voice_load AI tool - new entry in src/ai/ToolDefinitions.cpp, gated on AI/ffxiv_mode. Returns globalPeak, overflowRanges (tick spans where simultaneous voices > 16), and rateHotspots (per-channel passages exceeding the 14 notes/sec/channel cap). Implemented in src/ai/FfxivVoiceLoadCore.cpp so the analysis is testable in isolation without pulling in MidiFile/Selection.
  • VOICE-002 - Voice load lane - optional dense-score visualiser stripe under the velocity lane in src/gui/MatrixWidget.cpp; colour-coded green/yellow/red against the 16-voice ceiling.
  • VOICE-003 - Test coverage - tests/test_ffxiv_voice_analyzer.cpp covers empty input, monophonic = 1 voice, 17-note chord overflow detection, per-channel rate hotspots, and simultaneous-chord-on-different-channels accounting.

🟡 Medium Phase 33 - Time-Preserving Tempo Conversion

  • TEMPO-CONV-001 - Convert Tempo dialog - new src/gui/TempoConversionDialog.cpp under Tools → Tempo Tools. Prompts for source/target BPM, scope (whole file / events only / per channel / selected events), and a tempo handling mode (replace fixed tempo / scale existing tempo curve / events only). Applied inside a single Protocol::startNewAction() so Ctrl+Z is one step.
  • TEMPO-CONV-002 - Engine in TempoConversionService - src/converter/TempoConversionService.cpp implements preview() and apply(). Three passes: (1) collect target events by scope, (2) scale tick positions by target/source, (3) rewrite tempo events according to the chosen TempoMode (ReplaceFixed / ScaleTempoMap / EventsOnly).
  • TEMPO-CONV-003 - Right-click entry points - channel list and track list both surface Convert Tempo, Preserve Duration… scoped to the clicked row, in src/gui/ChannelListWidget.cpp and src/gui/TrackListWidget.cpp.
  • TEMPO-CONV-004 - Test coverage - tests/test_tempo_conversion_service.cpp ships eight fixtures covering null/zero-BPM error paths, identity-BPM warning, ReplaceFixed tempo collapse, EventsOnly per-channel scope isolation, selected-event-only scaling, and round-trip (90→180→90) restoration.

🟡 Medium Phase 32 - Voice-Awareness UI surfaces (UI counterparts to VOICE-001)

  • VOICE-GAUGE-001 - Toolbar voice gauge - new src/gui/FfxivVoiceGaugeWidget.cpp, a 24-segment stereo-style LED meter with a fixed left N/16 readout, a reserved +N overflow slot and a dedicated tick mark at the documented 16-voice ceiling. Registered as a customisable toolbar action ffxiv_voice_gauge so it survives toolbar customisation and *Reset to Defaults*.
  • VOICE-LANE-001 - Piano-roll voice lane - new src/gui/FfxivVoiceLaneWidget.cpp renders an auto-scaled bar chart under the velocity lane: soft / red threshold colouring, dashed red ceiling at 16 with halo + 16 tag, plus a per-channel rate-hotspot strip for any 1-second window exceeding 14 notes/sec. Toggled from View → Show Voice Lane and from clicking the toolbar gauge.
  • VOICE-AUTOBIND-001 - Voice Awareness auto-binds to FFXIV SoundFont Mode - new Settings → MIDI → FFXIV → Voice Limiter group; enabling FFXIV Mode flips Voice Awareness on (gauge + lane visible, analyser running), disabling it stops the analyser entirely so non-FFXIV users pay zero perf cost. Manual override persists at FFXIV/voiceLimiter/userOverride. Wired through FluidSynthEngine::ffxivSoundFontModeChanged(bool), mirroring the bard-accurate-mode auto-bind from 1.5.3 (BARD-MODE-001).
  • VOICE-TAILS-001 - Empirical sample-tail model - FfxivVoiceLoadCore::sampleTailMs() extends each NoteOn..NoteOff window by an estimated audible release per GM family / drum pitch (Lute / Harp / Piano 0.5-0.9 s, Cymbal 1.2 s, Snare 0.2 s, …) so the voice count matches in-game / MogNotate observations instead of under-reporting on plucked instruments. Visual thresholds (green ≤18, yellow 19-23, red ≥24) are intentionally relaxed from the documented hard ceiling per empirical in-game testing; the analyser still reports the raw count and overflow against 16 for the AI tool.

🟡 Medium Phase 34 - Paste Special (Cross-Instance Paste)

  • PASTE-SPEC-001 - SharedClipboard v2 wire format - src/tool/SharedClipboard.cpp bumps CLIPBOARD_VERSION from 1 → 2. The new prelude carries a (trackId → trackName) table plus a qint32 sourceTrackId per event record (per-event header is now 16 bytes). v1 buffers from older instances are rejected via the existing version check. New static accessors SharedClipboard::sourceTrackList() and SharedClipboard::getPasteSourceInfo(index) surface the metadata to callers.
  • PASTE-SPEC-002 - PasteSpecialDialog - new src/gui/PasteSpecialDialog.cpp. Modal, summary line *"Clipboard: N event(s), M source track(s), C channel(s), D of music"*, three radio buttons (NewTracksPerSource / PreserveSourceMapping / CurrentEditTarget), and *Don't ask again this session* / *Make this the new default* check-boxes (the latter only enabled when the former is). Persistent default lives under QSettings("Editing/pasteSpecialDefault").
  • PASTE-SPEC-003 - EventTool::pasteFromSharedClipboardWithOptions - src/tool/EventTool.cpp. Builds a QHash<int sourceTrackId, MidiTrack *> according to opts.assignment (NewTracksPerSource creates Pasted: <name> tracks via MidiFile::addTrack(), PreserveSourceMapping reuses by name and falls back to creating with the original name, CurrentEditTarget collapses to NewNoteTool::editTrack() / editChannel()). Per-event channel + track is routed via SharedClipboard::getPasteSourceInfo(). Pre-snapshots every distinct source channel into channelCopies so the protocol covers all of them. Track creation happens inside the same startNewAction(...) block so a single Ctrl+Z unwinds everything.
  • PASTE-SPEC-004 - Backwards compatibility - legacy EventTool::pasteFromSharedClipboard() becomes a thin wrapper using CurrentEditTarget, so the existing Ctrl+V cross-instance path is byte-identical to 1.5.x.
  • PASTE-SPEC-005 - Edit → Paste Special… menu - src/gui/MainWindow.cpp registers the new action, builds a PasteClipboardSummary from the deserialised metadata, opens the dialog, persists the chosen default to QSettings, and dispatches into EventTool::pasteFromSharedClipboardWithOptions(opts).
  • PASTE-SPEC-006 - Ctrl+V routes through Paste Special - src/gui/MainWindow.cpp MainWindow::paste() now opens the Paste Special dialog whenever EventTool::hasSharedClipboardData() reports cross-instance data; the in-process clipboard keeps the legacy fast path. The dialog's *Don't ask again this session* toggle silences the modal for subsequent Ctrl+V presses and reuses the chosen assignment.

🟡 Medium Phase 36 - Copy to Track / Copy to Channel

  • COPY-TARGET-001 - EventTool::copySelectionToTrack / copySelectionToChannel - src/tool/EventTool.cpp. Duplicates the current selection 1:1 onto a chosen track or channel (0..15), keeps the originals in place, snapshots each touched MidiChannel exactly once for the protocol, and sets the new copies as the active selection so the user can immediately apply *Octave Up*, velocity edits, etc. NoteOn/Off pairing is preserved via cloneEventOnto(). Copy-to-Channel rejects meta channels (16/17/18) by contract.
  • COPY-TARGET-002 - Tools menu integration - src/gui/MainWindow.cpp adds Tools → Copy events to track… and Tools → Copy events to channel…. Channel submenu is populated once with channels 0-15; track submenu is rebuilt from updateTrackMenu() whenever the file's track list changes. Both menus are gated on a non-empty selection via checkEnableActionsForSelection().

🔧 Fixed Bug Fixes

  • DARKMODE-MOVETO-001 - Eye icons in Move-to context submenus were unreadable in dark themes (legacy bug) - MatrixWidget::contextMenuEvent()'s Move to Track / Move to Channel submenus loaded all_visible.png / all_invisible.png directly via QIcon(":/..."), bypassing Appearance::adjustIconForDarkMode(). Result: black-on-dark glyphs that were effectively invisible in every dark theme since UX-CTX-001 shipped in v1.4.2. Both icons are now routed through Appearance::adjustIconForDarkMode(QStringLiteral(...)) so the eye glyphs invert and stay readable in cascading menus across all themes. Regression since v1.4.2.

v1.5.3

2026-04-29
Bard-Accurate Playback (Closer to In-Game Sound)
  • FFXIV bard-accurate playback mode - when FFXIV SoundFont Mode is on, FluidSynth now reshapes its output so MidiEditor AI sounds much closer to the in-game bard performance instead of a generic GM player. Auto-enabled with FFXIV mode, persisted under FluidSynth/bardAccurateMode (default on); toggling either flag off restores the user's reverb/chorus state and 256-voice polyphony (BARD-MODE-001).
  • Phase 1 - Dry & capped - in bard mode FluidSynth disables reverb (fluid_synth_set_reverb_on(0)) and chorus (fluid_synth_set_chorus_on(0)), caps polyphony to 16 voices (fluid_synth_set_polyphony(16)) to match the in-game bard's hard voice limit, and the default sample rate is now 48 kHz to match WASAPI shared-mode (BARD-DRY-001).
  • Phase 2 - Bard dynamics - per-instrument-per-register minimum note length table modelled after the in-game bard's instrument behaviour (Harp ≥ 1.13 s, Piano ≥ 1.53 s, Flute clamp 0.5-4.5 s, Snare 0.26 s, …). NoteOff is held by QTimer::singleShot with a per-(channel,key) generation counter so engine reset / SoundFont reload can't fire stale lambdas. Volume (CC7) and expression (CC11) are reshaped through a cubic perceptual curve (v = (CC7/127)·(CC11/127); v³) and emitted on CC7 only. Velocity is forced to 127 on every NoteOn so dynamics flow through the curve (BARD-DYN-001).
  • Phase 3 - Percussion parity - new FluidSynthEngine::ffxivDrumProgramForGmNote() maps GM Standard-Kit keys (35/36 kick → 117 BassDrum; 37/38/40 snare → 118; 41/43/45/47/48/50 toms → 47 Timpani; 42/44/46 hats + 49/51/52/53/55/57/59 cymbals → 119 Cymbal; 60-81 hand drums / blocks / shakers → 116 Bongo) to the closest FFXIV percussion preset. MidiOutput::sendCommand first tries the existing track-name lookup (drumProgramForTrackName), then falls back to the GM-key map so single-channel GM drum tracks finally route correctly instead of all hits sharing one preset (BARD-DRUM-001).
  • Audio export now honours FFXIV mode (all formats) - offline rendering (Export → WAV / FLAC / OGG / MP3) used fluid_player_t directly and bypassed the live FFXIV remap, so e.g. *Acoustic Guitar (nylon)* exported as Piano. A new playback callback runs the same bank-select forcing and program-fallback table during export so every rendered file - regardless of format - sounds the same as live playback (BARD-EXPORT-001).
  • Mute- and solo-aware audio export (all formats) - the Export Audio pipeline is now fully channel- *and* track-aware. Channel mute / solo state (MidiFile::channelMuted()) is forwarded to the offline render via a new ExportOptions::mutedChannelsMask, and any per-track mute is honoured by writing those tracks as empty MTrk chunks into the temp MIDI passed to FluidSynth. Drop a single drum track or solo a single guitar and the rendered WAV / FLAC / OGG / MP3 contains exactly that - same behaviour you'd get during live playback (BARD-MUTE-001, BARD-TRACK-MUTE-001).
  • Per-track FFXIV percussion routing in export (all formats) - for FFXIV percussion tracks (Bass Drum / Snare Drum / Timpani / Bongo / Cymbal, with optional +N / -N octave suffix) the export pipeline now injects a CH9 Program Change before each NoteOn so the offline FluidSynth player picks the correct bard percussion preset per track. This mirrors the live per-note PC injection in MidiOutput::sendCommand and finally lets multi-drum-track arrangements export with the same drum kit balance you hear during playback. Format-agnostic - the routing happens before the encoder, so it applies to WAV, FLAC, OGG and MP3 alike (BARD-DRUM-EXPORT-001).
  • Per-program loudness trim - the FFXIV SoundFont presets are sampled at uneven levels (ElectricGuitarClean is noticeably quieter than Lute / Harp / Piano). New bardProgramAttenuationCb() adds a per-program GEN_ATTENUATION (in centibels) on every Program Change in bard mode so the perceived loudness across instruments is balanced; cleared back to 0 when bard mode is turned off (BARD-LOUDNESS-001).

✨ Added New Features

  • BARD-MODE-001 - Bard-accurate playback mode - new public API FluidSynthEngine::setBardAccurateMode(bool) / bardAccurateMode() plus bardAccurateModeChanged(bool) signal in src/midi/FluidSynthEngine.h, wired into applyBardAccuracySettings() in src/midi/FluidSynthEngine.cpp. The mode is gated on _ffxivSoundFontMode && _bardAccurateMode so toggling either flag restores the user's previous reverb/chorus state and the 256-voice polyphony default. Persisted under FluidSynth/bardAccurateMode in QSettings (default true); reapplied on every initialize() and on every setFfxivSoundFontMode(bool) call.
  • BARD-DRY-001 - Dry & capped output (Phase 1) - applyBardAccuracySettings() calls fluid_synth_set_reverb_on(synth, 0), fluid_synth_set_chorus_on(synth, 0), fluid_synth_set_polyphony(synth, 16) because the in-game bard runs dry (no global reverb/chorus colouring) with a hard 16-voice limit per performer. Default _sampleRate raised from 44100.0 to 48000.0 in the constructor and in loadSettings() so the FluidSynth engine starts at 48 kHz / WASAPI shared-mode and gives the FFXIV SoundFont presets the sample-rate context they were authored against. Removes the boomy reverb tail that was making FFXIV preset attacks sound smeared compared to the in-game bard.
  • BARD-DYN-001 - Bard dynamics (Phase 2) - three coordinated changes in FluidSynthEngine::sendMidiData():
  • Min note length - new bardMinNoteLengthMs(instrumentIndex, midiKey, duration) carries per-instrument-per-register tables (Harp / Piano / Lute / Fiddle / Flute family / Timpani / Bongo / Bass Drum / Snare / Cymbal / Brass / Strings / E-Guitar variants) that match the in-game bard's minimum sustain per instrument and register. NoteOff timing: if elapsed >= minLen we send fluid_synth_noteoff immediately, else we schedule a QTimer::singleShot(remaining, this, lambda) that captures a QPointer<FluidSynthEngine> guard, the channel/key, and the current per-(channel,key) generation counter. resetBardNoteState() (also called from initialize()) bumps the generation counter so any pending lambdas drop their work - no spurious noteoffs after SoundFont reload, song change, or engine shutdown.
  • Cubic CC7·CC11 curve - new applyBardVolumeCurve(channel) computes v = (CC7/127) * (CC11/127), raises it to the third power, rounds to 0..127 and emits the result on CC7 while pinning CC11 to 127. The CC handler in sendMidiData() intercepts incoming CC7 / CC11, stores them in _bardCC7[16] / _bardCC11[16], calls the curve, and short-circuits forwarding so the synth never sees the raw linear values. Matches the perceptual (vol·expr)^3 response the in-game mix uses.
  • Force vel=127 - every NoteOn in bard mode is forced to velocity 127; if a previous note for the same (channel,key) is still held by min-length, a NoteOff is flushed first so the synth retriggers cleanly. Mirrors the in-game bard's fixed-FFF note delivery and lets the cubic curve own all dynamics.
  • BARD-DRUM-001 - GM drum-key → FFXIV percussion fallback (Phase 3) - new FluidSynthEngine::ffxivDrumProgramForGmNote(int gmNote) in src/midi/FluidSynthEngine.h / .cpp returns the closest FFXIV bard percussion program for a GM Standard Kit key (35..81 covered). MidiOutput::sendCommand in src/midi/MidiOutput.cpp keeps the existing per-track-name path as the primary lookup and falls back to the GM-key map only when drumProgramForTrackName() returns -1, so well-tagged GP/MusicXML imports keep working unchanged while generic single-channel GM drum tracks (kick/snare/toms/hats/cymbals on CH9) now route each hit individually instead of all sharing the first injected preset.

🟡 Medium Improvements

  • BARD-CTOR-001 - Per-channel bard state - FluidSynthEngine now tracks the active program per channel (_bardCurrentProgram[16]) so the min-note-length lookup can map by current FFXIV bard instrument. NoteOn timestamps and held flags live in _bardNoteOnMs[16][128] / _bardNoteHeld[16][128], indexed by a QElapsedTimer (_bardClock) started from initialize(). The generation counter _bardNoteGen[16][128] invalidates pending QTimer lambdas without needing to track them explicitly.
  • BARD-INSTMAP-001 - FFXIV instrument index map - new static bardInstrumentIndexForProgram(int program) collapses both common reference program numbers (115 snare, 127 cymbal) and our actual FFXIV SoundFont program numbers (118 snare, 119 cymbal) onto a unified 1..28 instrument index so bardMinNoteLengthMs works regardless of how the host sequencer numbered the percussion presets.
  • BARD-CC-001 - CC7/CC11 forwarding policy - outside bard mode the CC handler keeps the existing FFXIV bank-select (CC#0 / CC#32 → 0) behaviour unchanged. Inside bard mode CC7/CC11 are intercepted *before* the bank-select short-circuit so the cubic curve is the only volume path; all other CCs (sustain, pan, modulation, …) pass through untouched.

🔧 Fixed Bug Fixes

  • BARD-RESET-001 - Stale NoteOff lambdas after engine reset - without the generation counter, QTimer::singleShot lambdas scheduled before a SoundFont reload or shutdown()/initialize() cycle could fire fluid_synth_noteoff against a freshly initialized synth, occasionally truncating the first notes of the next playback. resetBardNoteState() now bumps _bardNoteGen[ch][key] for every (channel,key), and the lambda body checks guard->_bardNoteGen[channel][key] != gen (plus _initialized && _synth) before touching the synth so stale callbacks are dropped silently.

🟡 Medium Audio Export Pipeline

  • BARD-EXPORT-001 - Live-parity FFXIV remap in audio export (all formats) - offline rendering used fluid_player_t directly and bypassed the live sendMidiData() path, so the FFXIV bank-select forcing and the program-fallback table never ran during export. New static FluidSynthEngine::exportPlaybackCallback(void*, fluid_midi_event_t*) is installed via fluid_player_set_playback_callback() whenever FFXIV mode is on; it forces CC#0 / CC#32 to 0 and remaps program changes through the same fallback table as live playback (24→25 Lute, 26→27 Clean Guitar) before forwarding to fluid_synth_handle_midi_event(). Because the callback runs ahead of FluidSynth's audio renderer and audio.file.format (the encoder selector - WAV / FLAC / OGG / MP3), the remap applies uniformly to every export format the dialog offers. The export synth also picks up the dry/16-voice settings when bard mode is active and seeds GEN_ATTENUATION for default program 0 on every channel so tracks that never send a Program Change still get the same loudness trim as live playback. Tracks like *Acoustic Guitar (nylon)* (GM prog 24) now export with the same Lute voicing they have during live playback.
  • BARD-MUTE-001 - Channel mute / solo respected in audio export (all formats) - MainWindow::exportAudio() and exportAudioSelection() in src/gui/MainWindow.cpp build a quint32 mutedChannelsMask from MidiFile::channelMuted(i) (which already folds in solo logic) and pass it through the new ExportOptions::mutedChannelsMask field. FluidSynthEngine::exportPlaybackCallback drops every channel-bound MIDI message (0x80/0x90/0xA0/0xB0/0xC0/0xD0/0xE0) for any channel in the mask, so exporting with only the harp audible produces a harp-only WAV/FLAC/OGG/MP3. The callback is registered whenever the mask is non-zero even outside FFXIV mode, so generic exports also respect mute/solo state across every format. The callback re-applies per-program bardProgramAttenuationCb() on every Program Change in offline render so loudness balance matches live playback. Note: the cubic CC7·CC11 curve and per-instrument min-note-length still run only on the live MidiOutput timing path; full offline parity for those would require a larger export refactor.
  • BARD-TRACK-MUTE-001 - Per-track mute respected in audio export (all formats) - MidiFile::save(QString, bool skipMutedTrackEvents = false, const QHash<QString,int> &drumProgramByTrackName = {}) in src/midi/MidiFile.cpp gained two new parameters. With skipMutedTrackEvents=true, muted tracks are written as empty MTrk chunks (header + end-of-track only) so the offline FluidSynth player can't see their events - required because the offline player flattens to channels and would otherwise still hear muted-track events if multiple tracks share a channel. MainWindow::exportAudio() / exportAudioSelection() scan file->tracks() for any muted track; if found, they force the temp-MIDI export path even for already-.mid sources and pass skipMutedTrackEvents=true. Applies to every export format because the temp MIDI is the input to a single render pipeline shared by WAV / FLAC / OGG / MP3.
  • BARD-DRUM-EXPORT-001 - Per-track FFXIV percussion routing in audio export (all formats) - MidiFile::save()'s new drumProgramByTrackName parameter inserts a delta-0 CH9 Program Change before each NoteOn from a track whose name matches an FFXIV percussion preset (Bass Drum / Snare Drum / Timpani / Bongo / Cymbal, with +N/-N octave suffix stripped). MainWindow::exportAudio() builds this map from FluidSynthEngine::drumProgramForTrackName() for every track and forces the temp-MIDI path when the map is non-empty. FluidSynthEngine::exportPlaybackCallback flips a new _exportExplicitPC[16] flag whenever it sees an explicit Program Change, so the GM-drum-key fallback (ffxivDrumProgramForGmNote()) on CH9 NoteOn no longer overwrites the program a named track already requested. CH9 NoteOn / NoteOff in bard mode are now triggered via direct fluid_synth_noteon / fluid_synth_noteoff calls instead of fluid_synth_handle_midi_event, which was silently routing CH9 events into a non-existent percussion bank. Because both the temp-MIDI PC injection and the callback fallback run before FluidSynth's encoder stage, the per-track drum routing is identical for WAV, FLAC, OGG and MP3.

📝 Notes Technical / Internal

  • FLUIDSYNTH-SR-001 - Default sample rate raised to 48 kHz - both the constructor (_sampleRate(48000.0)) and loadSettings() (settings->value("sampleRate", 48000.0)) now default to 48 kHz so a fresh install or a settings file without the key matches WASAPI shared-mode and the sample-rate context the FFXIV SoundFont presets were authored against. Existing user overrides are honoured.
  • PLAN-08-001 - Phase status updated - internal planning doc Phases 1-3 marked ✅ implemented with the actual function/setting names, leaving Phase 4 (audio period tuning, in-game OBS A/B reference) as the only outstanding item.

v1.5.2

2026-04-28
QoL Update: FFXIV SoundFont Toolbar Toggle & Auto-Setup
  • One-click FFXIV SoundFont Mode toolbar toggle - new XIV button next to the existing MCP button flips FFXIV SoundFont Mode on/off without opening Settings, stays in sync with the checkbox via a new ffxivSoundFontModeChanged(bool) signal, and is registered as a customisable toolbar action ffxiv_toggle (FFXIV-TOGGLE-001).
  • Auto-download + snapshot/restore orchestration - toggling FFXIV Mode on auto-locates the FFXIV SoundFont (stack → disk → DownloadSoundFontDialog), snapshots the previous non-FFXIV selection to QSettings("FFXIV/savedEnabledSoundFonts"), and restores it verbatim on disable, with a Microsoft GS Wavetable Synth fallback when the FluidSynth stack ends up empty (FFXIV-AUTO-001).
  • Piano roll opens centred on C3..C6 by default - empty/new files now position the matrix viewport so the playable middle octaves sit in the visible centre regardless of window height, instead of dropping the user at C7+ or C1- (QOL-VIEW-001).
  • Dark-mode tinting bug for the green mcp_on toolbar icon - the *MCP server running* indicator was painted gray-on-gray in dark themes; added mcp_on to the Appearance::adjustIconForDarkMode skip list so coloured "active" icons render verbatim (DARKMODE-MCP-001).
  • Generic dark-mode artwork override via <name>_dark.png - Appearance::adjustIconForDarkMode(QString) now auto-uses an explicit *_dark.png sibling when one exists, fixing the gold-text ffxiv_fix icon and giving every future icon a drop-in dark variant path (DARKMODE-VARIANT-001).
  • Manual updates for the new toolbar toggle - added a Tools row in manual/menu-tools.html and a new "FFXIV SoundFont Mode - Toolbar Toggle" section in manual/soundfont.html with inline XIV_on/off previews and matching CSS exceptions in manual/style.css (DOC-XIV-001).

✨ Added New Features

  • FFXIV-TOGGLE-001 - Toolbar toggle for FFXIV SoundFont Mode - new dedicated XIV button next to the existing MCP toolbar button so the mode can be flipped with a single click instead of opening *MIDI Settings → FluidSynth → FFXIV SoundFont Mode* every time. Implemented as FfxivToggleWidget (src/gui/FfxivToggleWidget.cpp) using the same paint/hover idiom as McpToggleWidget: two preloaded pixmaps (XIV_on.png / XIV_off.png in run_environment/graphics/tool/) swapped per state, no opacity dim, fixed 40×40 px so the toolbar layout stays uniform. Stays in sync with the Settings checkbox via the new FluidSynthEngine::ffxivSoundFontModeChanged(bool) signal (only emitted on actual value change). Registered as a customisable toolbar action ffxiv_toggle in src/gui/LayoutSettingsWidget.cpp with own default-list entry, so it survives toolbar customisation and *Reset to Defaults*.
  • FFXIV-AUTO-001 - Auto-download of the FFXIV SoundFont on first enable - toggling FFXIV Mode on (button or checkbox) now goes through the new FfxivSoundFontHelper (src/gui/FfxivSoundFontHelper.cpp). Resolution order: FFXIV SF already in the FluidSynth stack → on disk in <appDir>/soundfonts/ → otherwise the user is asked *"Download FFXIV SoundFont now?"* and the existing DownloadSoundFontDialog opens scoped to the FFXIV entry. The downloaded file is auto-loaded into the engine and selected, all other SoundFonts are disabled, and the engine flag flips on. If the user cancels the download the toggle reverts to OFF - no half-state. SF detection is by basename match (ff14* or ffxiv*, case-insensitive).
  • FFXIV-AUTO-001 - Snapshot + restore of the previous SoundFont selection - before enabling FFXIV Mode the helper snapshots all currently enabled non-FFXIV SoundFonts to QSettings("FFXIV/savedEnabledSoundFonts"). On disable, the snapshot is restored verbatim (re-loading any SF that was unloaded in the meantime). If the snapshot is empty or all snapshotted files are gone, the first non-FFXIV SF in the stack is enabled as a sane fallback.
  • FFXIV-AUTO-001 - Microsoft GS Wavetable Synth fallback - if disabling FFXIV Mode leaves the FluidSynth stack with zero enabled SoundFonts (clean install / freshly cleared list), the helper switches the active MIDI output to the system *"Microsoft GS Wavetable Synth"* port (matched by name) and shows a one-line info dialog so playback keeps working out of the box.
  • QOL-VIEW-001 - Piano roll opens centred on the C3..C6 range - when MidiEditor AI starts with an empty file (or any file with no notes) the matrix viewport is now positioned so that the playable middle octaves (lines 43..79, midpoint line 61) sit in the centre of the visible area regardless of window height. Implemented in src/gui/MatrixWidget.cpp setFile(): when the existing maxNote - 5 heuristic reports no notes, startLineY = 61 - linesInView/2 (clamped to 0). Files that contain notes keep the previous "scroll to the highest note in the song" behaviour byte-identically - only the empty-file QoL case changes, so the user can start drawing immediately without scrolling.

🔧 Fixed Bug Fixes

  • DARKMODE-MCP-001 - Dark mode tinted the green mcp_on toolbar icon to gray - Appearance::adjustIconForDarkMode paints a (180,180,180) overlay over every non-skipped icon to lift black artwork against dark backgrounds, but the MCP "active" icon is intentionally green and was missing from the skipIcons list. Result: the *MCP server running* state looked the same medium-gray as the *stopped* state in dark themes - original v1.4.0 oversight when the MCP toolbar toggle was first introduced. Added mcp_on (and the new XIV_on) to the skip list in src/gui/Appearance.cpp so coloured "active" toolbar icons render verbatim in every theme. Regression since v1.4.0.
  • DARKMODE-VARIANT-001 - Dark mode tinted the gold-text ffxiv_fix Tools icon - the FFXIV Channel Fixer icon has a black harp glyph (hard to read on dark backgrounds) and gold lettering (which the gray overlay flattens). Solved generically: Appearance::adjustIconForDarkMode(QString iconPath) now looks for an explicit <name>_dark.png variant next to the original and, if present, returns it verbatim (no tinting, no inversion). Shipped run_environment/graphics/tool/ffxiv_fix_dark.png as the first variant. The same pattern works for any future icon - drop a *_dark.png next to the file and it is picked up automatically in dark mode.
  • DARKMODE-XIV-001 - XIV_off toolbar icon was unreadable in dark mode - the new OFF state is intentionally black, but the FfxivToggleWidget constructor was loading the pixmap directly via QPixmap(...) instead of going through Appearance::adjustIconForDarkMode, so it stayed black on a dark toolbar. Routed both XIV_on and XIV_off through Appearance::adjustIconForDarkMode(QPixmap, name) (mirroring McpToggleWidget); the mcp_on/XIV_on skip-list entry above keeps the green ON variant untouched while the OFF variant is lifted to light gray for dark themes.

🟡 Medium Documentation

  • DOC-XIV-001 - Manual entries for the new FFXIV SoundFont toolbar toggle - added a *FFXIV SoundFont Mode toggle* row to manual/menu-tools.html (right under the FFXIV Channel Fixer entry, with inline XIV_on/off previews) and a new *"FFXIV SoundFont Mode - Toolbar Toggle"* section to manual/soundfont.html (#ffxiv-toolbar-toggle) covering Smart Enable (snapshot → locate → auto-download → activate) and Smart Disable / Fallback (restore snapshot → first non-FFXIV SF → Microsoft GS Wavetable Synth). manual/style.css gained a CSS exception for XIV_on/XIV_off (no inversion, native 72×25 size) so the inline previews render correctly in dark themes.

📝 Notes Notes

  • Version bumped to 1.5.2 in CMakeLists.txt.
  • The FFXIV download URL is the existing release asset at https://github.com/happytunesai/MidiEditor_AI/releases/download/soundfonts/FF14-c3c6-fixed.sf2, already wired into DownloadSoundFontDialog.
  • No agent / AI behaviour changes in this release.

✨ Added Files Added

  • src/gui/FfxivToggleWidget.h - toolbar widget header for the new XIV button.
  • src/gui/FfxivToggleWidget.cpp - toolbar widget implementation (paint/hover/click, syncs to FluidSynthEngine::ffxivSoundFontModeChanged).
  • src/gui/FfxivSoundFontHelper.h - public namespace API for enable/disable orchestration.
  • src/gui/FfxivSoundFontHelper.cpp - auto-download + snapshot/restore + Microsoft GS Wavetable Synth fallback logic.
  • run_environment/graphics/tool/XIV_on.png - green ON-state toolbar icon (72×25 native).
  • run_environment/graphics/tool/XIV_off.png - neutral OFF-state toolbar icon (72×25 native).
  • run_environment/graphics/tool/ffxiv_fix_dark.png - explicit dark-mode variant of the FFXIV Channel Fixer icon (first user of the new <name>_dark.png lookup).
  • manual/tools/XIV_on.png / manual/tools/XIV_off.png - manual copies of the toolbar icons for inline previews.

🟡 Medium Files Modified

  • src/midi/FluidSynthEngine.h / .cpp - added ffxivSoundFontModeChanged(bool) signal, emitted only on actual value change to avoid feedback loops with the toolbar widget.
  • src/gui/MainWindow.h / .cpp - created ffxivToggleAction, registered it in the action map, special-cased the toolbar widget instantiation in all four toolbar code paths, and seeded two action-order lists.
  • src/gui/LayoutSettingsWidget.cpp - registered ffxiv_toggle in the toolbar registry and appended it to all five default action-order lists.
  • src/gui/MidiSettingsWidget.cpp - onFfxivModeToggled now routes through FfxivSoundFontHelper::requestEnable/Disable and reverts the checkbox if the user cancels the download dialog.
  • src/gui/MatrixWidget.cpp - empty-file branch in setFile() centres the viewport on the C3..C6 range (line 61); files with notes are unchanged.
  • src/gui/Appearance.cpp - extended skipIcons with XIV_on + mcp_on; added automatic <name>_dark.png lookup in adjustIconForDarkMode(QString).
  • resources.qrc - registered XIV_on.png, XIV_off.png, ffxiv_fix_dark.png.
  • manual/menu-tools.html - added the FFXIV SoundFont Mode toggle row with inline icon previews.
  • manual/soundfont.html - added the new #ffxiv-toolbar-toggle section covering enable/disable orchestration.
  • manual/style.css - added CSS exception so the wide XIV_on/off icons render at native 72×25 size and the green ON variant is not inverted in dark themes.

v1.5.1

2026-04-28
FFXIV SoundFont Mapping Fixes
  • FFXIV SoundFont - Acoustic Guitar (nylon) silently played as Piano - FF14-c3c6-fixed.sf2 exposes Lute on GM program 25 (Acoustic Guitar steel slot), not 24, and Snare Drum / Cymbal on 118 / 119 rather than 115 / 127. The FFXIV instrument → GM-program tables in src/ai/FFXIVChannelFixer.cpp and src/ai/ToolDefinitions.cpp used the old (wrong) numbers, so the FFXIV Channel Fixer and the AI agent emitted Program Changes onto empty preset slots; FluidSynth then silently fell back to bank 0 / prog 0 (= Piano). Verified against the SF2 phdr chunk and corrected to 25 / 118 / 119.
  • FluidSynth - GM program fallback when running with the FFXIV SoundFont - old or imported MIDIs (e.g. Guitar Pro, MusicXML) frequently send Program Change 24 (Acoustic Guitar nylon) or 26 (Acoustic Guitar jazz). The FFXIV SoundFont contains no preset for those slots, so they collapsed to Piano. Added a small remap in FluidSynthEngine::sendMidiData() (only active when FFXIV SoundFont Mode is on): 24 → 25 (Lute), 26 → 27 (Clean Guitar). The remap is logged via qDebug as prog 24 → 25 (FFXIV fallback). All other programs are passed through unchanged.

v1.5.0

2026-04-21
Live Agent Streaming, Dynamic Models, Prompt Profiles, Agent Conductor & GPT-5.5 Isolation
  • Live agent streaming on every provider (Phase 25 + 27) - the Agent loop streams assistant text, tool-call arguments and reasoning live for OpenAI Chat-Completions, OpenAI Responses-API (gpt-5*), OpenRouter Chat-Completions and native Gemini (:streamGenerateContent?alt=sse with thinkingConfig.includeThoughts:true). A single extractReasoningFromJson() covers Responses-API reasoning items, Chat-Completions reasoning_content, Gemini parts[].thought and Anthropic thinking blocks, so the MidiPilot 💭 thought block renders live everywhere. Gemini 3.x's mandatory part.thoughtSignature is captured during streaming and echoed back on the follow-up functionCall, so multi-step Gemini agent loops no longer 400 with *"missing thought_signature"*.
  • GPT-5-family Responses-API routing (Phase 27.8) - OpenAI gpt-5* reasoning models with tools route through /v1/responses consistently, including reasoning summaries, tool-call argument reassembly, usage normalisation and a stable prompt_cache_key for repeated requests.
  • Per-session streaming fallback with Force Streaming override (Phase 27.7 + 31.1) - every streaming request is armed with a non-streaming retry; on HTTP 4xx/5xx, network error, or HTTP 200 with no parsable content/tool calls, MidiPilot transparently retries via sendRequest / sendMessages and marks the (provider, model, mode) triple with a warning icon for the rest of the session - Simple Mode and Agent Mode are tracked separately so a streaming failure in one mode never poisons the other. The Settings model dropdown, MidiPilot footer dropdown and the Force Streaming for This Model button show which mode is blocked: ⚠ <model> (Simple) / (Agent) / (Simple+Agent).
  • Dynamic provider model list with favourites & non-LLM filter (Phase 26) - model dropdowns drop their hardcoded entries; a 🔄 refresh button queries the active provider's /models endpoint, normalises the four response shapes (OpenAI / OpenRouter / Gemini / Custom) into <userdata>/midipilot_models.json (7-day TTL) and feeds AiClient::contextWindowForModel cache-first. Every cached model runs through ModelFavorites::isLikelyChatModel to drop image/audio/embedding/tts entries, and a new Settings → AI → "Manage favourites…" dialog persists per-provider favourites at AI/favorites/<provider> and shows favourites-only when any are pinned.
  • Persistent per-turn metadata + scrollable history popup (Phase 27.5 + 27.6) - MidiPilotHistory/<id>.json now stores a turns[] array (reasoning text, agent steps, latency, provider, model, token counts) alongside messages[]; loadConversation() re-renders saved 💭 thoughts and 🔧 Steps summaries so reopened conversations look like they did live. The history dropdown is a frameless QDialog with QLineEdit search, date-grouped scroll list and per-row Load / Delete buttons that stays usable past hundreds of conversations.
  • OpenRouter robustness (Phase 28) - *transient upstream errors* ("provider returned error" / "provider_name" / HTTP 400 + openrouter) are reclassified as RetryKind::Network and ride the existing 3-attempt back-off so the retry routes to a different upstream. *Capability-aware* HTTP 404 *"No endpoints found that support tool use"* (and the generic *"does not support tools / function calling"* family) is now caught up-front: AiClient::markToolsIncapableForCurrentModel persists the flag at AI/incapable_tools/<provider>:<model>, MidiPilot posts a clear "Model does not support tool calling - pick a different model or switch to Simple mode" bubble instead of burning three retries on a permanent gap, and onSendMessage short-circuits the agent loop until the user changes model.
  • Per-Model Prompt Profiles (Phase 29) - new PromptProfileStore and Settings → AI → "Prompt Profiles…" dialog let the user (or a built-in profile) attach a custom system prompt to one or more provider:model entries via checkbox, with optional glob suffixes (gpt-5.5*) and an "append to default" flag. Resolution runs in MidiPilotWidget::buildSystemPrompt() before the default is emitted; sidebar status shows *Prompt: <name>* whenever a profile is active. Ships read-only with GPT-5.5 Decisive, bound to openai:gpt-5.5* / openrouter:openai/gpt-5.5*, that appends explicit "commit after one short analysis paragraph; treat editor_state as pre-existing" rules.
  • Lightweight Agent Conductor & Working State (Phase 30) - AgentRunner keeps a compact program-owned AgentWorkingState (goal, taskType, confirmedState, lastToolResult, activeConstraints, nextStepHint, repeatedFailureCount). A heuristic classifier tags every run as composition / edit / analysis / repair; tool results are summarised into confirmed facts (e.g. *"Tempo set to 82 BPM; 8 tracks created; bars 1-16 inserted"*) without growing _messages. Before each request a synthetic high-priority Current Agent State developer-layer is injected request-locally so the model resumes from confirmed facts after rejections instead of re-deriving them. Bounded-failure stop after 2 consecutive incomplete writes preserves partial progress and surfaces an actionable explanation.
  • GPT-5.5 model-isolation policy (Phase 31) - central AgentToolPolicy table gates every GPT-5.5 mitigation behind isGpt55Model(model, provider), so non-GPT-5.5 runs are byte-identical to the previous behaviour. For gpt-5.5* composition/edit on OpenAI-native: schema-light tools (no pitch_bend branch in insert_events / replace_events), positive-only rejection guidance that never echoes the pitch_bend token back into context, per-request parallel_tool_calls:false + reasoning.effort:low overrides on the Responses API, and a bounded-failure stop after two consecutive incomplete writes. OpenRouter passthrough of gpt-5.5 keeps the schema/prompt mitigations but skips the API-body fields. Pitch Bend remains fully supported for every other model.
  • MidiPilot UX polish - chat bubbles render Markdown (bold, *italic*, lists, fenced code, links) for both the live reasoning stream and the final assistant bubble; agent step outcomes are colour-coded OK → green / retrying → orange / failed → red in the bottom status pill, with the dot-pulse + cycling fun-message animation kept alive while the agent loop is in flight; a Braille spinner replaces the underscore blink on the live thought label; the final assistant bubble now sits above the Agent Steps widget; the footer ⚙ menu's "AI Settings…" entry is renamed to "MidiPilot Settings…" to avoid implying an OpenAI-specific action.
  • Fixed MidiTrack copy ctor / reloadState losing _assignedChannel (TEST-001, legacy bug) - the default ctor initialises _assignedChannel = -1, but the copy ctor (src/midi/MidiTrack.cpp) and reloadState() only mirrored _number, _nameEvent, _file, _hidden, _muted. Because every setNumber()/setNameEvent()/setHidden()/setMuted() snapshots the track via copy() for the protocol stack, every track-attribute change produced a snapshot whose _assignedChannel was uninitialised heap memory; a subsequent undo would then reloadState() from that snapshot and overwrite the live track's channel assignment with garbage, silently corrupting FFXIV-channel routing across the undo stack. Discovered by tests/test_midi_track.cpp (which observed 513 in one run). Fixed by adding _assignedChannel = other._assignedChannel; to both methods; regression-tested.
  • tests/test_streaming_fallback.cpp - covers the per-session, mode-aware streaming fallback surface (streamingDisabledForCurrentModel / streamingBlockedForSession / markStreamingUnsupportedForCurrentModel / clearStreamingBlocklist) and the retrying signal used by the MidiPilot UI.
  • tests/test_model_favorites.cpp - chat-model heuristic, QSettings round-trip, and visibleModels() filter behaviour with and without favourites set.
  • tests/test_prompt_profiles.cpp - glob resolution, replace-vs-append flag, disabled profiles, persistence round-trip, built-in immutability and ordering between user custom / profile / default.
  • tests/test_agent_runner_state.cpp - task classification, working-state updates from successful tool results, request-local state-layer injection (does not permanently grow _messages), failure-to-steering conversion.
  • tests/test_agent_tool_policy.cpp - isGpt55Model matrix, schema-light branch gating, sanitised rejection guidance and Responses-API overrides for every covered (provider, model) pair.
  • tests/test_provider_matrix.cpp - live-API smoke runner that exercises all four AiClient code paths (non-stream / stream × no tools / +tools) for OpenAI, OpenRouter and Gemini; each provider is skipped unless its MIDIPILOT_TEST_<PROVIDER>_KEY env var is set.

🔧 Fixed Bug Fixes (Legacy)

  • TEST-001 - MidiTrack copy constructor / reloadState did not propagate _assignedChannel - the default ctor initialises _assignedChannel = -1, but the copy ctor (src/midi/MidiTrack.cpp lines 36-43) only mirrored _number, _nameEvent, _file, _hidden, _muted. The same omission was present in reloadState(). Because every setNumber()/setNameEvent()/setHidden()/setMuted() call snapshots the track via copy() for the protocol stack, every track-attribute change produced a snapshot whose _assignedChannel was uninitialised heap memory. A subsequent undo would then reloadState() from that snapshot and overwrite the live track's channel assignment with garbage - silent FFXIV-channel-routing corruption that survived since the field was added. Fix: add _assignedChannel = other._assignedChannel; to both methods. tests/test_midi_track.cpp::copy_clonesNumberHiddenMutedAndFile now actively asserts clone->assignedChannel() == 7, and reloadState_fromMidiTrackEntry_restoresAllFields was extended to mutate and restore assignedChannel as well.

✨ Added New Features

  • Phase 25 + 27 - Live agent-loop streaming everywhere - AiClient::sendStreamingMessages(messages, tools) mirrors the existing sendMessages but with stream:true. The OpenAI-CC parser handles choices[0].delta.tool_calls[*].function.{name,arguments} deltas: per-call accumulators keyed by tool_calls[i].index collect id + name from the first chunk and append arguments JSON fragments from subsequent chunks. New signals streamAssistantTextDelta, streamReasoningDelta, streamToolCallStarted, streamToolCallArgsDelta, streamToolCallArgsDone flow through the agent loop; the reassembled assistant message is emitted via the existing responseReceived signal so AgentRunner::onApiResponse is unchanged. Per-provider routing:
  • OpenAI Chat-Completions / OpenRouter - OpenAI-CC SSE shape, full text + tool-args + (OpenRouter only) delta.reasoning deltas.
  • OpenAI Responses-API (Phase 27.8) - sendStreamingMessagesResponses + onResponsesStreamDataAvailable parses event-typed SSE (response.output_text.delta, response.reasoning_summary_text.delta, response.function_call_arguments.delta, response.completed) and reassembles a synthetic Chat-Completions payload so AgentRunner is unchanged. Used for gpt-5* reasoning models when tools are present; reasoning summaries stream live.
  • Native Gemini - sendStreamingMessagesGemini posts to https://generativelanguage.googleapis.com/v1beta/models/<model>:streamGenerateContent?alt=sse&key=<KEY> because Google's OpenAI-compat endpoint rejects stream:true + tools with HTTP 400. Helpers convert OpenAI-shape messages[] → Gemini contents[] + systemInstruction (with a tool_call_id → name lookup so Gemini receives function names, not opaque OpenAI ids) and OpenAI tools → tools[0].functionDeclarations[]. generationConfig.thinkingConfig.includeThoughts:true is enabled on Gemini 2.5+/3.x with thinkingBudget mapped from the existing reasoning_effort (low=2048, medium=8192, high=24576, off=0, default=auto). Thought parts (parts[].thought == true) emit streamReasoningDelta; whole functionCall parts get a synthetic call id (gemini_<n>) so AgentRunner reassembly stays uniform. Gemini 3.x's mandatory part.thoughtSignature is captured per StreamToolCall::thoughtSignature and written back onto the functionCall on the next request via the synthetic _gemini_thought_signature field.
  • Universal reasoning extractor - AiClient::extractReasoningFromJson(QJsonObject) walks every known shape (Responses-API reasoning items, Chat-Completions reasoning_content / reasoning, Gemini parts[].thought, Anthropic content[].type == "thinking", plus generic reasoning / thoughts fallbacks), so the 💭 thought block UI lambda is provider-agnostic.
  • Gemini schema sanitiser - sanitizeSchemaForGemini() strips OpenAPI-3.0-incompatible fields (additionalProperties, $schema, $id, $ref, definitions, strict) and removes integer enums, so MidiPilot's strict-mode tool schemas no longer trip Gemini's validator with HTTP 400.
  • Setting: AI/streaming_mode (default "on") gates the path in AgentRunner::sendNextRequest; checkbox Settings → AI → Live Streaming → "Stream agent responses live (text + tool-call arguments)".
  • Phase 27.7 + 31.1 - Per-session streaming fallback safety net (mode-aware) - every streaming request arms a per-request retry context (armStreamingRetryAgent / armStreamingRetrySimple) before the POST. Each streaming finished-lambda inspects the outcome via shouldFallbackToNonStreaming(httpStatus, netError, gotContent, gotToolCalls) and, when true, tryStreamingFallback(reason) dispatches to the regular sendRequest / sendMessages path and emits the new retrying(QString) signal so the UI can show the reason. The offending entry is persisted via markStreamingUnsupportedForCurrentModel(reason) keyed by (provider, model, mode) - Simple Mode (tools=0) and Agent Mode (tools=1) are tracked separately so a streaming failure in one mode never disables streaming for the other. Public 2-arg helpers (streamingBlockedForSession(provider, model), clearStreamingBlockForSession) keep their meaning by OR-ing both modes. Wired into all four streaming entry points (Chat-Completions agent + simple, Responses-API agent, Gemini native). Explicitly not retried: QNetworkReply::OperationCanceledError (user pressed Cancel) and Gemini semantic finish reasons SAFETY, RECITATION, MAX_TOKENS, MALFORMED_FUNCTION_CALL. UI: Settings model dropdown, MidiPilot footer dropdown and Force Streaming for This Model button show ⚠ <model> (Simple) / (Agent) / (Simple+Agent) with matching tooltips and button labels.
  • Phase 26 - Dynamic provider model list with favourites & non-LLM filter - drops hardcoded _modelCombo->addItem(…) from AiSettingsWidget::populateModelsForProvider and MidiPilotWidget::populateFooterModels. New components:
  • src/ai/ModelListCache.{h,cpp} - JSON cache file at <AppDataLocation>/midipilot_models.json (versioned schema, 7-day TTL, per-provider entries with id, displayName, contextWindow, supportsTools, supportsReasoning).
  • src/ai/ModelListFetcher.{h,cpp} - single-shot QNetworkAccessManager worker that hits the per-provider /models endpoint, normalises four response shapes (OpenAI data[].id, OpenRouter data[].{id,name,context_length,architecture,supported_parameters}, Gemini models[].{name,inputTokenLimit,supportedGenerationMethods,displayName}, Custom data[].id) into the cache schema, filters out embedding/audio/image/legacy/preview entries, and emits finished(provider, array) or failed(provider, error).
  • AiClient::contextWindowForModel consults ModelListCache::contextWindowFor() first, falls back to the hardcoded prefix-match table only when the cache has no entry.
  • UI: 🔄 button next to the model combo in both AiSettingsWidget and the MidiPilot footer; settings dialog shows a small "Models updated 2 days ago" status line. Combos remain editable so the user can still type a not-yet-published model id.
  • ModelFavorites + non-LLM filter - every cached model runs through isLikelyChatModel to drop image/audio/embedding/tts entries; Settings → AI → "Manage favourites…" dialog persists per-provider favourites at AI/favorites/<provider> and shows favourites-only when any are pinned.
  • Phase 27.5 + 27.6 - Persistent per-turn metadata + scrollable history popup
  • MidiPilotHistory/<id>.json now persists a turns[] array alongside messages[]. Each turn anchors to its assistant message via assistantIndex and stores reasoning, steps[] ({step, tool, success, recoverable}), streamed, latencyMs, effort, provider, model, promptTokens, completionTokens, status. MidiPilotWidget::resetTurnState() snapshots provider/model/effort + start time at user-send; streamReasoningDelta and streamAssistantTextDelta lambdas accumulate the reasoning text and flip _turnStreamed; onAgentStepCompleted appends a compact step record; finalizeTurn() is called from onAgentFinished / onAgentError / onResponseReceived and seals the record. loadConversation() re-indexes turns[] by assistantIndex and re-renders the saved 💭 thought block above and the 🔧 Steps: ✓ tool1, ✗ tool2 summary below each assistant bubble.
  • showHistoryMenu rewritten from QMenu to a frameless QDialog with QLineEdit search, QScrollArea body capped at ~500 px, conversations grouped by date bucket (Today / Yesterday / weekday / Month Year), per-row Load + Delete buttons. Live filter hides non-matching rows and collapses now-empty section headers.
  • Phase 28 - OpenRouter robustness & capability-aware error handling
  • 28.1 Transient-upstream classifier - AgentRunner::classifyError and MidiPilotWidget::onErrorOccurred::isRetriable treat "provider returned error", "provider_name" and HTTP 400 + openrouter as RetryKind::Network, so the existing self-healing retry kicks in (3 attempts, exponential back-off). Reuses AI/agent_max_retries / AI/simple_max_retries.
  • 28.2 Capability-aware error surfacing (HTTP 404 - no tool support) - AiClient::errorIndicatesNoToolSupport(error) heuristic catches the OpenRouter HTTP 404 *"No endpoints found that support tool use"* family plus generic *"does not support tools / function calling is not supported"* variants. AgentRunner::onApiError checks this before the retry classifier, calls markToolsIncapableForCurrentModel(reason) and surfaces *"Model does not support tool calling - pick a different model in Settings → AI, or switch to Simple mode for this request."* The flag is persisted at AI/incapable_tools/<provider>:<model> via toolsIncapableForCurrentModel() / markToolsIncapableForCurrentModel() / clearToolsIncapableFlag() (mirrors the streaming-blocklist API). MidiPilotWidget::onSendMessage (agent branch) consults the flag before spinning up the agent loop and posts a friendly system bubble instead of round-tripping. Per-(provider,model), so picking a different model re-enables agent mode automatically.
  • Phase 29 - Per-Model System Prompt Profiles
  • src/ai/PromptProfileStore.{h,cpp} + src/ai/PromptProfile.h - store keyed under AI/prompt_profiles/<id>/{name, system, append_to_default, builtin, models[], enabled} plus AI/prompt_profiles/order. models[] is an array of <provider>:<modelId> entries with * glob suffixes (e.g. openai:gpt-5.5*). Resolution in resolvePromptForModel(provider, model, defaultPrompt, userCustom):
  • src/gui/PromptProfilesDialog.{h,cpp} - list with checkbox enable, Add/Duplicate/Delete; right pane has name, append-to-default flag, monospace prompt editor with token count, and the Provider → Model QTreeWidget from ModelFavoritesDialog for binding. Built-ins show a lock icon and can only be duplicated. Reachable from Settings → AI → "Prompt Profiles…" and the MidiPilot ⚙ menu.
  • MidiPilotWidget::buildSystemPrompt() calls _profileStore->resolvePromptForModel(...) at the existing custom-vs-default decision point; sidebar status line shows *"Prompt: <name> (auto-bound)"* when a profile resolved.
  • Built-in: GPT-5.5 Decisive - builtin=true, bound to openai:gpt-5.5* and openrouter:openai/gpt-5.5*, append_to_default=true. Body adds: commit after one short analysis paragraph; treat editor_state events as pre-existing user data; do not re-derive ticksPerQuarter / timeSignature across turns; do not "redo" or "fix" successful tool calls unless the user explicitly says so.
  • Phase 30 - Lightweight Agent Conductor & Working State
  • AgentRunner::AgentWorkingState - {goal, taskType, confirmedState, lastToolResult, activeConstraints, nextStepHint, repeatedFailureCount}, kept under ~1200 chars by coalescing older facts (e.g. *"Tracks created: Piano, Bass, Drums, Lead"*).
  • classifyTask(userMessage, systemPrompt) - heuristic classifier produces composition / edit / analysis / repair; result steers cadence and policy.
  • Working-state updates from tool results (updateWorkingStateFromToolResult) - create_track / set_tempo / insert_events / replace_events successes append confirmed facts; query_events / get_editor_state summarise counts (not payload); rejected writes set lastToolResult and a corrective nextStepHint; duplicate-write rejections increment repeatedFailureCount.
  • Dynamic state-layer injection - messagesForNextRequest() clones _messages and inserts one synthetic high-priority Current Agent State message immediately after the developer/system prompt; the layer is regenerated every turn from AgentWorkingState. _messages itself never grows from this - it stays the canonical protocol transcript.
  • Composition cadence - for taskType == composition, nextStepHint favours one substantial insert_events / replace_events per track or section over inspect-after-every-phrase loops.
  • Bounded-failure stop - if repeatedFailureCount >= 2, the loop halts with an actionable user-facing explanation; partial successful changes remain in the protocol stack.
  • Diagnostics: every turn logs [AGENT-STATE] step=N task=... confirmed="..." last="..." next="...".
  • Phase 31 - GPT-5.5 model-isolation policy
  • src/ai/AgentToolPolicy.{h,cpp} - central policy table; every GPT-5.5 mitigation is gated behind isGpt55Model(model, provider), so non-GPT-5.5 runs are byte-identical to the previous behaviour.
  • For gpt-5.5* composition/edit on OpenAI-native:
  • Schema-light tools - insert_events / replace_events are emitted without the pitch_bend event branch in their JSON schemas, removing the placeholder anti-pattern from the model's available actions for this model only.
  • Sanitised rejection guidance - AgentRunner::processToolCalls rewrites failure guidance to positive-only language and never echoes the pitch_bend token back into context.
  • Per-request API overrides - parallel_tool_calls:false + reasoning.effort:low injected into the Responses-API body to cap reasoning explosions on long composition prompts.
  • Bounded-failure stop - same 2-incomplete-write threshold as Phase 30, with model-specific guidance.
  • OpenRouter passthrough of gpt-5.5 keeps the schema/prompt mitigations but skips the API-body fields (Responses-API specific).
  • Pitch Bend is unchanged for every other model - no schema diff, no rejection logic, no behaviour change.
  • MidiPilot UX polish
  • Chat bubbles render Markdown (Qt::MarkdownText on the final assistant bubble, the live streaming bubble and the reasoning 💭 stream label, including loaded conversations). User and system bubbles stay Qt::PlainText.
  • Bottom status pill colour-codes step outcomes - OK → green, retrying → orange, failed → red - and the dot-pulse + cycling fun-message animation stays alive while the agent loop is in flight regardless of colour. A successful step's green flash auto-reverts to Thinking… (orange) after 700 ms so the bar never appears to freeze on Step N: OK.
  • Braille spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏, ~8 fps) on the live thought label, replacing the underscore blink.
  • onAgentFinished / onAgentError lift the Steps widget out of the dock so the order in chat becomes [thoughts] → [final response] → [steps].
  • Footer ⚙ menu's "AI Settings…" entry renamed to "MidiPilot Settings…" (and three matching streaming-block tooltip strings) - the old label looked like an OpenAI-specific action.

🟡 Medium New Files

  • src/ai/ModelListCache.{h,cpp}, src/ai/ModelListFetcher.{h,cpp}
  • src/ai/ModelFavorites.{h,cpp}, src/gui/ModelFavoritesDialog.{h,cpp}
  • src/ai/PromptProfile.h, src/ai/PromptProfileStore.{h,cpp}, src/gui/PromptProfilesDialog.{h,cpp}
  • src/ai/AgentToolPolicy.{h,cpp}
  • tests/test_streaming_fallback.cpp, tests/test_model_favorites.cpp, tests/test_prompt_profiles.cpp, tests/test_agent_runner_state.cpp, tests/test_agent_tool_policy.cpp, tests/test_provider_matrix.cpp

🟡 Medium Files Modified

  • src/midi/MidiTrack.cpp - copy ctor and reloadState() now copy _assignedChannel (TEST-001)
  • tests/test_midi_track.cpp - copy_clonesNumberHiddenMutedAndFile flipped from "document gap" to active assertion; reloadState_fromMidiTrackEntry_restoresAllFields extended to cover assignedChannel
  • src/ai/AiClient.{h,cpp} - streaming entry points for Chat-Completions, Responses-API and native Gemini; SSE parsers; mode-aware streaming-blocklist API; tools-incapable flag API; contextWindowForModel cache lookup
  • src/ai/AgentRunner.{h,cpp} - streaming path selection, working-state conductor, task classifier, request-local state-layer injection, capability-aware error handling
  • src/ai/ToolDefinitions.{h,cpp} - per-policy schema-light branch for insert_events / replace_events
  • src/gui/AiSettingsWidget.{h,cpp} - model combo + 🔄 refresh button + status label; "Live Streaming" checkbox; "Manage favourites…" + "Prompt Profiles…" buttons; mode-aware streaming-block status
  • src/gui/MidiPilotWidget.{h,cpp} - footer model combo + 🔄 button; per-turn metadata; history dialog; capability-aware send guard; Markdown chat bubbles; status pill colour coding; spinner; rename to "MidiPilot Settings…"
  • Planning/03_bugs.md - TEST-001 marked FIXED
  • Planning/02_ROADMAP.md - Phases 26, 27, 28.1, 28.2, 29, 30, 31 marked DONE

📝 Notes Notes

  • Version bumped to 1.5.0 in CMakeLists.txt. TEST-002 (SysExEvent VLQ length prefix) remains open in Planning/03_bugs.md - reader and writer are paired non-spec-conformant, fix needs a tolerant loader plus spec-conformant writer plus real third-party SysEx fixtures and is therefore deferred.
  • Phase 26 keeps the existing hardcoded model lists as a fallback so a fresh install or an offline user still sees a populated picker. Refresh is opt-in (user clicks 🔄); we explicitly do not auto-refresh on app launch to avoid a network call on every startup.
  • Phase 28 sub-phases 28.3 (per-model capability cache from /models), 28.4 (provider-pinning UI), 28.5 (upstream attribution in chat) and 28.6 (long-timeout awareness for reasoning models) remain on the roadmap for a later release.

v1.4.2

2026-04-21
Bulk-Op Memory/Perf Fixes + Test Harness Expansion + Live Playback Panels
  • Side panels stay live during playback by default (UX-PLAY-001) - MainWindow::play() and record() previously hard-disabled the Tracks, Channels, Event and Protocol panels for the entire duration of playback / recording, blocking visibility toggling and event inspection mid-song. Now unlocked by default; legacy lock-during-playback behaviour can be restored via Settings → System & Performance → Playback → "Lock side panels during playback" (playback/lock_panels, default false).
  • Eye icons in the Matrix "Move to Track" / "Move to Channel" context-menu submenus (UX-CTX-001) - right-clicking a note now shows a quick-glance visibility cue next to every entry in both submenus (all_visible.png for visible / unmuted, all_invisible.png for hidden), so you can see at a glance which tracks/channels are currently shown without having to scroll the side panels.
  • Track deletion no longer eats 40-50 GB of RAM on heavy/CC-dense tracks (PERF-DEL-001) - MidiFile::removeTrack deep-cloned the full channel QMultiMap once per removed event; brass tracks with thousands of Breath-Controller CCs blew the open undo step into tens of GB.
  • MidiFile::deleteMeasures and MidiFile::setMaxLengthMs got the same treatment (PERF-DEL-002, PERF-DEL-003) - same per-event-protocol explosion in the two sibling mass-delete paths.
  • FFXIV Channel Fixer Tier 2 dropped from ~5 minutes / ~64 GB to seconds / bounded RAM (PERF-FFXIV-001) - five hot mutation sites in FFXIVChannelFixer::fixChannels() were each cloning the full track + channel state per call.
  • NoteOnEvent::setVelocity(int, bool toProtocol = true) overload (API-002) - needed by the FFXIV bulk-op refactor so the velocity-normalisation pass can skip the per-event Protocol clone; default behaviour unchanged for existing callers.
  • C++ test harness grown from 3 executables / 41 sub-tests to 19 / 229 (PHASE-25.3) - new coverage across MidiTrack, MidiChannel, MidiFile, Protocol, ProtocolItem, Selection, EventTool, LyricManager, LyricBlock, MidiEvent, channel events, SysExEvent, and MmlConverter. Full ctest run: 100% tests passed, 0 tests failed out of 19 in ~14 s.

✨ Added New Features

  • Live side panels during playback (UX-PLAY-001) - historical MidiEditor behaviour was to call setEnabled(false) on _miscWidgetContainer, channelWidget, protocolWidget, _trackWidget and eventWidget() for the duration of every play/record session, then re-enable them in stop(). The intent was to prevent the user from mutating state mid-playback, but in practice it also blocked harmless interactions like toggling channel/track visibility, inspecting an event in the Event tab, or scrolling the Protocol log while listening. The disables are now gated on _settings->value("playback/lock_panels", false).toBool(), defaulting to off - panels remain interactive throughout playback. A new "Lock side panels during playback" checkbox under Settings → System & Performance → Playback restores the legacy behaviour for users who relied on it. The stop() re-enable path is unchanged (it's a safe no-op when the panels were never disabled in the first place).
  • Eye icons on the Matrix context-menu "Move to…" submenus (UX-CTX-001) - MatrixWidget::contextMenuEvent() now sets a per-entry icon on every action inside the Move to Track and Move to Channel submenus, mirroring the visibility/audibility state from MidiTrack::hidden() and MidiChannel::visible(): visible entries get :/run_environment/graphics/tool/all_visible.png, hidden entries get all_invisible.png. Saves a trip to the side panel when you're trying to remember which channels are currently muted before reassigning a note.

🔧 Fixed Bug Fixes

  • Fixed track deletion ballooning RAM to 40-50 GB on CC-dense brass tracks (PERF-DEL-001) - MidiFile::removeTrack (src/midi/MidiFile.cpp) iterated every event on every channel and called channels[ch]->removeEvent(event) with the default toProtocol=true. That path runs MidiChannel::copy(), which deep-clones the channel's full QMultiMap<int, MidiEvent*> into a fresh ProtocolItem - so a Trumpet track with thousands of "Breath Controller (MSB)" CC events produced thousands of full-channel clones piling up inside one open undo step. On the user's reference file peak RSS hit ~50 GB and the UI stalled for minutes. Replaced with the proven bulk-snapshot pattern: one channel->copy() per touched channel before the event-removal loop, removeEvent(ev, false) inside the loop, then one channels[i]->protocol(snap, channels[i]) commit per channel after. Undo semantics are identical because MidiChannel::reloadState() swaps the _events pointer back wholesale - a single pre-mutation snapshot covers any number of fine-grained mutations. Peak RAM during a track delete now scales with channel count (~19), not event count.
  • Fixed MidiFile::deleteMeasures having the same per-event clone explosion (PERF-DEL-002) - same root cause as PERF-DEL-001 in the "Delete Measures" path: each channel(ch)->removeEvent(event) defaulted to toProtocol=true and cloned the full channel map. Rewritten to snapshot only the channels that actually have events to delete (skipping the snapshot when toRemove is empty), pass false to removeEvent, and commit the snapshots at the end.
  • Fixed MidiFile::setMaxLengthMs truncating long files via per-event protocol calls (PERF-DEL-003) - setMaxLengthMs is called when a SoundFont/audio export needs to clip the tail; on long files the trailing-event delete loop produced the same blow-up. Reworked to use a QHash<int, ProtocolEntry*> channelSnapshots keyed by channel: snapshot lazily on first deletion in a channel, mutate fast, commit at the end.
  • Fixed FFXIV Channel Fixer Tier 2 taking ~5 minutes and ~64 GB on 20-track guitar files (PERF-FFXIV-001) - FFXIVChannelFixer::fixChannels() (src/ai/FFXIVChannelFixer.cpp) had five hot mutation sites all defaulting to toProtocol=true: MidiChannel::removeEvent in the CLEAN phase, MidiChannel::insertEvent for relocated CCs/PCs, MidiEvent::moveToChannel during MIGRATE, NoteOnEvent::setVelocity during the velocity-normalisation pass, and the per-event setTrack calls. On a heavy file each call deep-cloned the whole channel + track snapshot, producing tens of thousands of clones inside the single open undo action. Now snapshots every touched track + channel once before the CLEAN/MIGRATE phases (QVector<ProtocolEntry*> trackSnapshots, QVector<ProtocolEntry*> channelSnapshots), runs all mutations with toProtocol=false, and commits the snapshots via entry->protocol(snap, current) before the JSON result is built. User confirmed: same file that previously took ~5 minutes now completes in seconds with bounded RAM.
  • Added NoteOnEvent::setVelocity(int v, bool toProtocol = true) overload (API-002) - the FFXIV bulk-op refactor needs to mutate velocity without firing the per-event protocol path. Header (src/MidiEvent/NoteOnEvent.h) declares the overload with toProtocol defaulted to true, body (src/MidiEvent/NoteOnEvent.cpp) takes the fast path if (!toProtocol) { _velocity = v; return; } before reaching the existing copy/protocol calls. All existing call sites compile unchanged.

🟡 Medium Test Harness (PHASE-25.3)

  • Sixteen new test executables added under tests/, bringing ctest to 19 executables / 229 sub-tests (was 3 / 41 in v1.4.1):
  • tests/test_midi_track.cpp - MidiTrack ctor / copy ctor / accessors.
  • tests/test_midi_channel.cpp - MidiChannel event insertion, removal, progAtTick, eventMap invariants.
  • tests/test_midi_file.cpp - MidiFile open/save round-trip, pasteTrack, removeTrack, deleteMeasures, msOfTick boundary cases.
  • tests/test_protocol.cpp and tests/test_protocol_item.cpp - Protocol action lifecycle, goTo undo/redo navigation, ProtocolItem reverse-action correctness.
  • tests/test_selection.cpp - Selection singleton return-by-value invariant (since v1.3.2), setSelection no-op detection, signal emission.
  • tests/test_event_tool.cpp - EventTool selection helpers, paste path local-list construction.
  • tests/test_lyric_manager.cpp and tests/test_lyric_block.cpp - block insert/sort/split/merge, overlap clamping.
  • tests/test_midi_event.cpp - base-class accessors and meta-channel routing.
  • tests/test_channel_events.cpp - save round-trips for ControlChangeEvent, PitchBendEvent, ProgChangeEvent, ChannelPressureEvent, KeyPressureEvent (status-byte / channel-nibble correctness).
  • tests/test_sysex_event.cpp - F0/F7 framing and length encoding round-trip.
  • tests/test_mml_converter.cpp - MML scale/tempo/length/dot/octave/tie/sharp/flat/instrument/garbage/empty cases (14 sub-tests).

📝 Notes Technical Notes - The Bulk-Snapshot Pattern

🟡 Medium Files Modified

  • CMakeLists.txt - version 1.4.11.4.2
  • src/gui/MainWindow.cpp - play() and record() panel-disable blocks gated behind playback/lock_panels QSettings key, default off (UX-PLAY-001)
  • src/gui/PerformanceSettingsWidget.h / .cpp - new "Playback" group with "Lock side panels during playback" checkbox; persisted under playback/lock_panels; reset to unchecked in resetToDefaults() (UX-PLAY-001)
  • src/gui/MatrixWidget.cpp - contextMenuEvent() sets visibility eye icons on every entry of the Move to Track and Move to Channel submenus (UX-CTX-001)
  • manual/editor-and-components.html - new "Context Menu" subsection documenting the visibility eye icons (UX-CTX-001)
  • manual/playback.html - new "Live Side Panels During Playback" subsection documenting the new default and the opt-out toggle (UX-PLAY-001)
  • manual/index.html / manual/download.html - version bump 1.4.1 → 1.4.2; What's New card updated
  • manual/changelog.html - regenerated from CHANGELOG.md via scripts/build_changelog.py
  • src/MidiEvent/NoteOnEvent.h / .cpp - new setVelocity(int, bool toProtocol = true) overload with fast-path skip (API-002)
  • src/ai/FFXIVChannelFixer.cpp - bulk-snapshot pattern applied to all five hot mutation sites, plus required #include "../protocol/ProtocolEntry.h" and <QVector> (PERF-FFXIV-001)
  • src/midi/MidiFile.cpp - removeTrack snapshots all 19 channels, mutates with removeEvent(ev, false), commits per-channel (PERF-DEL-001); deleteMeasures lazy-snapshots only channels with deletions (PERF-DEL-002); setMaxLengthMs uses QHash<int, ProtocolEntry*> lazy-snapshot map (PERF-DEL-003)
  • tests/CMakeLists.txt - registered 16 additional test executables with the Qt 6 DLL-PATH workaround
  • tests/test_midi_track.cpp (new)
  • tests/test_midi_channel.cpp (new)
  • tests/test_midi_file.cpp (new)
  • tests/test_protocol.cpp (new)
  • tests/test_protocol_item.cpp (new)
  • tests/test_selection.cpp (new)
  • tests/test_event_tool.cpp (new)
  • tests/test_lyric_manager.cpp (new)
  • tests/test_lyric_block.cpp (new)
  • tests/test_midi_event.cpp (new)
  • tests/test_channel_events.cpp (new) - covers CC, PC, PitchBend, ChannelPressure, KeyPressure save round-trips
  • tests/test_sysex_event.cpp (new)
  • tests/test_mml_converter.cpp (new)
  • Planning/03_bugs.md - PERF-DEL-001/002/003 and PERF-FFXIV-001 noted as fixed in 1.4.2
  • Planning/06_TEST_CASES.md - coverage table updated 3 → 19 executables / 41 → 229 sub-tests

v1.4.1

2026-04-19
Manual Bugfix + SrtParser Tests
  • Fixed stale "What's New" card on the manual landing page (DOC-002) - the index.html What's New block was hardcoded HTML still showing v1.3.2 content even after the v1.4.0 release. The sister changelog.html is auto-generated from CHANGELOG.md by scripts/build_changelog.py, so it had the right content; the landing card was the only place still drifting. manual/site.js now fetches changelog.html once (a promise shared with the existing dynamic bugfix counter), pulls the first .cl-version block, and rewrites the card's heading, italic subtitle, and bullet list at runtime. The static markup in index.html was also refreshed to v1.4.0 content as a no-JS fallback, and a small .whats-new-subtitle rule was added to site.css.
  • SrtParser unit tests (PHASE-25.2) - new tests/test_srt_parser.cpp with 14 cases covering the happy path (two-block CRLF SRT), . accepted in place of , per the regex, multi-line cue text joined with a single space, final entry without a trailing blank, malformed timing line dropped while parser recovers, UTF-8 BOM stripped from the first line, missing file → empty list, null MidiFile* → empty list, full export↔import round-trip, empty / null export rejection, and canonical HH:MM:SS,mmm formatting for 1h+ timecodes. ctest is now 3 executables / 41 sub-tests, all green.
  • CHANGELOG_TEMPLATE.md removed from the repo - added to .gitignore and git rm --cached'd. The file stays on disk locally as a personal scratch template; it is no longer part of the published source tree.
  • CMakeLists.txt - version 1.4.01.4.1
  • manual/site.js - shared changelogDocReady promise; new populateWhatsNew() rewrites the landing card from changelog.html
  • manual/index.html - version bump (4 places); What's New static fallback refreshed to v1.4.0 content with new .whats-new-subtitle
  • manual/site.css - .whats-new-subtitle rule (italic, muted, tight top margin)
  • manual/changelog.html - regenerated from CHANGELOG.md via scripts/build_changelog.py
  • tests/test_srt_parser.cpp (new) - 14 unit tests for SrtParser
  • tests/CMakeLists.txt - register test_srt_parser with the Windows DLL-PATH workaround
  • Planning/06_TEST_CASES.md - SrtParser row ⬜ → ✅; coverage table 2 → 3 executables / 24 → 41 sub-tests
  • .gitignore - ignore CHANGELOG_TEMPLATE.md

v1.4.0

2026-04-19
MusicXML & MuseScore Import + C++ Test Harness
  • MusicXML import - open .musicxml, .xml, and compressed .mxl scores from Finale, Sibelius, MuseScore, Dorico, and any notation tool that exports MusicXML; parts, voices, chords, rests, tuplets, dotted notes, time/key signatures, tempo, and instrument program changes are converted to MIDI on the fly (PHASE-24.1)
  • MuseScore import - open .mscz (zipped) and .mscx (plain XML) files from MuseScore 3/4 directly, with the same coverage as MusicXML plus grace notes (PHASE-24.2)
  • Shared SMF writer - extracted XmlScoreToMidi so MusicXML and MuseScore importers emit Standard MIDI File bytes through a single, unit-tested code path instead of duplicating delta-time and meta-event logic in two places (PHASE-24.3)
  • Format-aware error dialogs - failed file opens now report the actual format ("MusicXML import failed", "MuseScore .mscz import failed") instead of a generic "load failed", making it obvious whether the user picked the wrong file or a real parser error occurred (UX-OPEN-001)
  • First C++ unit test harness - opt-in Qt Test executables under tests/ (-DBUILD_TESTING=ON), runnable via build_tests.bat. Initial coverage: XmlScoreToMidi (6 cases) and ChordDetector (18 cases). See Planning/06_TEST_CASES.md for the test roadmap (PHASE-25.1)
  • Manual reorganization - Guitar Pro Import page replaced by a unified Supported Files page covering MIDI, Guitar Pro, MusicXML, and MuseScore in one place; old URL redirects automatically (DOC-001)
  • CHANGELOG_TEMPLATE.md - new top-level template documenting the canonical changelog entry format (Summary + `

✨ Added New Features

  • MusicXML / MXL importer (PHASE-24.1) - src/converter/MusicXml/MusicXmlImporter.{h,cpp} is a full XML → MIDI converter for the standard interchange format used by Finale, Sibelius, MuseScore, Dorico, and most notation software. Single-file .musicxml / .xml files are parsed directly via QXmlStreamReader; .mxl containers are auto-detected via the ZIP signature, unpacked through QuaZip, and the embedded META-INF/container.xml is read to locate the rootfile. The parser walks each <part>'s measures, tracking the running divisions value, time/key signature changes, tempo metas (<sound tempo="…"> and <direction> BPM hints), and per-voice cursors so chords and grace notes accumulate at the correct tick. Output goes through the shared XmlScoreToMidi writer, producing one MIDI track per part with correct delta times. Sample files committed under the workspace root (BeetAnGeSample.musicxml, MozartTrio.musicxml, il-vento-doro-giornos-theme.mxl).
  • MuseScore .mscz / .mscx importer (PHASE-24.2) - src/converter/MusicXml/MsczImporter.{h,cpp} is a native parser for MuseScore's own format. .mscx plain XML is read directly; .mscz containers are unzipped (the embedded .mscx is found by extension scan, since the inner filename varies). The parser handles MuseScore's part / staff / voice hierarchy, durationType strings (whole, half, …, 64th), dotted notes via <dots>, tuplets via <actualNotes>/<normalNotes>, chord groupings via the <Chord> wrapper, instrument program changes via <Channel><program value="…"/></Channel>, and grace notes via the <grace…> element family. Tempo, time signature, and key signature metas land on the correct meta channels. Same XmlScoreToMidi back-end as the MusicXML path.
  • Shared SMF writer (PHASE-24.3) - src/converter/MusicXml/XmlScoreToMidi.{h,cpp} is the single back-end used by both importers. Takes a XmlScore model (parts → events with absolute ticks and channels) and emits a complete Standard MIDI File: header chunk (format 1, division = 480 PPQ), one track per part, default tempo (500000 µs / quarter = 120 BPM) and time signature (4/4) inserted on track 0 when the score has none, correct VLQ delta times between events, and an explicit end-of-track meta on every track. Avoiding two parallel SMF emitters means parser bugs only have to be fixed in one place; it is also the first piece of the import pipeline with real unit tests.
  • Format-aware error dialogs (UX-OPEN-001) - MainWindow::openFile() previously delegated to a generic "load failed" message regardless of which importer rejected the file. Now the open path inspects the file extension and produces "MusicXML import failed: <reason>", "MuseScore .mscz import failed: <reason>", or "Guitar Pro import failed: <reason>" so users immediately know whether the file was malformed or whether they accidentally pointed the importer at the wrong format.
  • Unified manual/supported-files.html (DOC-001) - single reference page documenting every input format MidiEditor AI can open: MIDI (.mid / .midi), Guitar Pro (.gp / .gp3 / .gp4 / .gp5 / .gpx / .gtp), MusicXML (.musicxml / .xml / .mxl), and MuseScore (.mscz / .mscx). Replaces the old Guitar-Pro-only page; manual/guitar-pro.html is now a meta-refresh redirect to supported-files.html#guitar-pro so existing bookmarks and external links still resolve.

🟡 Medium Test Harness (PHASE-25.1)

  • tests/CMakeLists.txt - registers Qt Test executables behind a top-level BUILD_TESTING option (default OFF, opt-in via -DBUILD_TESTING=ON). Applies the Windows DLL-path workaround (add_test … set_tests_properties(... PROPERTIES ENVIRONMENT "PATH=…")) required for ctest to find Qt 6 at runtime; without it every test executable would fail to launch with qt6Core.dll not found.
  • tests/test_xml_score_to_midi.cpp - six cases covering the SMF writer's invariants: MThd header bytes, expected track count for a multi-part score, NoteOn ordering when two events share a tick, presence of a default tempo meta on track 0 when none was supplied, presence of a default 4/4 time-sig meta, and the explicit end-of-track marker on every track.
  • tests/test_chord_detector.cpp - eighteen cases covering pitch-class naming, major/minor triads, dominant/major/minor sevenths, suspended chords, all three triadic inversions, and the unknown-pattern fallback. Authored by the new test-author agent.
  • build_tests.bat - convenience script that reconfigures with -DBUILD_TESTING=ON, builds all test targets, and runs ctest --output-on-failure. Useful both locally and from CI.
  • tests/data/ - local-only fixture directory (gitignored). Tests that need real-world files read the MIDIEDITOR_FIXTURES env var and QSKIP when the file is missing, so the suite stays green for contributors who do not have the proprietary tab files. Policy documented in tests/data/README.md.
  • Planning/06_TEST_CASES.md - living test roadmap (gitignored under Planning/). Module inventory with per-module status, prioritized backlog, and conventions. Maintained by the test-author agent on every test addition.

🟡 Medium Documentation

  • manual/supported-files.html (new) - comprehensive file-format reference with anchor sections #midi, #guitar-pro, #musicxml, #musescore, #workflow-ffxiv, #general-tips. Documents what each format supports, known limitations (e.g. MusicXML lyrics not yet imported, MuseScore tempo text without BPM hint not parsed), and which converter is invoked for which extension.
  • manual/guitar-pro.html - replaced with a minimal meta-refresh redirect stub pointing at supported-files.html#guitar-pro, preserving deep links from prior releases.
  • Sidebar / dropdown / sitemap link migration - every manual page's nav, footer, and the global navigation.js doc-subnav now point to the new Supported Files page instead of Guitar Pro Import. sitemap.xml URL updated. Landing-page feature card relabelled "Score & Tab Import" with the new copy.
  • Manual version display bumped to v1.4.0 - index.html (schema.org softwareVersion, hero badge, two download CTAs), download.html (current-release banner, download buttons, ZIP filename, GitHub release link), and docs-index.html (manual hero badge).
  • Docs landing chat-demo widget synced with marketing landing - docs-index.html's decorative MidiPilot chat widget now uses the same markup as index.html's newer version (token display, three-mode select, status bar, model tags) instead of the older single-mode layout.
  • CHANGELOG_TEMPLATE.md (new, top-level) - canonical template for future changelog entries. Documents the Summary + ` + Bug Fixes + Files Modified pattern with rules and reference entries ([1.3.2.2], [1.3.2], [1.2.1]`).

🟡 Medium Files Modified

  • CMakeLists.txt - version 1.3.2.21.4.0; opt-in BUILD_TESTING option + add_subdirectory(tests); new MusicXml/ source files added to the editor target
  • README.md - version bump; Features table now lists MusicXML and MuseScore alongside Guitar Pro
  • src/gui/MainWindow.cpp - file-open filter extended to include *.musicxml *.xml *.mxl *.mscz *.mscx; openFile() rewrite with format-aware error dialogs (UX-OPEN-001)
  • src/converter/MusicXml/MusicXmlImporter.{h,cpp} - new MusicXML / MXL parser (PHASE-24.1)
  • src/converter/MusicXml/MsczImporter.{h,cpp} - new MuseScore .mscz / .mscx parser (PHASE-24.2)
  • src/converter/MusicXml/XmlScoreToMidi.{h,cpp} - new shared SMF writer (PHASE-24.3)
  • src/converter/MusicXml/MusicXmlModels.h - shared score data model (parts, voices, events, metas)
  • tests/CMakeLists.txt - new test harness configuration with Qt 6 DLL-path workaround
  • tests/test_xml_score_to_midi.cpp - 6 unit tests for the shared SMF writer
  • tests/test_chord_detector.cpp - 18 unit tests for ChordDetector
  • tests/data/README.md, tests/data/.gitkeep - fixture directory policy + placeholder
  • build_tests.bat (new) - reconfigure + build + ctest convenience script
  • .gitignore - ignore tests/data/* real fixtures, keep README.md + .gitkeep
  • manual/supported-files.html (new) - unified import-format reference
  • manual/guitar-pro.html - replaced with meta-refresh redirect stub
  • manual/index.html - version bump (4 places); Score & Tab Import feature card
  • manual/download.html - version bump (4 places); Score & Tab features item; meta description
  • manual/docs-index.html - manual hero badge → v1.4.0; Supported Files section card; chat-demo widget synced with index.html
  • manual/editor-and-components.html - File → Open description updated for new formats
  • manual/navigation.js - doc-subnav entry: Guitar Pro → Supported Files
  • manual/sitemap.xml - guitar-pro.htmlsupported-files.html
  • 6 other manual pages - sidebar/footer link migration to Supported Files
  • CHANGELOG_TEMPLATE.md (new, top-level) - canonical entry template

v1.3.2.2

2026-04-17
Hotfix: Paste Selection + v1.3.1 Audit Fallout
  • Paste (Ctrl+V) now re-selects the pasted notes - another fallout of the v1.3.2 selection overhaul; pasted notes appeared but were not highlighted, making them hard to move (PASTE-001)
  • AI no longer misreads minor keys as their parallel major - v1.3.1's AI-008 scanned the wrong channel for KeySignatureEvents; every minor-key file was silently reported to the AI as the parallel major (AI-008-FIX)
  • Lyric Visualizer no longer freezes when a position update arrives without a preceding playerStarted - defensive timer restart in onPlaybackPositionChanged() (P3-008-RESIDUAL)
  • Clipboard deserialize UAF eliminated - added NoteOnEvent destructor that symmetrically removes itself from the static OffEvent::onEvents map; a corrupt clipboard payload no longer leaves a dangling pointer for the next file load to dereference (V131-P2-03)
  • Agent setup_channel_pattern tool no longer leaks events - the AI-tool entry point now wraps the FFXIV fixer in startNewAction/endAction so the CLEAN-phase removeEvent calls actually record into Protocol instead of silently discarding the undo items (V131-P2-04)
  • Lyric Visualizer first phrase after file change now fades in - setFile() was resetting _fadeIn to 1.0f (fully visible), skipping the fade-in for the seek+play case; now starts at 0.0f (V131-P2-06)
  • FFXIV Tier 3 now renames switching tracks by their first note's channel - v1.3.0's Bug #4 fix was too rigorous: it skipped renames entirely for tracks with notes on >1 guitar channel. But if the user moves a track's opening notes onto a different guitar channel (e.g. Clean track now starts with Overdriven), the rename should follow. Fixed to use the chronologically earliest NoteOn's channel for the rename decision, which works correctly for both single-channel and switching tracks (FFXIV-RENAME-001)
  • Second-pass v1.3.1 audit via bug-hunter subagent (Claude Opus 4.7) against the MidiEditor_meow upstream - full report in Planning/03_bugs.md → "v1.3.1 Audit - Second Pass"

🔧 Fixed Bug Fixes

  • Fixed Paste (Ctrl+V) not selecting the pasted notes (PASTE-001) - Another fallout from the v1.3.2 selection overhaul: EventTool::pasteAction() and pasteFromSharedClipboard() called selectEvent(event, false, true, /*setSelection=*/false) in a loop, then Selection::setSelection(Selection::selectedEvents()) at the end. This pattern worked before v1.3.2 because selectedEvents() returned by reference, so selectEvent(..., setSelection=false) accumulated the selection by mutating the internal list directly. After v1.3.2 made selectedEvents() return by value (SEL-001 proper fix), each selectEvent call built a throwaway local copy, and the final setSelection(selectedEvents()) just read the still-empty selection and set it back. Pasted notes were inserted correctly (visible in Protocol panel as "Paste N events") but were not selected/highlighted, making them hard to move - especially when pasted on top of the originals. Rewrote both paste paths to build a local pastedSelection list while inserting events and call Selection::setSelection() once with the complete list (same pattern as the v1.3.2 SelectTool box-selection fix). Respects channel visibility, track hidden flag, and skips OffEvents. Regression since v1.3.1.
  • Fixed AI context reporting every minor key as its parallel major (AI-008-FIX) - v1.3.1's AI-008 "optimization" moved captureKeySignature() from scanning all 19 channels to scanning channel 18 - but KeySignatureEvents actually live on channel 16 (the meta-event channel for KeySig, Text, Lyrics, Marker). Channel 18 holds TimeSignatureEvents. The dynamic_cast<KeySignatureEvent*> on a ch18 iterator never matched any event, so isMinor was always false. Every minor-key file was silently reported to the AI as its relative major (e.g. A minor as C major, E minor as G major), causing wrong scale/chord suggestions and drum fills that clashed with the actual tonality. Verified against MidiEvent::loadMidiEvent() (meta events default to ch16) and MidiFile::tonalityAt() (reads channels[16]). One-line fix: channel 1816. Silent regression since v1.3.1.
  • Fixed Lyric Visualizer staying animation-dead after idle timer stop (P3-008-RESIDUAL) - v1.3.1's P3-008 stops the Visualizer's 30 fps refresh timer when not playing and fully faded in. v1.3.2 patched the main playerStarted signal path on Windows where PlayerThread is recreated per play(), but onPlaybackPositionChanged() did not itself restart the timer. Any code path delivering a position update without a preceding playerStarted (AI tool play toggles, scrubbing, future signal-order changes) would leave the widget frozen until the next showEvent. Added a defensive if (!_timer.isActive()) _timer.start(REFRESH_MS) in the slot.
  • Fixed dangling NoteOnEvent* in static OffEvent::onEvents map (V131-P2-03) - NoteOnEvent::NoteOnEvent() registers this in the process-wide static OffEvent::onEvents map so the next matching OffEvent can find it. There was no symmetric removal in the destructor, so any delete event path that freed a NoteOnEvent before an OffEvent consumed it (corrupted SharedClipboard payload, truncated file load, cancelled parse) left a dangling pointer in the map. The next OffEvent constructor on the same line would iterate onEvents->values(line()), dereference the freed pointer via ->channel(), and either crash or produce a silent use-after-free. Added NoteOnEvent::~NoteOnEvent() that calls OffEvent::onEvents->remove(line(), this) (a no-op when this isn't in the map, safe for copies). Adjacent to v1.3.1's CLIP-001 dead-catch removal - not caused by it, but surfaced by the audit that cleared CLIP-001 as safe.
  • Fixed Agent setup_channel_pattern tool leaking events and discarding undo steps (V131-P2-04) - ToolDefinitions::execSetupChannelPattern() called FFXIVChannelFixer::fixChannels() directly without an active Protocol action. The fixer's CLEAN phase uses MidiChannel::removeEvent(ev, true), which routes through Protocol::enterUndoStep() - but that method silently deletes the undo item when _currentStep == nullptr. Every removed program change / CC / pitch bend became an unrecorded leak, and the operation was un-undoable. The interactive MainWindow path already wraps in startNewAction/endAction, but the AI-tool entry point did not. Wrapped execSetupChannelPattern in the same.
  • Fixed Lyric Visualizer skipping fade-in on seek+play after file change (V131-P2-06) - LyricVisualizerWidget::setFile() reset _fadeIn to 1.0f (fully visible), so when playback began inside an existing block (seek+play) the first phrase appeared without fading in. Changed to 0.0f.
  • Fixed FFXIV Tier 3 not renaming switching tracks based on their first note's channel (FFXIV-RENAME-001) - v1.3.0's Bug #4 fix added an if (chsWithNotes.size() != 1) continue; gate, skipping the rename for any track whose notes spanned multiple guitar channels. This broke the legitimate use case where a user manually moves a track's opening notes to a different variant (e.g. takes an "ElectricGuitarClean" track and moves the first phrase onto the Overdriven channel so the track *starts* with Overdriven) - the fixer was supposed to then rename it to "ElectricGuitarOverdriven". Replaced the "skip if multi-channel" gate with an explicit chronologically-earliest NoteOn lookup: iterate all guitar channels, take the first NoteOn per channel (already the earliest on that channel thanks to the sorted event map), then pick the channel whose first note has the smallest tick. That channel's variant determines the new name. Works correctly for single-channel tracks (identical to old behaviour) and for switching tracks (renames to whichever variant they start on). Updates Planning/05_FFXIV_Channel_Fix.md Bug #4 section to reflect the revised rule.

🟡 Medium v1.3.1 Audit - Second Pass

🟡 Medium Files Modified

  • src/tool/EventTool.cpp - PASTE-001 rewrite of both paste paths with local selection list
  • src/ai/EditorContext.cpp - AI-008-FIX channel 1816 in captureKeySignature()
  • src/gui/LyricVisualizerWidget.cpp - P3-008-RESIDUAL defensive timer restart + V131-P2-06 _fadeIn = 0.0f in setFile()
  • src/MidiEvent/NoteOnEvent.h/.cpp - V131-P2-03 new destructor removes this from static OffEvent::onEvents map
  • src/ai/ToolDefinitions.cpp - V131-P2-04 execSetupChannelPattern wrapped in startNewAction/endAction
  • src/ai/FFXIVChannelFixer.cpp - FFXIV-RENAME-001 Tier 3 rename now uses chronologically-earliest NoteOn's channel
  • Planning/03_bugs.md - second-pass audit report appended
  • Planning/05_FFXIV_Channel_Fix.md - Bug #4 section revised

v1.3.2.1

2026-04-16
Hotfix: Ctrl+Drag Deselection & CI Workflow
  • Fixed Ctrl+drag box deselection not working - The v1.3.2 selection system rewrite (SelectTool::release()) built a local event list but only had code to *add* events - there was no code path to *remove* already-selected events when Ctrl was held. Ctrl+drag box selection over selected notes did nothing instead of deselecting them. Added ctrlHeld check: when Ctrl is held, events inside the selection box are removed from the current selection instead of added. Applied to box selection, single selection, and left/right selection tools. Regression since v1.3.1.
  • Added GitHub Actions CI/Release workflow - New .github/workflows/ci.yml automates the full build-and-release pipeline. Triggers on push to main, v* tag push, or manual dispatch with optional release tag. Build job: checks out repo, installs Qt 6.5 (cached), sets up MSVC x64, downloads FluidSynth 2.5.2, configures and builds via CMake/NMake, deploys Qt with windeployqt, uploads build artifact (14-day retention for CI, 90-day for releases). Release job: downloads artifact, creates versioned ZIP, extracts release notes from CHANGELOG.md (falls back to git log), publishes GitHub Release with the ZIP attached. Includes build summary with duration, commit, and toolchain info.
  • Files modified: SelectTool.cpp, .github/workflows/ci.yml

v1.3.2

2026-04-15
MCP Server, Documentation System & Prompt Architecture v3
  • MCP Server - Built-in Model Context Protocol server exposing all MidiEditor AI tools (15 tools) with security, rate limiting, and client identification
  • MCP toolbar toggle - Clickable MCP Server button in the toolbar (green when running, gray when stopped); toggleable in Customize Toolbar; synced with the Settings checkbox
  • Centralized Navigation System - Single-source-of-truth for all doc pages (navigation.js); automatic active page highlighting and easy maintenance
  • Global Audio Player System - Reusable audio player component (audio.css) with browser controls and download links
  • Enhanced Manual - Lightbox on all images/videos, audio demos with players, improved MCP documentation, showcase integration
  • Prompt Architecture v3 - FFXIV simplification, token budgeting, conditional sections, retry logic, truncation recovery
  • Selection system overhaul - Fixed empty Event tab on note click, broken box selection, and broken deselect; reverted flawed SEL-001 early-return and O(n) comparison from v1.3.1/v1.3.1.2; selectedEvents() now returns by value to prevent internal list mutation
  • Fixed Lyric Visualizer not displaying during playback - v1.3.1 P3-008 timer idle stop exposed a latent bug: on Windows, PlayerThread is destroyed and recreated on each play(), breaking the playerStarted/playerStopped signal connections established at toolbar creation time; now reconnects all three signals in play() and record()
  • Fixed MCP toolbar button showing as text placeholder - _mcpServer was created after setupActions(), so the toolbar widget check (if _mcpServer) always failed during construction; moved server creation before toolbar build
  • Fixed Lyric Timeline not showing after LRC import - importLyricsLrc() was missing _lyricArea->setVisible(true) that both SRT and plain text import had; the timeline stayed hidden while the Visualizer displayed lyrics correctly

🟡 Medium MCP Server (Phase 23.5)

  • Built-in MCP server - Exposes all MidiEditor AI tools via the Model Context Protocol (2025-03-26), allowing Claude Desktop, VS Code Copilot, Cursor, and other MCP clients to edit MIDI files directly
  • Streamable HTTP transport - Single /mcp endpoint on localhost (default port 9420) with JSON-RPC 2.0
  • MCP Resources - Three read-only resources: midi://state, midi://tracks, midi://config
  • Security - Localhost-only binding, Origin header validation, optional Bearer token auth, rate limiting (100 calls/min), session management with 1-hour expiry
  • Settings UI - Enable/disable, port, auth token generation, and "Copy MCP Config" button for easy client setup
  • MCP toolbar toggle - New McpToggleWidget adds a clickable MCP Server button to the toolbar; shows mcp_on.png (green) when the server is running, mcp_off.png when stopped; click to start/stop without opening Settings; selectable and repositionable in Customize Toolbar dialog; checkbox in Settings stays synced bidirectionally
  • Client identification in Protocol panel - MCP actions show the client name and model (e.g. "MidiPilotMCP (VS Code Copilot Claude Opus 4.6): Agent insert events") - parsed from clientInfo sent during MCP initialize
  • MCP Documentation - Setup guide for Claude Desktop, VS Code, Cursor, and Windsurf in the built-in manual; removed unnecessary restart requirement (server starts immediately when toggled)
  • MCP Demo - Metal remix of Mozart's Eine kleine Nachtmusik (20 measures with guitar solo) composed via MCP protocol; includes MIDI file, WAV audio with embedded player, and screenshots in both manual and showcase

🟡 Medium Documentation System

  • Centralized Navigation (navigation.js) - Single file controls all 17 doc pages; auto-injects doc-subnav on every page with automatic active link highlighting; new pages inherit full nav by including 2 scripts
  • Global Audio Player System (audio.css) - Reusable .audio-box component with native browser player, download links, and dark theme; added to all 20 pages
  • Lightbox on All Pages - Images and videos click-to-enlarge; automatically excludes tiny icons and video fallbacks; no special markup required
  • Showcase Enhancement - Added MCP demo (webp) as slide 11 in the carousel; now 11 slides showing all major features

🟡 Medium Prompt Architecture v3 (Phase 23.x)

  • FFXIV prompt simplification - Streamlined FFXIV context for lower token usage
  • Enhanced tool result summaries - Improved tool output formatting for AI readability
  • Token budgeting - Smart history truncation when approaching context window limits
  • Conditional prompt sections - Only include drum/guitar sections when relevant to the file
  • GM program_change reminder - Strengthened create_track and insert_events tool descriptions
  • Retry logic - Automatic retry on 429/5xx errors with exponential backoff
  • Truncation auto-recovery - Detects and retries when API response is truncated
  • Effort-based prompt selection - Low/medium/high effort selects compact/standard/detailed prompts

🔧 Fixed Selection System Overhaul (3 compounding bugs)

  • Fixed empty Event tab when clicking a note - selectedEvents() now returns by value instead of by reference. Code that previously accumulated events through the reference (EventTool, SelectTool) now builds a local list and calls setSelection() once. The SEL-001 early-return optimization has been removed entirely.
  • Fixed box selection not selecting any events - SelectTool::release() called selectEvent(event, false, false, false) with setSelection=false, relying on reference mutation to accumulate events. With return-by-value, each call modified a throwaway copy. Rewritten to build a local QList<MidiEvent*> and call setSelection() once with the complete list.
  • Fixed deselect not updating Event tab - EventTool::deselectEvent() removed an event from the local copy but never called setSelection() to propagate the change. Added setSelection(selected) after removal.
  • Fixed Protocol::endAction() not emitting signals - endAction() only emitted actionFinished() and protocolChanged() when protocol items were recorded. Since selection changes don't record protocol items, the signals never fired and EventWidget::reload() was never triggered. Now always emits signals and marks file unsaved.
  • Reverted v1.3.1.2 O(n) comparison - The <200 event threshold comparison in setSelection() is no longer needed since the early-return was removed entirely.
  • Files modified: Selection.h, Selection.cpp, EventTool.cpp, SelectTool.cpp, EditorContext.cpp, MainWindow.cpp, Protocol.cpp

🔧 Fixed Bug Fixes

  • Fixed Lyric Visualizer not displaying during playback - The v1.3.1 P3-008 fix (timer stops when idle) exposed a latent bug: on Windows, MidiPlayer::play() deletes and recreates the PlayerThread object, breaking the playerStarted/playerStopped signal connections established at toolbar creation time. The timer only restarts via playbackStarted(), which never fired because it was connected to the destroyed object. Before P3-008 the timer ran at 30fps forever, masking the broken connection. Fixed by reconnecting all three visualizer signals (playerStarted, playerStopped, timeMsChanged) in both play() and record()
  • Fixed MCP toolbar button showing as text placeholder - _mcpServer was created after setupActions() built the toolbar, so the check if (actionId == "mcp_toggle" && _mcpServer) always failed during construction, falling through to the placeholder text path. Moved _mcpServer creation to before setupActions() and removed the duplicate creation at the later location
  • Fixed Lyric Timeline not showing after LRC import - importLyricsLrc() did not call _lyricArea->setVisible(true) or update _toggleLyricTimeline, so the timeline lane stayed hidden after importing an LRC file. Both importLyricsSrt() and importLyricsText() already had this. The Lyric Visualizer in the toolbar displayed lyrics correctly because it doesn't depend on _lyricArea visibility

✨ Added Files Added/Modified

  • New: src/gui/McpToggleWidget.h/.cpp - MCP Server toolbar toggle button widget
  • New: navigation.js (2.6 KB) - Centralized navigation controller
  • New: audio.css (620 bytes) - Global audio player styles
  • Modified: All 17 doc pages - Added centralized navigation and audio support
  • Modified: mcp-server.html - Added MCP demo with audio player, updated Quick Start
  • Modified: index.html - Added MCP demo slide to showcase
  • Added: midieditor_ai_MCP.mid, midieditor_ai_MCP.wav (manual demos)
  • Added: Screenshots and webp in manual/screenshots/

v1.3.1.2

2026-04-12
Hotfix: Selection Regression (superseded by v1.3.2)
Note:** Both fixes below were reverted in v1.3.2 and replaced by a proper architectural fix (selectedEvents() returns by value, SEL-001 early-return removed entirely). See v1.3.2 "Selection System Overhaul" for details.
  • Fixed Ctrl+A / large selection freezing the UI - batchSelectEvents() obtained a reference to the internal _selectedEvents list and modified it in-place (clear + append), so setSelection()'s equality check (SEL-001) always saw them as identical and returned early - no protocol entry, no EventWidget update, no Ctrl+T. Fixed by building a new local list instead of mutating through the reference.
  • Fixed O(n) selection comparison blocking large files - setSelection() did an element-by-element QList::operator== on every call, taking seconds for 4000+ events. Now only performs the full comparison for selections under 200 events; larger selections skip the check entirely.

v1.3.1.1

2026-04-12
Hotfix: Post-Update Changelog
  • Fixed "Could not load patch notes" in post-update and update-available dialogs - fetchChangelog() shared the same QNetworkAccessManager whose finished signal was already connected to onResult(), consuming the response before the changelog lambda could read it. Additionally, the GitHub Pages URL redirected to the custom domain, but Qt6 does not follow redirects by default. Fixed by using a separate QNetworkAccessManager, correcting the URL to midieditor-ai.de, and adding NoLessSafeRedirectPolicy as a safety net.

v1.3.1

2026-04-12
Bugfix Release
  • 16 bug fixes across AI subsystem, lyric editor, clipboard, selection, FluidSynth export, and FFXIV Channel Fixer - memory leaks, use-after-free, undo spam, overlap prevention, and performance improvements
  • Fix FFXIV Channel Fixer undo crash - Reverting Fix XIV Channels (Tier 2 or Tier 3) caused a crash due to use-after-free; removed incorrect event deletion that conflicted with the Protocol undo system
  • Documentation fixes - Fixed lyrics manual page layout, removed dead CLI argument documentation
  • Website UX tweaks - Showcase carousel videos, 16:9 aspect ratio, darker caption bar, What's New section updates

🔧 Fixed Bug Fixes (Pass 4 - AI Subsystem & Core)

  • AI cancel safety - Disconnect network reply signals before abort() in streaming cancel, preventing potential use-after-free (AI-002)
  • Agent cancel after processEvents - Added _cancelled check immediately after QCoreApplication::processEvents() in the tool-call loop (AI-001)
  • Conversation store O(1) lookup - loadConversation() now uses direct path lookup instead of scanning all JSON files (AI-009)
  • FFXIV Channel Fixer memory leaks - Removed events (ProgChange, CC, PitchBend) are now properly deleted in the CLEAN phase (AI-010, AI-011)
  • Selection undo spam - setSelection() returns early when selection is unchanged, avoiding redundant Protocol entries (SEL-001) - reverted in v1.3.2 (caused empty Event tab; see Selection System Overhaul)
  • SharedClipboard dead catch - Removed try/catch(...) around delete (destructors are noexcept in C++11+) (CLIP-001)
  • Export temp file cleanup - ExportOptions::deleteMidiFileAfterExport flag is now honored; temp MIDI files are deleted after export (FLUID-002)
  • Key signature scan - captureKeySignature() now only scans channel 18 (meta) instead of all 19 channels (AI-008)
  • API log truncation - Non-streaming request/test logs now truncate body to 4000 chars, matching the streaming pattern (AI-005)

🔧 Fixed Bug Fixes (Remaining Lyric & AI Bugs)

  • Insert overlap prevention - Double-click, Insert Before, and Insert After now clamp new blocks to avoid overlapping adjacent blocks (P2-007)
  • Split encapsulation - Split operation now uses addBlockDirect() instead of calling insertSorted() + emitting lyricsChanged() from outside LyricManager (P3-007)
  • Visualizer timer idle stop - LyricVisualizerWidget timer now stops when not playing and fully faded in, eliminating unnecessary 30fps CPU usage (P3-008)
  • Multi-timestamp LRC import - LRC import now handles karaoke-style multi-timestamp lines like [00:12.34][01:56.78]Text, creating one block per timestamp (P3-011)
  • Truncated response logging - Truncated API responses now log partial content (up to 500 chars) for debugging before emitting the error (AI-004)
  • FFXIV polyphony sort - execValidateFFXIV now sorts notes by tick before the polyphony check, ensuring the first reported overlap is chronologically first; inner loop early-breaks for better performance (AI-006)

🔧 Fixed Documentation Fixes

  • Fixed lyrics manual page - "Lyric Metadata" section was a mismatch of Customize Toolbar and metadata content. Split into two proper sections: "Customize Toolbar" (toggle Lyric Visualizer on/off) and "Lyric Settings & Metadata" (LRC metadata fields with lyric_settings.png screenshot)
  • Removed dead documentation - "Larger Playback Toolbar" section in setup.html referenced a --large-playback-toolbar CLI argument that doesn't exist in the codebase. Removed section and screenshot reference

🟡 Medium Website UX Tweaks

  • Showcase carousel videos - Replaced static screenshots with webm videos for MidiPilot, Lyric Visualizer, and other features
  • Showcase aspect ratio - All slides now use a fixed 16:9 aspect ratio with object-fit: cover cropping for a consistent, clean layout; click any slide for the full uncropped view in the lightbox
  • Showcase caption bar - Darker, taller gradient overlay for better text readability
  • What's New section - Fixed year (2025 to 2026), updated version to v1.3.1, added bug fix count

v1.3.0

2026-04-11
Lyric Editor & Visualizer
  • Lyric Timeline - New dedicated lane below the piano roll for visual lyric editing (add, delete, move, resize, split, merge, inline text editing)
  • Sync Lyrics dialog - Tap-to-sync: hold Space while each phrase is sung to capture precise timing in real time
  • Import/Export - Import from plain text, SRT subtitles, or MIDI Text/Lyric events; export to SRT or LRC (karaoke) format
  • Lyric Visualizer - Karaoke-style toolbar widget with left-to-right color sweep, two-line display (current + next phrase), dynamic sizing (200-600px)
  • Full undo/redo - All lyric operations integrate with the Protocol system for complete undo/redo support
  • FluidSynth fixes - synth.reverb.engine compatibility guard for FluidSynth 2.5.2; bundled SoundFonts survive clean builds
  • FFXIV Channel Fixer - 5 Tier 3 bugs fixed - wrong programs, broken renames, unwanted event migration; progAtTick(0) now single source of truth for channel programs
  • Website & documentation - New Lyrics manual page, version bump to 1.3.0 across all pages, GIF→webm migration for bandwidth savings, video lightbox support

🟡 Medium Lyric Timeline Widget

  • New Lyric Timeline lane - A dedicated lane below the piano roll displays lyric blocks
  • Full lyric editing - Add, delete, move, resize, split, and merge lyric blocks directly
  • Sync Lyrics dialog - Tap-to-sync mode: play the song and hold Space while each phrase
  • Import lyrics - Import from plain text (one phrase per line), SRT subtitle files
  • Export lyrics - Export to SRT subtitle format or LRC (karaoke) format. LRC export
  • Undo/Redo support - All lyric operations (add, delete, move, resize, edit, split,
  • Context menu - Right-click any block for Edit Text, Delete, Split at Cursor, Merge

🟡 Medium Lyric Visualizer (Karaoke Display)

  • Karaoke toolbar widget - A compact toolbar widget that shows the current lyric phrase
  • Dynamic sizing - The visualizer box dynamically expands (200-600px) based on the
  • Always visible - Stays visible at all times (matching the MIDI Visualizer pattern).
  • Full toolbar integration - Appears in the Customize Toolbar dialog with its own icon,
  • Toolbar integration fixes - Added lyric_visualizer to all three "single source of

🔧 Fixed FluidSynth Fixes

  • synth.reverb.engine compatibility - FluidSynth 2.5.2 doesn't support the
  • Bundled SoundFonts survive clean builds - Added CMake post-build rule to copy

📝 Notes Technical Notes

  • New files: src/gui/LyricVisualizerWidget.h/.cpp, run_environment/graphics/tool/lyric_visualizer.png
  • Modified files: MainWindow.h/.cpp (action registration, 4 toolbar creation sites, 2 migration blocks, signal connections), LayoutSettingsWidget.cpp (ToolbarActionInfo entry, 3 single-source-of-truth methods, default order/distribution), resources.qrc (icon), FluidSynthEngine.cpp (reverb engine guard), CMakeLists.txt (SoundFont copy rule)
  • Lyric Editor files (from Phase 21): src/midi/LyricManager.h/.cpp, src/midi/LyricBlock.h, src/gui/LyricTimelineWidget.h/.cpp, src/gui/LyricEditorDialog.h/.cpp, src/gui/SyncLyricsDialog.h/.cpp

🟡 Medium Website & Documentation

  • New Lyrics manual page - Full manual/lyrics.html with 10+ sections: Timeline overview,
  • Version bump to 1.3.0 - Updated CMakeLists.txt, README.md, index.html (4 locations),
  • README.md updated - 3 new feature rows, 2 architecture entries, full Lyric Editor section,
  • Navigation updated on all pages - Doc-subnav (🎤 Lyrics link) added to all 16 doc pages;
  • GIF→webm migration - All 11 standalone GIF <img> tags across 8 HTML pages replaced with
  • Video lightbox - Extended lightbox.js to support click-to-enlarge on <video> elements.
  • Lightbox added to lyrics page - Added missing lightbox.js script include; wrapped
  • Fixed mojibake in FFXIV page - Corrected â€" (corrupted UTF-8 em dash) to proper -
  • Lightbox click-to-close on images - Fixed enlarged images not closing when clicked (only

🔧 Fixed FFXIV Channel Fixer - Tier 3 Overhaul (5 bugs)

  • Bug #1: Reserved guitar channels (from Tier 2) were invisible to rename/switch logic - allGuitarChs now includes guitarChToProgram channels
  • Bug #2: Track name was used instead of actual channel program for PC insertion and chToVariant - now uses guitarChToProgram as primary source
  • Bug #3: Duplicate guitar track names caused event migration between channels - removed all duplicate logic from Tier 3 (every track keeps its own channel)
  • Bug #4: Rename on switching tracks (notes on multiple channels) was unreliable - now only renames single-channel guitar tracks
  • Bug #5: guitarChToProgram scan took the first PC at tick 0 instead of the effective one - now uses channel->progAtTick(0) (matches channel view display)
  • Files modified: src/ai/FFXIVChannelFixer.cpp

🔧 Fixed Lyric System Bug Fixes (Pass 3 Audit - 7 fixes)

  • P3-001 (Critical) - lyricsChanged signal unconditionally cleared the timeline selection
  • P3-002 (High) - importLyricsLrc() called addBlock() in a loop, each creating its own
  • P3-003 (Medium) - Consecutive MIDI Text events at the same tick produced zero-duration
  • P3-004 (Medium) - LyricTimelineWidget::paintEvent() directly overwrote the _file
  • P3-005 (Medium) - LyricSyncDialog::onDone() did not re-sort blocks after applying
  • P3-006 (Medium) - LyricTimelineWidget::setFile() never disconnected the old file's
  • P3-009 (Low) - LyricVisualizerWidget::onLyricsChanged() did not reset

v1.2.2

2026-04-11
FFXIV Channel Fixer Bugfix & Website Fixes
  • Fix XIV Channels - Tier 3 (Preserve) now sees reserved guitar channels
  • Fix lightbox on manual pages - Images wrapped in <a class="lightbox-link"> on
  • Updated FFXIV Channel Fixer manual page - Clearer Tier 2 / Tier 3 descriptions,

v1.2.1

2026-04-09
Stability & Bugfix Release
  • 72 bug fixes across the entire codebase - memory leaks, crashes, undefined behavior, protocol corruption, concurrency issues, parser security hardening, and correctness fixes
  • Guitar Pro 5 parser fixes - 4 byte-alignment bugs fixed; GP5 files that previously failed to open now load correctly
  • MidiPilot timestamp readability - Chat timestamps moved from inside the bubble to a clean label above each message; uses light gray text for dark themes, muted gray for light themes - readable across all 5 themes

🔴 Critical Critical: Crash & Hang Prevention (9 fixes)

  • EVT-001 - loadMidiEvent() infinite recursion on malformed MIDI data. Added recursion guard: bail when running status byte is 0 instead of recursing forever
  • EVT-002 - SysEx loader infinite loop on truncated stream (missing 0xF7 terminator). Added content->atEnd() check in the read loop
  • EVT-003 - TempoChangeEvent constructor division by zero on zero tempo value. Guards value <= 0 with 120 BPM default
  • EVT-004 - TempoChangeEvent::save() division by zero when _beats is 0. Guards _beats > 0 before division
  • PROTO-005 - Protocol::goTo() use-after-free when navigating to a redo step. Replaced dangling-pointer contains() loop with index-based count
  • CONV-001 - readIntByteSizeString() negative size from file → SIZE_MAX string read. Returns empty string when d < 0
  • CONV-002 - readIntSizeString() negative length from file → SIZE_MAX string read. Returns empty string when length < 0
  • CONV-005 - GP6 BitStream powers[] array out-of-bounds read for wordSize > 10. Replaced lookup table with direct (1 << i) bit shift
  • CONV-010 - Stack buffer overflow: lastNoteIdx[10] OOB write from untrusted GP6/7 string number. Added MAX_STRINGS constant with std::min clamping

🟠 Memory Protocol System Memory Leaks (5 fixes)

  • PROTO-001 - Protocol had no destructor - both undo/redo stacks and all contained steps leaked on file close. Added ~Protocol() with qDeleteAll cleanup
  • PROTO-002 - startNewAction() cleared redo stack without deleting ProtocolStep objects. Added qDeleteAll before clear()
  • PROTO-006 - enterUndoStep() leaked ProtocolItem when no action step was open. Added delete item fallback
  • PROTO-007 - ProtocolStep destructor didn't delete contained ProtocolItem objects. Added qDeleteAll(*_itemStack)
  • PROTO-008 - releaseStep() leaked old items after calling release(). Added delete item after extracting reverse action

🔧 Fixed MIDI Engine (8 fixes)

  • MIDI-001 - msOfTick() leaked a heap-allocated QList on every call (the most-called timing function). Changed to stack allocation - eliminates the single largest memory leak in the codebase
  • MIDI-002 - save() leaked QFile and QDataStream on every save. Changed to stack allocation
  • MIDI-003 - timeSignatureEvents()/tempoEvents() used reinterpret_cast from QMultiMap* to QMap* - undefined behavior in Qt 6 where these are different types. Changed return types to QMultiMap*, updated 12 callers
  • MIDI-004 - MidiChannel::number() checked this == nullptr (UB in C++) and used useless try/catch. Removed dead guards
  • MIDI-005 - PlayerThread::stopped used volatile bool instead of std::atomic<bool> - data race between main and player threads
  • MIDI-006 - playedNotes QMap accessed from multiple threads without synchronization. Added QMutex protection around all accesses
  • MIDI-007 - setMaxLengthMs() leaked QList from eventsBetween(). Added delete ev
  • MIDI-008 - reloadState() leaked old _tracks list on every undo/redo. Added delete _tracks before reassignment

🔧 Fixed MidiEvent Layer (5 fixes)

  • EVT-005 - OnEvent::moveToChannel() null dereference when OffEvent is missing. Added null check
  • EVT-006 - TimeSignatureEvent::ticksPerMeasure() used powf for integer power-of-2 (float precision loss) and could divide by zero. Replaced with 1 << denominator and added null/zero guards
  • EVT-007 - KeySignatureEvent::save() OR'd meta type byte 0x59 with channel - latent corruption if channel != 16. Changed to hardcoded char(0x59)
  • EVT-008 - ProtocolEntry::protocol() leaked the copy() allocation when file() is null (during file loading). Added delete oldObj fallback - fixes memory leak of 2 objects per TextEvent loaded
  • EVT-009 - TempoChangeEvent::msPerTick() null dereference when file() is null. Added null/zero guards with 1.0ms fallback

🔧 Fixed Tools & Selection (8 fixes)

  • TOOL-001 - SelectTool::draw() used the macro literal SELECTION_TYPE_BOX (always 2, always true) instead of stool_type == SELECTION_TYPE_BOX. All selection types incorrectly drew box rectangles
  • TOOL-005 - EventTool::copiedEvents leaked old clipboard events on each copy operation. Added qDeleteAll before clear()
  • CONV-003 - GpBinaryReader::checkBounds() integer overflow: count * N could wrap negative for large counts. Added count < 0 / pointer_ < 0 guards with size_t cast
  • CONV-006 - GP7/8 ZIP extract() didn't validate data offset+size against buffer. Added bounds check
  • CONV-007 - GP7/8 ZIP inflateData() trusted claimed uncompressed size for allocation (DoS). Added 256MB cap
  • CONV-008 - readDuration() left-shift overflow from out-of-range signed byte. Clamped to std::clamp(byte + 2, 0, 7)
  • CONV-011 - SimileMark::simple on first measure accessed measures[-1]. Added measureIndex > 0 guard
  • CONV-013 - GpMidiExport::createBytes() accessed raw[0] on empty vector for unrecognized message types. Added empty check

🔧 Fixed Medium-Severity Fixes (29 fixes)

Protocol & Undo
  • PROTO-003/004 - endAction() marked file unsaved and emitted signals even with no active action step. Now only triggers when items were actually recorded
  • PROTO-009 - ProtocolItem::release() deletion guard checked the wrong variable (entry instead of _oldObject)
MIDI Engine Correctness
  • MIDI-011 - PlayerThread had no destructor - QTimer and QElapsedTimer leaked on every play/stop cycle (Windows). Added ~PlayerThread()
  • MIDI-012 - MIDI input receiveMessage callback wrote shared state from RtMidi thread without synchronization. Added QMutex protection
  • MIDI-013 - endInput() invalidated QMultiMap iterator during container modification. Changed to erase() which returns next valid iterator
  • MIDI-014 - progAtTick() off-by-one skipped the first element in backward search. Fixed loop to check begin() element
  • MIDI-015 - Metronome sent triple NoteOn with single NoteOff, creating stuck notes that accumulated. Reduced to single NoteOn
  • MIDI-016 - MidiFile(int, Protocol*) protocol-copy constructor left channels[], _tracks, etc. uninitialized. Added safe defaults
  • MIDI-018 - MidiInput::inputPorts() crashed if init() failed (null _midiIn). Added null check
MidiEvent Correctness
  • EVT-010 - PitchBendEvent::toMessage() sent "cc" instead of "pitchbend" command - wrong MIDI messages during playback
  • EVT-011 - _tempID uninitialized in MidiEvent constructors. Initialized to -1
  • EVT-012 - Tempo loading used fragile magic-number subtraction (value -= 50331648). Replaced with value &= 0x00FFFFFF bitmask
  • EVT-014 - NoteOnEvent::setNote() had no range validation. Added qBound(0, n, 127)
Tool System
  • TOOL-002 - NewNoteTool() constructor reset static _channel/_track to 0, losing user's selection when StandardTool was created
  • TOOL-003 - Tool::setImage() leaked previous QImage on reassignment. Added delete _image before new
  • TOOL-004 - Tool copy constructor shallow-copied QImage pointer (shared ownership → double-free risk). Changed to deep copy
  • TOOL-006 - EventTool copy/paste null dereference if OnEvent has no OffEvent. Added null check
  • TOOL-007 - EventMoveTool::computeRaster() null dereference on missing OffEvent. Added null check
  • TOOL-012 - TempoTool::release() leaked TempoDialog after exec(). Stack-allocated instead
  • TOOL-013 - TimeSignatureTool::release() leaked TimeSignatureDialog after exec(). Stack-allocated instead
Converter & Parser
  • CONV-004 - GpBinaryReader::skip() didn't validate pointer stayed in bounds. Added negative count guard and checkBounds()
  • CONV-009 - Tuplet enters=0 from GP6/7 XML caused float division by zero. Added enters > 0 guard
  • CONV-012 - SimileMark::secondOfDouble with notes underflow accessed negative indices. Added measureIndex >= 2 bounds check
  • CONV-014 - GP6/7 outer parser leaked when transferring to inner GP5 object. Fixed self pointer transfer to null before reset()
  • CONV-017 - GP1/2 gpReadStringBSoB with byte=0 → negative size → same crash as CONV-001. Added size < 0 guard
Terminal & System
  • TERM-001 - Terminal::execute() leaked old QProcess on re-execute. Added deleteLater() before creating new process
  • TERM-002 - Terminal::processStarted() leaked QTimer on each retry (one per second). Connected timeout to deleteLater()
  • MAIN-001 - wstrtostr() buffer overflow with multi-byte characters (CJK usernames). Rewrote with two-pass WideCharToMultiByte

🟢 Low Low-Severity Fixes (8 fixes)

  • MIDI-019 - MidiChannel::visible() was hardcoded to return true (dead code). Now delegates to ChannelVisibilityManager
  • MIDI-020 - MidiChannel::setVisible() used try/catch for control flow. Replaced with _num range guard
  • MIDI-021 - MidiTrack::reloadState() called setNumber() which re-entered the protocol system during undo. Changed to direct member assignment
  • EVT-017 - OffEvent::onEvents static map was heap-allocated and never freed. Changed to static local storage
  • TOOL-009 - GlueTool::mergeNoteGroup() null dereference on lastNote->offEvent(). Added null guard
  • TOOL-010 - ToolButton::refreshIcon() dereference of potentially null tool image. Added null guard
  • TOOL-011 - StandardTool copy constructor didn't copy newNoteTool (uninitialized pointer). Added missing copy
  • MAIN-002 - MainWindow heap-allocated in main() but never deleted. Added delete w before return

🔧 Fixed Guitar Pro 5 Parser Fixes (4 fixes)

  • GP5 readGrace() override - GP5 grace notes have 5 bytes (fret, dynamic, transition, duration, flags) but MidiEditor inherited GP3/4's 4-byte version. Added Gp5Parser::readGrace() override reading the missing dead/onBeat flags byte. This was the primary cause of cumulative byte misalignment
  • GP5 readMixTableChange() skip(1) - Added missing reader.skip(1) after allTracksFlags byte, matching TuxGuitar's reference implementation
  • GP5 readMixTableChange() v5.1+ strings - Changed conditional RSE string reads to always read 2 strings for GP5.1+ files, matching TuxGuitar
  • GP5 readMeasureHeader() field order - Moved repeatAlternative (flag 0x10) from before marker/keySignature to after beams section, matching TuxGuitar's byte ordering

🟢 Low Deferred Bugs (8 - low risk, high complexity)

  • EVT-013 - On/OffEvent copy constructor dangling pointers (deep protocol architecture issue)
  • EVT-015 - SysEx VLQ length prefix omission (load/save internally consistent; changing one breaks the other)
  • EVT-016 - QDataStream status checks throughout loadMidiEvent (too invasive for low crash risk)
  • EVT-018 - Dead < 0 checks on quint8-sourced values (harmless defensive code)
  • CONV-015 - Unchecked std::stoi() on ~30+ GP6/7 XML sites (needs helper + mass update)
  • CONV-016 - Custom GP6 XML parser edge cases (design-level issue)
  • MIDI-017 - RtMidi static objects never deleted (OS reclaims at exit)
  • TOOL-008 - ScissorsTool protocol recording (already works - setMidiTime() defaults to toProtocol=true)

📝 Notes Technical Notes

  • Bug audit: 137 bugs reported by automated analysis, 130 confirmed (7 false positives), 50 already fixed in v1.1.9/v1.2.0, 80 remaining → 72 fixed + 8 deferred
  • Files modified (40+): MidiEvent.cpp, TempoChangeEvent.cpp, TimeSignatureEvent.cpp, KeySignatureEvent.cpp, OnEvent.cpp, NoteOnEvent.cpp, PitchBendEvent.cpp, OffEvent.cpp, ProtocolEntry.cpp, Protocol.h/.cpp, ProtocolStep.cpp, ProtocolItem.cpp, MidiFile.h/.cpp, MidiChannel.cpp, MidiTrack.cpp, MidiInput.h/.cpp, MidiOutput.h/.cpp, PlayerThread.h/.cpp, Metronome.cpp, MidiPlayer.cpp, GpBinaryReader.cpp, Gp345Parser.h/.cpp, Gp678Parser.cpp, GpToNative.cpp, GpMidiExport.cpp, GpUnzip.cpp, GpImporter.cpp, GpModels.h, Gp12Parser.cpp, SelectTool.cpp, EventTool.cpp, EventMoveTool.cpp, NewNoteTool.cpp, Tool.cpp, GlueTool.cpp, ToolButton.cpp, StandardTool.cpp, TempoTool.cpp, TimeSignatureTool.cpp, Terminal.cpp, main.cpp
  • GP5 parser reference: TuxGuitar's GP5InputStream.java used to identify byte-alignment differences
  • Bug report: Planning/03_bugs.md - full scan results with verification status

v1.2.0

2026-04-08
Audio Export, MP3, FluidSynth Hardening
  • Audio Export - Export MIDI as WAV, FLAC, or OGG Vorbis using loaded SoundFonts (File → Export Audio, Ctrl+Shift+E)
  • Export Dialog with format selection (WAV/FLAC/OGG/MP3), quality presets (Draft/CD/Studio/Hi-Res), range options (full song/selection/custom measures), reverb tail toggle, and estimated file size
  • Selection export via right-click context menu - select notes, right-click → "Export Selection as Audio..."
  • Export Audio button added to SoundFont settings panel
  • Background export with progress dialog and cancel support
  • MP3 Export - Built-in LAME 3.100 encoder compiled as static C library; export directly to MP3 without external tools
  • Export Completion Dialog - After export finishes, shows dialog with Open File, Open Folder, and Close buttons
  • Guitar Pro Audio Export Fix - Exporting audio from Guitar Pro files (.gp3-.gp8) no longer produces silent/empty output; saves in-memory MIDI to temp file for rendering
  • SoundFont Enable/Disable - Per-SoundFont checkboxes in the settings list; uncheck to temporarily disable a font without removing it; state persists across sessions
  • FFXIV SoundFont Mode Auto-Toggle - SoundFonts with "ff14" or "ffxiv" in the filename automatically enable/disable FFXIV SoundFont Mode when checked/unchecked
  • FluidSynth Audio Driver Fallback - If the preferred audio driver fails (e.g., SDL3 after restart), automatically tries wasapi → dsound → waveout → sdl3 → sdl2
  • FluidSynth Settings Always Accessible - SoundFont list and settings are now configurable even when Microsoft GS Wavetable Synth is the active output
  • FluidSynth Error Dialog - Shows a clear error message when FluidSynth initialization fails, with automatic revert to previous output
  • Drum Channel Reset Fix - Switching from FFXIV SoundFont back to GM mode now properly restores channel 9 drums (bank select 128 + program change); previously drums played as piano
  • Radio button styling fix - checked radio buttons now render as proper circles in all 5 themes (dark, light, amoled, materialdark, pink)

✨ Added Added

  • MP3 export via LAME - LAME 3.100 compiled from source as a static C library (libmp3lame.a) and linked directly into the build. No DLL or external encoder needed. Export dialog shows MP3 as a format option alongside WAV/FLAC/OGG. Supports VBR quality presets matching the existing quality tiers.
  • Export completion dialog - QMessageBox with three buttons after any audio export: Open File (launches default audio player), Open Folder (opens containing directory in Explorer), Close (dismiss). Previously there was no feedback after export completion.
  • SoundFont enable/disable checkboxes - Each SoundFont in the FluidSynth settings list has a checkbox. Unchecking removes it from the active FluidSynth stack without deleting it from the list. Re-checking reloads it. Disabled state persisted in QSettings across sessions. Dual-state system: runtime (_soundFontStack + _disabledSoundFontPaths) and pending (_pendingSoundFontPaths + _pendingDisabledPaths) for before/after FluidSynth initialization.
  • FFXIV SoundFont Mode auto-toggle - updateFfxivModeFromSoundFonts() scans checked SoundFonts for "ff14" or "ffxiv" in the filename (case-insensitive). Auto-enables FFXIV SoundFont Mode when a matching font is checked; auto-disables when all matching fonts are unchecked. Reads from UI list widget directly to ensure correctness before engine commits.
  • Audio driver fallback chain - FluidSynthEngine::initialize() tries multiple audio drivers in sequence: user preference → wasapi → dsound → waveout → sdl3 → sdl2. Common with SDL3 failures after a shutdown/restart cycle. Driver combo in settings reflects the actual driver used.
  • FluidSynth settings always enabled - Removed setEnabled(false) on the FluidSynth settings group when non-FluidSynth output is selected. Users can manage SoundFonts at any time; settings applied when FluidSynth is next activated.
  • Pre-init SoundFont management - addPendingSoundFontPaths() method allows adding SoundFonts before FluidSynth is initialized. setSoundFontStack() and removeSoundFontByPath() also update pending paths when engine is not initialized.
  • FluidSynth error feedback - QMessageBox::warning shown when switching to FluidSynth output fails, with error details. Output automatically reverts to previous working port.
  • Output port fallback - MidiOutput::setOutputPort() saves previous port; if new port's FluidSynth init fails, previous port restored automatically.

🔧 Fixed Fixed

  • Guitar Pro audio export producing silence - file->path() returned the .gp5/.gpx path which FluidSynth cannot parse. Now saves the in-memory MidiFile to a temporary .mid file, exports from that, then cleans up via _exportTempMidiPath.
  • Drum channel playing piano after FFXIV→GM switch - Two issues: (1) updateFfxivModeFromSoundFonts() was called AFTER setSoundFontEnabled(), so applyChannelMode() still used old FFXIV flag during stack rebuild. Reordered to update mode FIRST. (2) applyChannelMode() GM restore didn't reset channel 9 properly - now sends bank_select(ch9, 128) + program_change(ch9, 0) to restore drum kit, plus program_change(0) on all melodic channels.
  • SoundFont state lost on shutdown - shutdown() only preserved _loadedFonts (enabled fonts), losing disabled font paths. Now preserves full stack via allSoundFontPaths()_pendingSoundFontPaths + _pendingDisabledPaths.
  • SoundFont list empty before init - allSoundFontPaths() returned empty when _soundFontStack was empty (before initialization). Now falls back to _pendingSoundFontPaths.
  • Disabled SoundFonts lost on settings change (V12-002) - Changing audio driver, sample rate, or reverb engine silently discarded disabled SoundFonts from the stack. Removed redundant setSoundFontStack() calls; shutdown()+initialize() already preserves and restores the full stack.
  • MP3 export reported success after encoding error (V12-003) - LAME encoder returned true even when lame_encode_buffer_interleaved() reported an error. Now tracks error state, skips flush, deletes corrupt output, and returns false.
  • Cancel button ineffective during MP3 encoding (V12-004) - Cancel only worked during WAV rendering phase. Now passes the cancel flag to LameEncoder::encode(), which checks it each iteration and aborts cleanly.
  • Export failure showed file path instead of error (V12-005) - Error dialog displayed the output file path rather than an actionable message. Now shows descriptive success/failure messages with guidance.
  • Misleading comment in driver fallback loop (V12-007) - Removed incorrect comment about re-creating settings+synth per driver attempt.
  • Radio button styling - checked radio buttons render as proper circles in all 5 themes.

🔄 Changed Changed

  • Export dialog now shows MP3 as a fourth format option
  • Phase 20 title updated to "Audio Export & FluidSynth Hardening"
  • Version bump to 1.2.0

📝 Notes Technical Notes

  • LAME integration: lame/ directory contains LAME 3.100 source; compiled as static C library via add_library(mp3lame STATIC ...) in CMakeLists.txt. LameEncoder.h/.cpp wraps the LAME API for Qt integration.
  • FluidSynth driver fallback: Preferred driver stored in QSettings("FluidSynth/audio_driver"). Fallback order: wasapi (Windows native) → dsound (DirectSound) → waveout (legacy) → sdl3 → sdl2. Each attempt calls fluid_settings_setstr + new_fluid_audio_driver; on failure, deletes settings/synth and tries next.
  • SoundFont state management: Four containers: _soundFontStack (active ordered list), _disabledSoundFontPaths (runtime disabled QSet), _pendingSoundFontPaths (pre-init ordered list), _pendingDisabledPaths (pre-init disabled QSet). shutdown() merges runtime → pending. initialize() consumes pending → runtime.
  • Files created: src/midi/LameEncoder.h/.cpp
  • Files modified: FluidSynthEngine.h/.cpp, MidiSettingsWidget.h/.cpp, MainWindow.h/.cpp, MidiOutput.cpp, CMakeLists.txt

v1.1.9

2026-04-07
MidiPilot AI Improvements + GUI Bug Sweep
  • Granular agent undo - each tool call gets its own Ctrl+Z step instead of one compound action
  • Token counting fix - OpenAI Responses API, Anthropic, and Gemini usage fields now correctly normalized
  • Persistent conversation history - conversations auto-saved as JSON, loadable from history menu
  • Context window management - sliding-window truncation prevents exceeding model context limits
  • Token label now shows context window size and warns at 80% usage
  • Agent progress steps now theme-aware (dark/light mode colors)
  • Response streaming (SSE) for Simple mode - text appears incrementally
  • Per-file AI presets - save/load model, provider, mode, FFXIV, effort, and custom instructions per MIDI file
  • 4 runtime bugfixes from Phase 19 testing - stop button crash, streaming JSON dump, vanishing prompt bubble, preset save on unsaved files
  • MidiPilot Send/Stop buttons now use proper themed icons instead of Unicode emoji (consistent with toolbar style)
  • 45 bug fixes across 18 GUI source files - memory leaks, crash-causing null derefs, undo/redo corruption, data loss, division-by-zero, deprecated Qt6 API usage, and more

🟡 Medium Phase 19 - MidiPilot AI Improvements

Fixed
  • Granular agent undo (19.1) - Previously, all tool calls from one Agent request were wrapped in a single startNewAction/endAction pair, so Ctrl+Z reverted everything at once. Now each tool call (e.g. transpose, edit, rename) creates its own protocol step. Undo reverts one action at a time.
  • Token counting for non-OpenAI providers (19.3) - normalizeResponsesApiResponse() was not copying the usage field from OpenAI Responses API responses (input_tokens/output_tokens). Also added normalization for Anthropic (input_tokens/output_tokens) and Gemini (usageMetadata.promptTokenCount/candidatesTokenCount) formats. All downstream code uses canonical prompt_tokens/completion_tokens field names.
  • Stop button crash - Clicking Stop during an API request could crash because cancel() called cancelRequest() first, which triggered a synchronous abort()onReplyFinishederrorOccurred double-fire. Fixed by reordering: cleanup() runs before cancelRequest().
  • Simple mode JSON dump - Streaming in Simple mode displayed raw JSON {"actions":[...]} in chat instead of executing it. Added _streamIsJson flag to suppress the streaming bubble for JSON action responses; onStreamFinished now delegates to onResponseReceived for proper action dispatch.
  • User prompt bubble disappearing - onResponseReceived and onErrorOccurred blindly removed the last chat widget (intended for the "Thinking..." indicator), which sometimes deleted the user's own prompt bubble. Now checks for "Thinking" text via qobject_cast<QLabel*> before removing.
  • Preset save on unsaved file - Saving an AI preset when the MIDI file had never been saved showed an error. Replaced with a QFileDialog::getSaveFileName fallback so the user can pick a file path first.
Added
  • Persistent conversation history (19.2) - New ConversationStore class saves conversations as JSON files in AppData/MidiPilotHistory/. Auto-saves after every assistant response (debounced 2s). History button in toolbar shows past conversations sorted by date. Click to load and resume any previous conversation.
  • Context window management (19.5) - Before each API request, conversation history is estimated for token count (~4 chars/token). If exceeding 70% of the model's context window, a sliding window keeps the first 2 messages + most recent messages that fit, inserting a truncation marker. Token label now shows session / contextWindow and turns yellow at 80% usage.
  • Model context window lookup - AiClient::contextWindowForModel() returns known context windows for GPT-5/4o/4.1, Claude 3/4, Gemini 1.5/2.0/2.5, o-series models.
  • Agent progress polish (19.4) - AgentStepsWidget step labels now use theme-aware colors via Appearance::isDarkModeEnabled(). Dark and light mode each have distinct pending/active/success/retry/failed colors.
  • Response streaming (19.6) - Simple mode now uses SSE streaming ("stream": true). Text appears incrementally in a streaming bubble. On completion, the bubble is replaced with a proper styled chat bubble. Handles [DONE] sentinel, captures usage from final chunk. Agent mode remains non-streaming.
  • Per-file AI presets (19.7) - Gear button now opens a menu with "Open AI Settings" and "Save AI preset for this file". Presets are stored as <filename>.midipilot.json sidecar files. Saves provider, model, mode, FFXIV, effort, and custom instructions. Auto-loaded when a file is opened via onFileChanged(). Custom instructions are appended to both Agent and Simple mode system prompts.

🔧 Fixed GUI Bug Sweep - 45 fixes across 18 files

Data Loss Prevention (3 fixes)
  • CORE-006/007/014 - newFile(), loadFile(), and load() now check the return value of saveBeforeClose(). Previously, clicking Cancel in the save prompt was ignored and unsaved work was silently discarded.
Undo/Redo Protocol Corruption (3 fixes)
  • CORE-005 - equalize() called endAction() without a matching startNewAction() when ≤1 notes were selected, corrupting the undo stack. Moved endAction() inside the guard.
  • WIDGET-001 - EventWidget::setModelData() left the protocol in a dangling state on validation error returns. Added endAction() before early returns.
  • WIDGET-002 - MiscWidget::mouseReleaseEvent() left the protocol open when track was null. Added endAction() before early returns.
Crash Fixes - Null Derefs & Out-of-Bounds (5 fixes)
  • CORE-009 - MatrixWidget::mouseDoubleClickEvent() null dereference when no file loaded. Added null check.
  • CORE-013 - MainWindow::editChannel() null dereference when file is null. Wrapped in if (file).
  • MISC-004 - TweakTarget::getTimeOneDivEarlier/Later() crashed on single-element divs list (out-of-bounds access). Added if (divs.size() < 2) return time guard.
  • MISC-005 - NoteTweakTarget and ValueTweakTarget missing upper-bound checks - arrow keys could push note/velocity/CC past 127, pitch bend past 16383. Added qBound() clamping.
  • WIDGET-005 - MiscWidget trackIndex could go out-of-bounds if undo changed data between mouse press and release. Added bounds check.
Memory Leaks (14 fixes)
  • CORE-001/002 - forward() and back() leaked heap-allocated QList on every call. Changed to stack allocation.
  • CORE-003/004 - saveas() and load() leaked heap QFile and discarded directory result. Changed to stack QFile, assigned dir properly.
  • CORE-008 - MatrixWidget::paintEvent() leaked QPainter on two early-return paths. Added delete painter before returns.
  • CORE-012 - Multiple dialog functions leaked heap-allocated dialogs. Added WA_DeleteOnClose for show() dialogs, delete after exec() dialogs.
  • DLG-001 - AboutDialog::loadContributors() returned heap QList* that was never freed. Changed to return by value.
  • DLG-002/003 - RecordDialog::enter() leaked filtered-out MidiEvent objects; no destructor to clean up _data. Added destructor and cleanup loop.
  • DLG-005 - TransposeDialog leaked parentless QButtonGroup. Added this as parent.
  • DLG-006 - SettingsDialog leaked heap-allocated _settingsWidgets QList. Changed to stack allocation.
  • MISC-001 - Appearance::decode() leaked defaultColor parameter on success path. Added delete defaultColor before returning new color.
  • MISC-002 - ClickButton::setImageName() leaked previous QImage on reassignment. Added delete image before new, initialized to nullptr.
  • MISC-010 - AppearanceSettingsWidget leaked heap-allocated _channelItems/_trackItems QLists. Changed to stack allocation.
Division by Zero (2 fixes)
  • CORE-010 - MatrixWidget::xPosOfMs(), msOfXPos(), timeMsOfWidth() could divide by zero with zero-length files or very narrow widget. Added zero-checks.
  • WIDGET-006 - MiscWidget::value() divided by height() without checking for zero. Added if (h <= 0) return 0 guard.
Signal & Resource Management (2 fixes)
  • CORE-011 - play() and record() accumulated timeMsChanged signal connections on each play/stop cycle, causing N+1 repaints per tick. Added disconnect() before connect().
  • MISC-008 - Appearance::processNextQueuedIcon() used try/catch on potentially dangling QAction* pointer (undefined behavior). Changed iconUpdateQueue to use QPointer<QAction> for safe null detection.
Logic & Correctness (4 fixes)
  • WIDGET-003 - EventWidget::setEditorData() called setMaximum() instead of setMinimum() in 7 places (copy-paste error). Time signature allowed numerator 0. Fixed to setMinimum().
  • WIDGET-008 - InstrumentChooser::accept() called hide() instead of QDialog::accept(), leaving result code as Rejected. Fixed to call base class.
  • DLG-004 - TransposeDialog constructor didn't pass parent to QDialog base class. Added : QDialog(parent) initializer.
  • DLG-009 - TempoDialog::accept() integer overflow in smooth tempo interpolation for long ramps. Changed to qint64 arithmetic.
Download Robustness (2 fixes)
  • DLG-007 - DownloadSoundFontDialog didn't flush remaining network buffer before closing file. Added readAll() flush in finished handler.
  • DLG-008 - Download write errors silently ignored (disk full → corrupt SoundFont). Added return value check on write(), abort on failure.
Deprecated/Wrong Qt6 API (2 fixes)
  • WIDGET-009 - TrackListWidget::dropEvent() used deprecated QDropEvent::pos(). Changed to position().toPoint().
  • MISC-011 - PaintWidget::enterEvent(QEvent*) didn't match Qt6 signature enterEvent(QEnterEvent*). Fixed signature.
Initialization & Hardening (5 fixes)
  • MISC-003 - GraphicObject::shownInWidget never initialized (undefined behavior on read). Initialized to false.
  • MISC-009 - MidiSettingsWidget::_inputPorts/_outputPorts pointer members uninitialized. Set to nullptr.
  • WIDGET-010 - ChannelListItem::loudAction/soloAction uninitialized for channel ≥ 16. Set to nullptr.
  • WIDGET-011 - ChannelListItem::toggleVisibility() silent catch-all exception handler. Added qWarning() logging.
  • MISC-006 - OpenGLPaintWidget::paintGL() used static QSize lastSize shared across all instances, causing unnecessary GPU reallocations. Changed to member _lastPaintSize.
UI & Cosmetic (3 fixes)
  • DLG-010 - DeleteOverlapsDialog::paintEvent() called update() on child widget, creating redundant repaint cycles. Removed override.
  • DLG-011 - DeleteOverlapsDialog::resizeEvent() didn't call base class QDialog::resizeEvent(). Fixed.
  • MISC-007 - AutoUpdater::applyUpdate() PowerShell command injection via single-quote in paths. Added ''' escaping.

📝 Notes Technical Notes

  • Phase 19 files created: src/ai/ConversationStore.h/.cpp
  • Phase 19 files modified: src/gui/MidiPilotWidget.h/.cpp, src/ai/AiClient.h/.cpp
  • Bug sweep files modified (18): MainWindow.cpp, MatrixWidget.cpp, EventWidget.cpp, MiscWidget.cpp, RecordDialog.cpp/.h, TransposeDialog.cpp, SettingsDialog.cpp/.h, AboutDialog.cpp/.h, DownloadSoundFontDialog.cpp, TempoDialog.cpp, DeleteOverlapsDialog.cpp, GraphicObject.cpp, ClickButton.cpp, Appearance.cpp, TweakTarget.cpp, InstrumentChooser.cpp, TrackListWidget.cpp, ChannelListWidget.cpp, OpenGLPaintWidget.cpp/.h, PaintWidget.h, MidiSettingsWidget.h, AppearanceSettingsWidget.cpp/.h, AutoUpdater.cpp
  • Bug report: Planning/03_bugs.md - full scan results with verification status

v1.1.8.1

2026-04-06
Bugfix: FFXIV Channel Fixer
  • Fixed undo crash when reverting Channel Fixer operations
  • Restored Tier 3 guitar track renaming with reliable data source
  • Added Tier 2 event cleanup (remove CC, PitchBend, etc.; keep Text/lyrics)
  • Context menu "Move to Channel" now shows instrument names
  • 4 bug fixes from v1.1.8 regression + 2 improvements

🔧 Fixed Fixed

  • Undo crash after Channel Fixer - v1.1.8 introduced toProtocol=false as a performance optimization for bulk operations, but this caused the Protocol undo system to record an empty step (discarded by endAction). Undoing past the Channel Fixer operation crashed because no undo entry existed and deleted events left dangling pointers. Reverted to toProtocol=true (default) so all changes are properly recorded; removed delete ev on removed Program Changes since the Protocol system keeps events alive for undo
  • Tier 3 guitar track renaming restored - v1.1.8.0 removed all Tier 3 renaming as a workaround for unreliable chToVariant data. Now that guitarChannelMap is built from assignedChannel() (the v1.1.8.0 fix), the reverse-map is reliable again. Tier 3 now detects when a guitar track's first note is on a different variant's channel and renames accordingly (e.g. Overdriven track with first note on Distortion channel → renamed to PowerChords)
  • Tier 3 crash / wrong channel on first run after Tier 2 - (carried forward from v1.1.8.0) Uses track->assignedChannel() as sole source of truth
  • Free channel reservation Tier 2 only - (carried forward from v1.1.8.0)

✨ Added Added

  • Tier 2 event cleanup - Tier 2 (Rebuild) now removes non-essential MIDI events (ControlChange, PitchBend, ChannelPressure, KeyPressure, SysEx, etc.) from all channels. FFXIV performance mode doesn't use these events. Text events (lyrics) and notes are preserved
  • Context menu channel names - Right-click "Move to Channel" submenu now shows instrument names (e.g. 0: Piano, 1: ElectricGuitarOverdriven) matching the toolbar channel menus, instead of bare channel numbers

📝 Notes Technical Notes

  • Undo root cause: toProtocol=false prevents MidiChannel::removeEvent/MidiEvent::moveToChannel from calling protocol(copy, this), so startNewAction/endAction recorded 0 items and silently discarded the step
  • Files modified: src/ai/FFXIVChannelFixer.cpp (undo fix, rename restoration, event cleanup), src/gui/MatrixWidget.cpp (channel names in context menu)

v1.1.8

2026-04-06
Mewo Feature Sync
Cherry-picking the best upstream features: context menus, note presets, timeline markers, chord detection, MML import, drum presets, and a whole lotta polish.
  • Right-click context menu, note duration presets, smooth playback scrolling
  • Timeline markers (CC/PC/Text), status bar with chord detection
  • MML importer (.mml/.3mle), drum kit presets, DLS SoundFont support
  • 7 bug fixes · 3 UI changes · Cherry-picked 16 features from Mewo upstream

✨ Added Added

  • Right-Click Context Menu - right-click on selected events in the piano roll for quick access to Quantize, Copy, Delete, Transpose, Move to Track/Channel, Scale, and Legato operations. Plain right-click opens the menu; Ctrl+Right-Click still creates notes
  • Note Duration Presets - when the pencil tool is active, select a fixed note duration (whole, half, quarter, 8th, 16th, 32nd) from the toolbar with keyboard shortcuts (1-6). Shows a semi-transparent ghost preview on hover before clicking
  • Smooth Playback Scrolling - playback cursor now smoothly scrolls the viewport instead of jumping by screen-widths. Toggle in Settings → Appearance or Additional Midi Settings
  • Timeline Markers - visual CC, Program Change, and Text/Marker event indicators on the timeline:
  • Status Bar with Chord Detection - bottom status bar showing selected note info, chord analysis (major/minor/7th/dim/aug/sus), tick position, and event count. Togglable in Settings → System & Performance
  • MML Importer - import Music Macro Language (.mml/.3mle) text files as MIDI. Supports 3MLE format used by FFXIV bard performers with multi-track channels, tempo, octave, note length, volume, and program change commands
  • DrumKit Preset Mapping - Split Channels dialog now includes a "Drum Kit Preset" dropdown for channel 9 with 17 standard GM drum kits (Standard, Room, Power, Electronic, etc.) that auto-insert the correct Program Change event
  • DLS SoundFont Support - file dialog now accepts .dls (Downloadable Sound) files alongside .sf2/.sf3

🔧 Fixed Fixed

  • Null-byte text event truncation - MIDI text events containing \0 null bytes no longer truncate or show [] boxes in the UI
  • SizeChangeTool improvements - resize handle behavior refined for more precise note length editing
  • Measure numbers shifting with markers - measure numbers now use a fixed Y position (textY = 41) so they don't jump when the timeline marker bar appears/disappears
  • TrackList/ChannelList stale toolbar colors - added refreshColors() methods that properly update toolbar palettes on theme change, instead of just repainting
  • FFXIVChannelFixer memory leak - Program Change events removed during channel fixing are now properly deleted after removal from the channel
  • FFXIVChannelFixer bulk operation performance - moveToChannel() and removeEvent() now accept a toProtocol parameter; Channel Fixer passes false to skip hundreds of individual undo entries during batch operations
  • Appearance Settings UI - collapsible Channel/Track Colors sections now use clickable arrow buttons (▶/▼) instead of checkboxes, readable in all themes. Settings panel wrapped in QScrollArea

🔄 Changed Changed

  • Timeline layout - separate MarkerArea rect for the 16px marker row below the 50px ruler; cleaner hit-testing separation
  • Full-height vertical divider - the divider between piano/header area and the note area now extends from top to bottom of the widget
  • Removed bottom/right border lines - cleaner look without redundant edge borders on the piano roll
  • Version bump to 1.1.8

📝 Notes Technical Notes

  • Mewo upstream sync: Cherry-picked 16 features/fixes from Meowchestra/MidiEditor commits 28eb14c..5080ff3 (62 commits after fork-point 25ebba3)
  • New files: src/gui/ChordDetector.h, src/gui/StatusBarSettingsWidget.h/cpp, src/gui/SplitChannelsDialog.h/cpp, src/midi/DrumKitPreset.h/cpp, src/converter/MML/ThreeMleParser.h/cpp, src/converter/MML/MmlConverter.h/cpp
  • Core modifications: MatrixWidget.h/cpp (context menu, smooth scroll, marker bar, MarkerArea), MainWindow.h/cpp (status bar, duration presets, MML import), Appearance.h/cpp (marker settings, refreshColors), MidiEvent.h/cpp + OnEvent.h/cpp + MidiChannel.h/cpp (toProtocol parameter), FFXIVChannelFixer.cpp (leak fix + toProtocol), TrackListWidget.h/cpp + ChannelListWidget.h/cpp (refreshColors), AppearanceSettingsWidget.cpp (collapsible arrows, scrollable layout)

v1.1.7

2026-04-05
The Totally Unnecessary Glow Up
Adding Dark/Light Mode and a totally useless but cool MIDI Visualizer.
  • 7 dark/light QSS themes (Dark, Light, Sakura, AMOLED, Material Dark, System, Classic)
  • Real-time 16-channel MIDI Visualizer in the toolbar
  • 10 note bar color presets, Sakura piano keys, dark title bar (Windows)
  • Dark mode icon adjustment, checkbox visibility fix
  • 2 bug fixes · Theme restart mechanism

✨ Added Added

  • Dark & Light QSS Themes - full application theming with seven modes:
  • Dark Title Bar (Windows) - native Windows dark title bar using DWM API (DWMWA_USE_IMMERSIVE_DARK_MODE), applies to all windows and dialogs automatically
  • Theme Change Restart - changing themes triggers app restart with confirmation dialog; reopens Settings → Appearance tab automatically via --open-settings CLI flag
  • MIDI Visualizer - real-time 16-channel equalizer bars in the toolbar:
  • Note Bar Color Presets - 10 one-click channel color schemes in Settings → Appearance:
  • Sakura piano keys - white keys tinted lavender blush (#FFF0F5), black keys dark rose (#502837), with matching hover/selected/highlight states
  • Toolbar icon dark mode adjustment - black toolbar icons automatically recolored to light gray in dark themes for visibility (colored icons like FFXIV Fix, Explode Chords, MidiPilot preserved as-is)
  • Checkbox visibility in dark mode - brighter borders for checkboxes in dark theme

🔧 Fixed Fixed

  • Color preset combo always showing "Default" - added _colorPreset persistence to QSettings; combo now correctly shows the saved preset when re-entering Settings
  • Piano key note labels unreadable in dark mode - C1/C2/C3 octave labels on the piano bar changed from hardcoded Qt::gray to light gray (QColor(200,200,200)) in dark themes for readability

🔄 Changed Changed

  • Toolbar inline styles refactored to use centralized Appearance helper methods for theme consistency
  • Version bump to 1.1.7

📝 Notes Technical Notes

  • New files: src/gui/themes/dark.qss, src/gui/themes/light.qss, src/gui/themes/pink.qss, src/gui/themes/amoled.qss, src/gui/themes/materialdark.qss, src/gui/MidiVisualizerWidget.h/cpp, run_environment/graphics/tool/midi_visualizer.png
  • Core modifications: Appearance.h/cpp (theme management, 7 themes, 10 color presets, piano key overrides, DWM dark title bar, icon adjustment), AppearanceSettingsWidget.h/cpp (theme selector + preset combo UI), MainWindow.h/cpp (toolbar theming, visualizer lifecycle, restart mechanism), SettingsDialog.h/cpp (setCurrentTab()), main.cpp (--open-settings CLI arg), LayoutSettingsWidget.cpp (visualizer in customize toolbar)
  • Visualizer lifecycle: Widget created fresh on each toolbar rebuild via toolbar->addWidget() - avoids Qt QWidgetAction ownership bugs where setDefaultWidget() transfers ownership to the toolbar which destroys the widget on rebuild
  • AMOLED/Material themes inspired by: GTRONICK/QSS (MIT License) - color palettes adapted into our QSS structure

v1.1.6.1

2026-04-04
Bugfix: Duplicate Guitar Track Channels
  • Fixed duplicate guitar variants mapped to wrong channel in Fix X|V
  • Fixed Tier 3 duplicate guitar notes not migrated to shared channel

🔧 Fixed Fixed

  • Fix X|V Channels: duplicate guitar variants mapped to wrong channel - when two or more tracks shared the same guitar variant name (e.g. two "ElectricGuitarPowerChords" tracks), the second track was assigned its own channel instead of sharing the first occurrence's channel. This caused the duplicate track to receive a wrong program (e.g. Piano on CH3 instead of Distortion Guitar). Now all tracks with the same guitar variant name share a single channel with the correct program change, in both Rebuild (Tier 2) and Preserve (Tier 3) modes
  • Fix X|V Channels Tier 3: duplicate guitar notes not migrated - in Preserve mode, duplicate guitar tracks had their notes stranded on the original channel. Added note migration for duplicate guitar tracks to move events to the shared target channel

🔄 Changed Changed

  • Version bump to 1.1.6.1

v1.1.6

2026-04-04
Guitar Pro Import (GP1-GP8)
  • Native Guitar Pro import: GP1-GP8 (.gtp, .gp3, .gp4, .gp5, .gpx, .gp)
  • Fixed GP3/GP4/GP5 binary parser bugs, GP6 BCFZ decompression, GP7/GP8 ZIP extraction
  • GP1/GP2 legacy parser ported from TuxGuitar reference
  • Explode Chords toolbar icon

✨ Added Added

  • Native Guitar Pro import - all Guitar Pro formats from 1990s DOS to 2024 are now supported:
  • GP3/GP4/GP5 binary parser fixes (Phase 16.1) - the upstream Meowchestra fork contained an unfinished Guitar Pro parser skeleton that failed on every real-world file. Fixed:
  • GP6 BCFZ decompression rewrite (Phase 16.2) - upstream used zlib inflate() on BCFZ data which is completely wrong (BCFZ is a custom bit-level LZ77 algorithm, not zlib/DEFLATE). Rewrote decompressGPX() with the correct algorithm, ported from C# BardMusicPlayer/LightAmp source code
  • GP7/GP8 ZIP extraction rewrite (Phase 16.3) - upstream parsed local file headers which have unreliable sizes when ZIP data descriptors are present. Rewrote to parse the central directory from the end of the file for correct entry sizes
  • GPIF XML node lookup fix (Phase 16.4) - getSubnodeByName() could find nested elements (e.g. <Bars> inside <MasterBar>) before the top-level collection, causing silent data loss. All lookups now use directOnly=true
  • GP1/GP2 legacy parser (Phase 16.6) - new Gp12Parser classes ported from TuxGuitar's GP1InputStream.java / GP2InputStream.java reference implementation
  • Explode Chords to Tracks icon - toolbar icon added for the Explode Chords tool

🔄 Changed Changed

  • Version bump to 1.1.6

📝 Notes Technical Notes

  • Origin: Meowchestra/MidiEditor upstream contained a non-functional Guitar Pro parser (src/converter/GuitarPro/) with basic structure for GP3-GP7. Binary parsers had field-ordering bugs, BCFZ used the wrong algorithm, ZIP extraction failed on data descriptors, XML lookups returned wrong elements. All bugs were fixed, GP1/GP2 support was added from scratch.
  • Architecture: Three parser families - binary (Gp345Parser inheritance chain), XML (Gp678Parser with BCFZ/ZIP decompression), and legacy (Gp12Parser inheritance chain). All share GpImporter as entry point with header-based detection.
  • Tested formats: GP1 (You've Got Something There.gtp, 8 tracks/12 measures), GP3 (U2 - Lemon.gp3, 9/199), GP4 (Sakuran.gp4, 5/137), GP5 (Flogging-Molly.gp5, 5/74), GP6 (Sweet Child O Mine.gpx, 6/180), GP7 (The Mirror.gp, 6/147)

v1.1.5

2026-03-31
Auto-Updater
  • Seamless in-app auto-updater from GitHub Releases (Update Now / After Exit / Download / Skip)
  • Self-update: renames running EXE, extracts new, restarts - no installer needed
  • Fixed MidiPilot chat Ctrl+C copy and context menu styling

✨ Added Added

  • Auto-Updater - seamless in-app update directly from GitHub Releases:
  • Manual: Auto-Update section updated with screenshots (Update dialog, Progress bar, Scheduled confirmation)

🔧 Fixed Fixed

  • MidiPilot chat: Ctrl+C copy - selected text in chat bubbles can now be copied with Ctrl+C (previously only right-click → Copy worked)
  • MidiPilot chat: context menu - replaced the unstyled default system context menu with a compact dark-themed menu (Copy / Select All) that matches the application style

🔄 Changed Changed

  • Feature table: "Auto-Update Checker" renamed to "Auto-Updater" reflecting the new in-app update capability
  • Version bump to 1.1.5

v1.1.4.1

2026-03-30
Chat UX Fix
  • Fixed MidiPilot chat Ctrl+C copy and styled context menu

🔧 Fixed Fixed

  • MidiPilot chat: Ctrl+C copy - selected text in chat bubbles can now be copied with Ctrl+C (previously only right-click → Copy worked)
  • MidiPilot chat: context menu - replaced the unstyled default system context menu with a compact dark-themed menu (Copy / Select All) that matches the application style

🔄 Changed Changed

  • Version bump to 1.1.4.1

v1.1.4

2026-03-29
Split Channels to Tracks
  • Split single-track multi-channel MIDI into one track per instrument
  • Preview dialog with GM instrument names, auto-naming, drum track option
  • Single undo action, toolbar migration for existing users

✨ Added Added

  • Split Channels to Tracks - convert single-track multi-channel GM MIDI files into one track per instrument:
  • Manual: Split Channels to Tracks page added with screenshots and animated GIF

🔄 Changed Changed

  • Version bump to 1.1.4

v1.1.3.1

2026-03-29
Auto-Save
  • Auto-save with sidecar .autosave backups after configurable idle period
  • Crash recovery for untitled documents, orphaned backup detection
  • Configurable interval (30-600s), toggle in Settings → System & Performance

✨ Added Added

  • Auto-Save - automatic backup saves to prevent data loss during editing:
  • Auto-Save settings in Settings → System & Performance:
  • Manual: Auto-Save section added to MidiPilot documentation page with screenshot

🔄 Changed Changed

  • Version bump to 1.1.3.1

v1.1.3

2026-03-28
Prompt Architecture v2, Crash Fix & Provider Selector
  • Provider selector in MidiPilot footer (OpenAI / OpenRouter / Gemini / Custom)
  • Prompt v2: priority rules, validation block, timing reference, truncation fallback
  • Fixed crash on New File during playback (use-after-free)

✨ Added Added

  • Provider selector in MidiPilot footer - switch between OpenAI, OpenRouter, Gemini, and Custom directly in the chat panel without opening Settings. Automatically swaps API key, base URL, and model list per provider
  • Prompt: Priority Rule (Phase 12.1) - mode-specific prompts (e.g. FFXIV) now explicitly override general rules, eliminating the #1 source of FFXIV agent errors (inserting program_change when the mode says not to)
  • Prompt: Final Validation Block (Phase 12.2) - compact checklist appended to system prompts that the LLM reviews before responding (field requirements, value ranges, FFXIV constraints)
  • Prompt: Timing Reference (Phase 12.3) - explicit note duration formulas (quarter = ticksPerQuarter, eighth = ticksPerQuarter/2, etc.) so LLMs no longer miscalculate rhythms
  • Prompt: Truncation Fallback (Phase 12.4) - instruction to produce the smallest complete musically coherent version instead of truncated output when a request is too large
  • Prompt: Schema unification (Phase 12.6) - Simple mode prompt now always requires the actions[] array format, reducing ambiguity (parser still accepts single-action format for backward compat)
  • Agent mode: Invalid event feedback (Phase 12.5) - deserialize() now collects validation errors and returns them in the tool result (skippedErrors array), allowing the LLM to self-correct in subsequent tool calls instead of silently losing events

🔧 Fixed Fixed

  • Crash: New File during playback - newFile() now stops playback before replacing the MIDI file, and MidiPlayer::stop() waits for the player thread to finish (wait()), preventing a use-after-free crash when the old file was deleted while PlayerThread still accessed it on a separate thread

🔄 Changed Changed

  • Removed unused tsEvents variable in EditorContext::captureKeySignature() (Phase 12.7)
  • Version bump to 1.1.3

v1.1.2.2

2026-03-28
Manual Update: Prompt Examples & CI
  • Prompt Examples with screenshots and downloadable MIDI files
  • Fixed broken download links, stray n artifact, navbar overflow
  • CI dependency bumps (GitHub Actions v6/v7)

✨ Added Added

  • Prompt Examples: screenshots & MIDI downloads - every prompt section (Composing, Editing, Harmony, Arrangement) now includes result screenshots and downloadable MIDI files
  • New manual assets - manual/midi/ and manual/wav/ directories for hosting example files locally on GitHub Pages

🔧 Fixed Fixed

  • Broken download links - MIDI/WAV links in Prompt Examples pointed to non-existent ../examples/ path (404); now link to midi/ and wav/ within the manual
  • Stray ` n `` artifact in 15 HTML pages - a PowerShell newline literal was rendered as visible text in the navbar
  • Navbar overflow - reduced font size, gap, and disabled wrapping so all 12 links fit in a single row

🔄 Changed Changed

  • README: documentation link now points to the manual index instead of directly to the MidiPilot page
  • CI: bumped GitHub Actions dependencies (checkout v6, upload-artifact v7, configure-pages v6, upload-pages-artifact v4, deploy-pages v5)

v1.1.2.1

2026-03-26
Hotfix: SoundFont Persistence & CI Fix
  • Fixed SoundFont stack lost when switching MIDI output
  • Fixed CI/Release FluidSynth download 404

🔧 Fixed Fixed

  • SoundFont stack lost when switching MIDI output - switching from FluidSynth to another output (e.g. Microsoft GS Wavetable) and back no longer loses loaded SoundFonts; the engine now preserves font paths across shutdown/reinitialize cycles
  • CI/Release workflow: FluidSynth download 404 - updated FluidSynth v2.5.2 asset URL to match upstream's renamed zip (fluidsynth-v2.5.2-… instead of fluidsynth-2.5.2-…)

v1.1.2

2026-03-26
FluidSynth & FFXIV SoundFont Mode
  • Built-in FluidSynth synthesizer with SoundFont stack management
  • FFXIV SoundFont Mode: melodic channels + per-note drum program injection
  • Velocity normalization in Fix X|V Channels (all notes → 127)
  • Version in title bar, DLS support, SoundFont download dialog
  • 2 bug fixes (guitar channel migration, transpose dialog)

✨ Added Added

  • Built-in FluidSynth synthesizer (upstream merge from Meowchestra/MidiEditor) - no external softsynth needed. Select *FluidSynth (Built-in Synthesizer)* as MIDI output and load any SF2/SF3 SoundFont directly in Settings
  • SoundFont stack management (upstream) - load multiple SoundFonts with drag-and-drop priority ordering; highest-priority font is checked first for presets
  • SoundFont download dialog (upstream) - one-click download of recommended SoundFonts (General MIDI, FFXIV) from within the application
  • FFXIV SoundFont Mode - single toggle in FluidSynth settings that:
  • Velocity normalization in Fix X|V Channels - Tier 2 (Rebuild) and Tier 3 (Preserve) now set all NoteOn velocities to 127 (max), since FFXIV performance has no dynamics
  • Manual: SoundFont & FluidSynth page - new documentation section covering built-in synth setup, SoundFont management, FFXIV SoundFont Mode, and audio settings
  • Version in title bar - application window title now shows MidiEditor AI v1.1.2 (with version number) on startup, file open, save, and new document

🔧 Fixed Fixed

  • Fix X|V Channels Rebuild mode: guitar notes stuck on wrong channel - removed an incorrect guitar-channel exemption that prevented notes from being moved to their target channel when the source channel belonged to another guitar variant (e.g., Track 5 PowerChords notes stayed on CH1 instead of moving to CH5)
  • Transpose dialog button order - swapped Cancel/Accept buttons back to original MidiEditor layout (Cancel left, Accept right) to match muscle memory from the original editor

🔄 Changed Changed

  • Fix X|V Channels - "All Channels Melodic (FFXIV)" checkbox renamed to "FFXIV SoundFont Mode" with updated tooltip explaining both melodic channels and drum program injection
  • Fix X|V Channels result dialog - now shows velocity normalization count (🔊 Normalized X note velocity(ies) to 127)
  • Fix X|V Channels tier descriptions - both Rebuild and Preserve modes now list velocity normalization as a bullet point
  • Manual: updated Fix X|V Channels page with new screenshots and velocity normalization info
  • README: added FluidSynth / SoundFont section and updated Features table
  • Version bump to 1.1.2

v1.1.1

2026-03-25
Upstream merge
  • Metronome rewrite: GM drum notes instead of WAV files
  • Removed Qt6::Multimedia and Qt6::Xml dependencies
  • Plugin path fix, rtmidi updated

🔄 Changed Changed

  • Metronome rewrite - replaced QSoundEffect audio playback with General MIDI drum notes on Channel 10 (High/Low Wood Block). No more WAV file dependency, works through the connected MIDI output device. Downbeats and regular beats now use distinct drum sounds
  • Metronome timing - all PlayerThread→Metronome signal connections now use Qt::DirectConnection for tighter timing in the audio thread
  • Removed Qt6::Multimedia and Qt6::Xml dependencies - fewer Qt modules required, smaller deployment footprint
  • Plugin path fix - QCoreApplication::addLibraryPath(appDir + "/plugins") added before QApplication construction; windeployqt now deploys to plugins/ subdirectory for reliable Qt plugin discovery
  • rtmidi updated - submodule bumped to latest upstream (a3233c2)
  • Version bump to 1.1.1

🔄 Changed Removed

  • Metronome WAV/MP3 audio files (no longer needed - metronome clicks via MIDI)
  • Qt6::Xml and Qt6::Multimedia from build dependencies

v1.1.0

2026-03-25
  • Fix X|V Channels: one-click deterministic FFXIV channel fixer
  • 5-step algorithm with Rebuild/Preserve modes, guitar variant channels
  • Rich result summary, progress dialog, single undo action
  • Fixed QSettings mismatch, stale channel menus

✨ Added Added

  • Fix X|V Channels - one-click deterministic FFXIV channel fixer (no AI call needed):
  • FFXIVChannelFixer class - static deterministic fixer delegated from setup_channel_pattern tool
  • Toolbar migration code ensures Fix X|V button appears for existing users

🔧 Fixed Fixed

  • QSettings constructor mismatch - FFXIV tools (setup_channel_pattern, validate_ffxiv, convert_drums_ffxiv) were never sent to the LLM because ToolDefinitions read from a different registry path than MidiPilotWidget wrote to. Both now use QSettings("MidiEditor", "NONE")
  • Stale channel menus - "Move events to channel" and similar channel context menus now refresh after Fix X|V Channels (added updateChannelMenu() to updateAll())

🔄 Changed Changed

  • Fix X|V Channels dialog - removed redundant Tier 1 (Abort) option; Abort button already cancels. Tier labels simplified to "Rebuild (Full Reassignment)" and "Preserve (Minimal Changes)" without tier numbers
  • Fix X|V Channels result - replaced debug text popup with structured HTML info log showing success status, channel mapping table, program change stats, and track renames
  • FFXIV system prompts simplified - removed verbose channel/program tables, now instructs the LLM to call setup_channel_pattern once
  • UI label renamed from "Fix FFXIV Channels" to "Fix X|V Channels" across all dialogs and menus
  • README: added Fix X|V Channels to Features table, FFXIV constraint table, and Tools reference
  • Manual: new Fix X|V Channels section in MidiPilot page with before/after screenshots and animated GIF
  • Manual: new Fix X|V Channels entry in Tools Menu page
  • Manual: separate Rebuild and Preserve GIF animations replacing single animation
  • Version single source of truth - setApplicationVersion() now reads from CMake define MIDIEDITOR_RELEASE_VERSION_STRING_DEF instead of a hardcoded string. Only update version in CMakeLists.txt line 3
  • Version bump to 1.1.0

v1.0.2

2026-03-24
  • FFXIV Bard Mode system prompts rewritten for better LLM compliance

🔄 Changed Changed

  • FFXIV Bard Mode system prompts rewritten for better LLM compliance:
  • Version bump to 1.0.2

v1.0.1

2026-03-24
  • Manual: Prompt Examples page with screenshots, MIDI downloads, demo videos
  • New Chat button redesign + confirmation dialog
  • FFXIV example prompts improved

🔄 Changed Changed

  • New Chat button: replaced ambiguous ➕ icon with a dedicated "message-plus" icon for clarity
  • New Chat now shows a confirmation dialog when conversation history exists
  • Manual: added Prompt Examples page with real-world prompts, screenshots, and demo videos
  • Manual: added API Log documentation to MidiPilot page
  • Manual: added navigation links on all pages for Prompt Examples
  • MidiPilot page hero image updated to animated Agent Run GIF
  • Manual: added lo-fi hip hop example files (MIDI + WAV) as downloadable examples
  • FFXIV example prompts improved: removed redundant MidiBard2 references and over-specified constraints
  • YouTube demo videos embedded on Prompt Examples page (4 videos: full agent run, audio preview, guitar solo, harmony)
  • Version bump to 1.0.1

v1.0.0

2026-03-20
  • MidiPilot AI Assistant - integrated chat panel with Agent Mode (13 tools), Simple Mode, FFXIV Bard Mode
  • Multi-provider support (OpenAI, Gemini, OpenRouter, Ollama, etc.), token tracking, editable system prompts
  • FFXIV Validation tool, GM Drum Conversion tool, API logging, reasoning display
  • Rebranded to MidiEditor AI with CI/CD, GitHub Pages manual
  • Based on Meowchestra/MidiEditor v4.3.1 (all upstream features included)

🟡 Medium MidiPilot AI Assistant (New)

  • MidiPilot - integrated AI chat panel for composing, editing, and transforming MIDI
  • Agent Mode - multi-step autonomous AI with 13 tool functions (create tracks, insert/replace/delete events, set tempo, transpose, etc.)
  • Simple Mode - single-step AI for quick edits with lower token usage
  • FFXIV Bard Mode - toggle for Final Fantasy XIV performance constraints (C3-C6 range, monophonic, 8-track limit, instrument validation)
  • FFXIV Validation tool - checks and auto-fixes MIDI files for FFXIV compliance
  • GM Drum Conversion tool - splits GM drum tracks into separate FFXIV tonal drum tracks
  • Multi-Provider Support - OpenAI, Google Gemini, OpenRouter, Groq, Ollama, LM Studio, or any custom OpenAI-compatible endpoint
  • Token Usage Tracking - displays per-request and session token counts in the status bar
  • Editable System Prompts - customize AI behavior via JSON file or built-in editor dialog
  • API Logging - all API requests/responses logged to midipilot_api.log for debugging
  • Reasoning Display - shows AI thinking/reasoning tokens in a collapsible section

🟡 Medium App & Infrastructure

  • Rebranded from MeowMidiEditor to MidiEditor AI
  • About dialog credits full upstream chain (Markus Schwenk → ProMidEdit → Meowchestra → MidiEditor AI)
  • Update checker redirected to happytunesai/MidiEditor_AI releases
  • GitHub Actions CI/CD: automated build on push + release workflow on tag
  • GitHub Pages manual/wiki deployed automatically
  • Dark-themed manual with MidiPilot documentation, screenshots, and getting started guide

🟡 Medium Based On

  • Meowchestra/MidiEditor v4.3.1 - all upstream features included:
  • Numerous fixes such as sustain notes disappearing if the start & end times were outside the view, sustains partially outside the view visually clipping the note length when dragged back into view, dead scrolls when zoomed in too far, cancelling color selection setting color black instead of properly cancelling, opacity not applying to custom colors set after opacity adjustment, and more.