You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Commodore 64 / SID support (Phase 42). Open a .sid tune like any other file and edit it as MIDI, and hear it the way it was meant to sound. The importer turns a SID's three voices into a multi-track MIDI (one track per voice plus a dedicated percussion track) with note durations, velocities and arpeggios reconstructed from the chip's register stream.
Two ways to play a C64 tune, one toolbar button. A new C64 button plays the tune in whichever mode you pick under Settings → MIDI I/O → Commodore 64 / SID:
SoundFont - plays the converted MIDI through a Commodore 64 SoundFont so it uses real C64 waveform timbres (pulse, sawtooth, triangle, noise) instead of General MIDI instruments. The C64 SoundFont downloads itself on first use (like the FFXIV font) if you don't already have one.
Emulation - plays the original .sid through the cycle-accurate libsidplayfp engine for authentic chip audio. It is driven by the normal transport: Play starts from the cursor, Stop / Pause and seeking work as usual, the piano-roll cursor follows the music in real-time sync, muting a channel or a track silences the matching SID voice live, and playback stops at the end of the note roll instead of looping forever.
Pick the engine the easy way. The first time you turn C64 mode on, a one-time prompt asks whether to use SoundFont or Emulation and remembers it; the C64 SoundFont (~11 MB) is fetched in the background so either engine works instantly later. A retro SF2 ⟷ EMU toolbar toggle (shown only while C64 mode is active) flips the engine without opening Settings - the toggle, the prompt and the Settings radio all stay in sync.
Authentic RSID import. Interrupt-installing tunes (Arkanoid, RoboCop, Great Giana Sisters, …) that the lightweight importer can't follow are now imported accurately through the libsidplayfp engine. SID tunes loop forever, so a musical loop detector trims the import to intro + one loop, with a configurable fallback length (Settings) when no clean loop is found.
MusicXML export (Phase 43).File → Export MusicXML… writes the current song as a MusicXML score, openable in MuseScore, Finale, Sibelius and Dorico. Since MIDI stores no notation, the exporter reconstructs it: per-track parts, measures from the time signature, note values + dots, rests, ties across barlines, chords, clefs, and enharmonic spelling from the key. v1 is a single voice per part on a 1/16 grid (notes/rhythm/instruments/tempo/key correct and openable everywhere - not publication-perfect engraving); the .mxl (compressed) container and Guitar Pro export are planned follow-ups.
Retro cursor-time display (Phase 41). An opt-in toolbar widget shows the edit-cursor time - or the live playback time, media-player style - as retro seven-segment digits on a dark bezel. Left-click cycles the readout: position, total length, remaining (countdown), tempo (BPM), and musical bar.beat + time signature. Right-click cycles the LED colour theme (Amber, Blue, Green, Sakura, Mono, Red). Adaptive MM:SS, widening to H:MM:SS past an hour. Added / positioned via Customize Toolbar like the MIDI Visualizer and loaded by default; the chosen readout and colour both persist across restarts.
Fixes from a code-review pass (pre-existing bugs). A toolbar widget could crash after you removed it via Customize Toolbar or switched theme (the widget was freed on toolbar rebuild but its pointer left dangling), and FFXIV SoundFont Mode could leave your MIDI output switched to the built-in FluidSynth if you declined the SoundFont download. Both fixed - see Fixed below.
Full Changelog - Commodore 64 / SID Support
New Features
Phase 42 - SID import - .sid is now an Open format. A Qt-free converter core under src/converter/Sid/ parses the PSID/RSID container (SidFile), and for PSID tunes a from-scratch cycle-stepped 6502 emulator (Mos6502) runs the tune's init + play routines at the C64 frame rate (SidCapture, 50 Hz PAL / 60 Hz NTSC) and snapshots the $D400-$D418 register file every frame. SidReconstruct turns those frames into notes: gate bit → note on/off, oscillator frequency → MIDI pitch (via the tune's clock), ADSR sustain → velocity and the release nibble → a modelled note-release tail, mid-note pitch changes while gated → new notes (arpeggios). Hard-restart artifacts (TEST-bit frames, 1-frame blips) and ring-modulation frames (inharmonic pitch) are filtered so they don't spawn phantom notes. A voice's multiplexed noise drum hits route to a dedicated 4th SID Percussion stream. SidMidiWriter emits a format-1 SMF (conductor + three voice tracks + percussion, voice → MIDI channel) which is loaded straight into the editor.
Authentic RSID import via libsidplayfp - RSID tunes that install their own interrupt player need full C64 hardware emulation, so they are imported through the vendored libsidplayfp 3.0.1 cycle-accurate engine: the engine plays the tune while an adapter captures the per-frame SID register writes, which then feed the same loop-detection → reconstruction → SMF pipeline. PSID tunes keep using the lightweight built-in emulator (fast). The slower RSID render shows an animated progress dialog.
Loop detection - SID tunes have no intrinsic length (they loop forever). SidCapture::detectLoopEnd derives a per-frame musical signature (gate + coarse pitch + waveform class across the three voices, with tolerance for LFO/vibrato jitter), finds the smallest repeating period at the tail and trims the capture to intro + one loop. When no clean loop is found the importer falls back to a configurable window (default 240 s).
C64 SoundFont Mode - a toolbar toggle that plays the converted MIDI with authentic C64 timbres. It remaps the importer's lead/percussion programs onto a C64 SoundFont's waveform presets (pulse / sawtooth / triangle / noise), isolating the C64 SoundFont while active and restoring the previous (General MIDI) selection when switched off (C64SoundFontHelper, mirroring the FFXIV SoundFont mode). FluidSynthEngine tracks each channel's pre-remap program so toggling the mode live re-voices every channel instantly instead of crackling on a single noise preset. Enabling the mode without a C64 SoundFont installed offers an auto-download (Commodore_64.sf2, added to the SoundFont download catalog) and switches the MIDI output to FluidSynth, exactly like FFXIV SoundFont Mode.
Emulation-mode polish - the MIDI Visualizer and the retro time display now animate during authentic SID playback too (they are normally fed by the MIDI player thread, which doesn't run in Emulation mode; both are now driven from the SID position). Switching the engine radio in Settings while C64 is active hands the active state to the chosen engine immediately. The time display now defaults to the Blue LED theme.
Authentic SID playback (Emulation mode) - plays the original .sid through libsidplayfp's engine to a QAudioSink. It is fully transport-controlled: arming is the C64 button (glow = armed), then Play renders from the cursor position, Stop / Pause stop (Pause parks the cursor), and the matrix playback cursor follows the audio in exact real-time sync. Channel mute and track mute mirror onto the three SID voices live (muting a channel, or muting every track that drives a voice, silences that voice; hiding is purely visual). Playback auto-stops at the end of the note roll.
One button, mode in Settings - a single C64 toolbar button triggers whichever engine the Settings → MIDI I/O → Commodore 64 / SID radio selects (SoundFont or Emulation), plus a default SID import length spinbox for the no-loop fallback. Switching the engine while C64 is active hands the active state over to the chosen engine immediately. The button is dark-mode-readable: the original colour Commodore logo on a light chip with an accent glow when active, a legible silhouette when off.
Engine picker, toolbar switch & SoundFont prefetch - a new C64Mode helper is the single source of truth for the engine choice (the Settings radios, the toolbar switch, the first-use picker and the C64 button all route through it, so the handover logic lives in one place and they stay in sync via a modeChanged notifier). On first C64 use a one-time QMessageBox picker (SoundFont / Emulation) is shown and persisted (C64/modeChosen); choosing Emulation also kicks off a silent background download of Commodore_64.sf2 into the soundfonts folder (announced in the picker) so a later switch to SoundFont is instant. New code-drawn C64ModeSwitchWidget (a glowing retro SF2 ⟷ EMU rocker) registers as the c64_mode_switch toolbar action and toggles its own QToolBar action visibility so it only appears while C64 mode is active.
Implementation Notes
libsidplayfp 3.0.1 vendored under third_party/libsidplayfp/ (the self-contained sidlite SID emulator, ROM-free) and built as a static library, with thin Qt-free adapters (SidFpCapture for import register-capture, SidFpPlayer for PCM playback). No new runtime DLL beyond Qt's multimedia plugins.
Real-time audio pacing - the playback renderer carries any per-call frame surplus forward in a leftover buffer instead of discarding it, so no emulated SID-time is ever skipped and the audio plays at exact real time (keeping the real-time cursor locked to the music).
Test Harness
5 new SID unit tests - test_sid_file, test_sid_cpu, test_sid_capture, test_sid_reconstruct, test_sid_midiwriter cover the container parser, the 6502 emulator, the capture loop + loop detection, the note reconstruction heuristics, and the SMF writer. With the MusicXML-export test (below) the full suite is 49/49 green.
src/gui/MidiSettingsWidget.{h,cpp} - the Commodore 64 / SID settings block (engine radio + default import length + immediate engine handover).
src/midi/FluidSynthEngine.{h,cpp} - C64 SoundFont mode + per-channel program remap.
src/gui/MainWindow.{h,cpp}, src/gui/MatrixWidget.cpp, CMakeLists.txt - .sid Open dispatch + progress dialog, transport routing to SID, cursor follow, channel/track → voice mute, Qt6::Multimedia + sidplayfp link.
Added - MusicXML Export (Phase 43)
MIDI → notation engraver - a new score::MidiToScore core (src/converter/Score/) reconstructs notation from flat MIDI: builds the measure grid from the time signature, quantises to a 1/16 + dotted grid, fills rests so every measure is complete, splits notes across barlines into tied notes, groups same-onset notes into chords, picks a clef from each part's median pitch, and spells pitches enharmonically from the key signature (sharps for sharp keys, flats for flat keys). Output is a score::Score IR. The engraver is split into a pure buildScore(ScoreInput) (unit-tested) and a MidiFile-coupled extractInput()/build().
MusicXML writer - MusicXmlWriter (src/converter/MusicXml/) serialises the Score IR to MusicXML 4.0 score-partwise via QXmlStreamWriter: part-list with MIDI instrument hints, per-measure <attributes> (divisions/key/time/clef, emitted only where they change), notes/rests with <type>/<dot>/<tie>/<tied>, <chord/>, and <sound tempo>/metronome directions.
UI - File → Export MusicXML… (MainWindow::exportMusicXml), enabled whenever a file is open, writing a plain-text .musicxml. One writer covers the whole notation ecosystem (MuseScore/Finale/Sibelius/Dorico all import MusicXML), so a dedicated MuseScore writer is unnecessary.
Tests - test_musicxml_export (new) covers note-value mapping, dotted values, ties across barlines, rest filling, chords, enharmonic spelling in sharp/flat keys, and the writer's MusicXML fragments. The engraver core links without the MidiFile tree, so the test stays narrow.
Added - Retro Cursor Time Display (Phase 41)
Phase 41 - Cursor Time Display - new TimeDisplayWidget (src/gui/), a self-sizing toolbar widget with a custom-painted seven-segment renderer: anti-aliased beveled segments, a soft per-segment glow over dim "ghost" segments, a colon that blinks during playback, and a rounded dark bezel with a subtle sheen. Time source is MidiFile::msOfTick(cursorTick()) while idle (refreshed on cursorPositionChanged) and PlayerThread::timeMsChanged during playback (snapped back to the cursor on playerStopped) - the same signals that already drive the lyric timeline / voice lane. Five readouts cycle on left-click (POS / LEN / REM / BPM / BAR); BAR shows bar.beat num/den from measure() + meterAt(), BPM from the last TempoChangeEvent at/before the tick. Right-click cycles six LED colour palettes. Both the readout mode and the colour persist in QSettings (View/timeDisplayMode, View/timeDisplayTheme).
Toolbar integration - registered as the time_display action in LayoutSettingsWidget (available-actions list + every default layout) and built on demand in all four toolbar-build paths, exactly like the MIDI Visualizer. A migration step in MainWindow injects it into existing saved toolbars so it appears (enabled) without a Reset to Default.
Files Modified
src/gui/TimeDisplayWidget.{h,cpp}(new), src/gui/TimeDisplayFormat.h(new) - the widget + its pure, unit-tested format/mode helpers.
src/gui/LayoutSettingsWidget.cpp - time_display added to the available-actions list and all default toolbar layouts.
src/gui/MainWindow.{h,cpp} - widget member + time_display action registration; on-demand instantiation in all toolbar-build paths with playback / cursor signal wiring; setFile() rebind; play() / record() reconnect; migration of existing saved layouts.
tests/test_time_display_format.cpp(new), tests/CMakeLists.txt - the format test executable.
Toolbar widget crash on rebuild - removing a toolbar widget (Cursor Time, MIDI Visualizer, Lyric Visualizer, FFXIV voice gauge, the C64/FFXIV/MCP toggles) via Customize Toolbar, or switching theme, then opening a file or stopping playback could crash: the widgets are destroyed when their toolbar is rebuilt, but only the MIDI-Visualizer pointer was being cleared, leaving the others dangling. All on-demand toolbar pointers are now cleared on every rebuild (MainWindow::nullOnDemandToolbarWidgets).
FFXIV SoundFont Mode left the MIDI output switched on cancel - turning on FFXIV SoundFont Mode without the bard SoundFont installed and then declining the download dialog left the MIDI output switched to the built-in FluidSynth (and persisted) even though the mode wasn't enabled. The previous output device is now restored when the enable is aborted (matching the C64 SoundFont mode behaviour).
Saving an imported file could overwrite the original (data loss) - pressing Save (Ctrl+S) on a file opened from an import-only format (SID, Guitar Pro, MML/3MLE, MusicXML, MuseScore) wrote a Standard MIDI file straight over the original on disk, since MidiFile::save() always emits MIDI bytes. The result destroyed the source file and left a .sid/.gp5/... that no longer re-imported. Save now detects an import-only path and redirects to Save As (pre-filled with .mid) after a one-time notice, so the original is never clobbered; once saved as .mid, Save works normally. (The list of import-only formats is now a single shared helper, also used by the collaboration shared-copy path, which had the same workaround but was missing .sid.)