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/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).
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.
🟡 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-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.cppMainWindow::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.
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.
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.cppsetFile(): 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.
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).
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.
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.
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.
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.
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
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.
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
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.
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):
📝 Notes Technical Notes - The Bulk-Snapshot Pattern
🟡 Medium Files Modified
CMakeLists.txt - version 1.4.1 → 1.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
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.0 → 1.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
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.2 → 1.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
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 18 → 16. 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 18 → 16 in captureKeySignature()
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.
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
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
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
🔧 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.
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
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.
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.
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
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
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
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)
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-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
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() 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)
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.
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() → onReplyFinished → errorOccurred 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.
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 - MiscWidgettrackIndex 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.
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.
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.
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
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
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
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
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.
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
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
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: 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)
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
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-…)
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
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
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
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
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.