Skip to content

Architecture Overview

Architecture Overview

Guitar Prometheus is a single-binary desktop app (JUCE 8) that imports Guitar Pro scores from MusicXML, native .gp (Guitar Pro 7 / 8), and native .gpx (Guitar Pro 6) sources, and produces MIDI files with keyswitches injected so VST instruments render guitar techniques realistically.

Import Pipeline

The import side fans into the same shared GPIF reader for both native Guitar Pro variants, then funnels every format through a common intermediate score and the final builder.

---
config:
  theme: base
  flowchart:
    curve: linear
  themeVariables:
    lineColor: "#c8a0b0"
    primaryBorderColor: "#c8a0b0"
    edgeLabelBackground: "#808080"
    textColor: "#777777"
    titleColor: "#777777"
---
flowchart LR
    subgraph Sources["Source files"]
        direction TB
        MX["MusicXML<br/>(.musicxml / .xml)"]:::blue
        GP["GP7 / 8 native<br/>(.gp)"]:::green
        GPX["GP6 native<br/>(.gpx)"]:::amber
    end

    Dispatcher["ScoreImporter<br/>dispatch"]:::slate

    subgraph Parsers["Format parsers"]
        direction TB
        MXP["MusicXmlParser"]:::blue
        GPP["GpFileParser<br/>(ZIP container)"]:::green
        GPXP["GpxFileParser<br/>(BCFZ / BCFS)"]:::amber
    end

    Gpif["GpifReader<br/>(shared GPIF XML)"]:::teal
    IR["IntermediateScore"]:::purple
    Builder["ScoreBuilder::build"]:::rose
    Score[("Score<br/>(immutable)")]:::slate

    MX --> Dispatcher
    GP --> Dispatcher
    GPX --> Dispatcher

    Dispatcher --> MXP
    Dispatcher --> GPP
    Dispatcher --> GPXP

    MXP --> IR
    GPP --> Gpif
    GPXP --> Gpif
    Gpif --> IR

    IR --> Builder
    Builder --> Score

    classDef blue fill:#4A90D9,stroke:#3570B0
    classDef green fill:#4EA882,stroke:#308862
    classDef amber fill:#C8A040,stroke:#A88020
    classDef teal fill:#48A8A0,stroke:#288888
    classDef purple fill:#7B68EE,stroke:#5B48CE
    classDef rose fill:#C86888,stroke:#A84868
    classDef slate fill:#808898,stroke:#606878

    style Sources fill:#88888814,stroke:#888888
    style Parsers fill:#4A90D914,stroke:#4A90D9

    linkStyle 0 stroke:#4A90D9
    linkStyle 1 stroke:#4EA882
    linkStyle 2 stroke:#C8A040
    linkStyle 3 stroke:#4A90D9
    linkStyle 4 stroke:#4EA882
    linkStyle 5 stroke:#C8A040
    linkStyle 6 stroke:#4A90D9
    linkStyle 7 stroke:#4EA882
    linkStyle 8 stroke:#C8A040
    linkStyle 9 stroke:#48A8A0
    linkStyle 10 stroke:#7B68EE
    linkStyle 11 stroke:#C86888

IScoreImporter is the abstract entry point; ScoreImporter::importFile dispatches by file extension. Every concrete parser emits an IntermediateScore (parser-internal IR with public fields), which ScoreBuilder::build finalises into the immutable Score — minting per-Part UUIDs and deriving cached tempo scalars from the score-wide timeline.

Export Pipeline

Once a Score exists, the export side composes pure stages over per-part event streams.

---
config:
  theme: base
  flowchart:
    curve: linear
  themeVariables:
    lineColor: "#c8a0b0"
    primaryBorderColor: "#c8a0b0"
    edgeLabelBackground: "#808080"
    textColor: "#777777"
    titleColor: "#777777"
---
flowchart LR
    Score[("Score")]:::slate
    Profile[("Profile<br/>per part")]:::purple
    ExportCfg[("ExportConfig")]:::amber

    Inject["KeyswitchInjector<br/>::inject"]:::blue
    Transpose["OctaveTransposer<br/>::applyOctaveOffsets"]:::green
    Humanize["Humanizer<br/>::humanize"]:::rose
    Write["MidiWriter<br/>::writeMerged /<br/>::writePerTrack"]:::teal
    Mid[("*.mid")]:::slate

    Score --> Inject
    Profile --> Inject
    Inject --> Transpose
    ExportCfg --> Transpose
    Transpose --> Humanize
    ExportCfg --> Humanize
    Humanize --> Write
    Write --> Mid

    classDef blue fill:#4A90D9,stroke:#3570B0
    classDef green fill:#4EA882,stroke:#308862
    classDef amber fill:#C8A040,stroke:#A88020
    classDef teal fill:#48A8A0,stroke:#288888
    classDef purple fill:#7B68EE,stroke:#5B48CE
    classDef rose fill:#C86888,stroke:#A84868
    classDef slate fill:#808898,stroke:#606878

    linkStyle 0 stroke:#808898
    linkStyle 1 stroke:#7B68EE
    linkStyle 2 stroke:#4A90D9
    linkStyle 3 stroke:#C8A040
    linkStyle 4 stroke:#4EA882
    linkStyle 5 stroke:#C8A040
    linkStyle 6 stroke:#C86888
    linkStyle 7 stroke:#48A8A0

Each stage is a pure function with explicit inputs and outputs; the orchestrator assemblePipelineRun threads per-part state through them. Profile humanizer settings overlay onto explicit per-part humanizer overrides before Humanizer::humanize runs.

Core Domains

Parser

Source: src/public/parser/, src/private/parser/

  • MusicXmlParser reads MusicXML via pugixml. Two walker classes split the ~490-line walk: PartWalker owns measure-spanning state (divisions, time / key, tempo, transpose, open spans) while MeasureBodyWalker owns the within-measure cursor and span emission.
  • GpFileParser opens .gp as a ZIP, extracts Content/score.gpif, and hands it to GpifReader.
  • GpxFileParser decodes the .gpx BCFZ-compressed BCFS filesystem via GpxFileSystem, extracts score.gpif, then GpifReader.
  • GpifReader walks the shared GPIF XML schema. Three walker classes split the ~480-line read: GpifTempoReader for the <MasterTrack><Automations> timeline, GpifTrackReader for per-Track metadata + master-bar orchestration, GpifBeatWalker for the voice / beat / note traversal.

Detection rules cover standard MusicXML elements (<palm-mute/>, <tap/>), articulation containers, Guitar Pro processing instructions (<?GP <root>…</root>?>), the F9.5 <other-technical type="X"/> escape hatch, the GP slide-flag bitfield (including the 0x40 / 0x80 pick-scrape encoding), and the bit-0x01 AutoSlide variant introduced in Phase 22.

Phase 24 added ParserUtilities::xmlDepthWithin — every loaded document is checked against a 256-level nesting cap before any walker touches it. Pathologically nested payloads surface a Diagnostic instead of blowing the stack during recursive descent.

Profiles

Source: src/public/profile/

A Profile is the VST keyswitch contract: per-technique keyswitch notes, per-articulation keyswitch notes, articulation modifiers (velocity boost + length multiplier), per-dynamic velocity, an optional drum-piece map, and optional humanizer settings (Phase 19).

Profiles serialise to JSON and live in the user’s persistence directory (Util::getPersistenceRoot()). The ProfileStore handles load / save / delete plus import / export to file, clipboard, and zip archives.

Project

Source: src/public/project/

A Project is a single user session: the embedded source payload, per-track profile bindings (scorePartId → profileUuid), tempo overrides, export config (humanizer + octave offset per part, stack pick direction), and a list of merged-in supplementary sources. Persisted as .gpproj JSON via ProjectStore.

Source format is captured in the project (SourceFormat::MusicXml | Gp | Gpx) so the project can re-materialise its score across re-opens — Phase 23 wired ScoreImporter::importMemory so all three formats round-trip through the project file. The schema migration framework (F9.1) lifts older .gpproj files to the current schema version on load.

Phase 22 added a BendPoint / BendCurve model on Engraving (populated by the GP parser from the BendOriginValue / BendMiddleValue / BendDestinationValue properties), and an OctaveCorrectionMode enum (Manual / Inferred) on ExportConfig. When Inferred is set, OctaveTransposer::inferOctaveOffset derives the per-part shift from the part’s tuning plus each note’s string / fret tags.

Pipeline (MIDI emission)

Source: src/public/midi/

Three stages composed by assemblePipelineRun:

  1. KeyswitchInjector::inject produces per-part MidiEvent streams. Iterates the score, emits NoteOn / NoteOff for every Engraving, then inserts keyswitch NoteOn / NoteOff pairs around runs of techniques (sustained), individual notes (per-note), or span ranges.
  2. OctaveTransposer::applyOctaveOffsets shifts each part’s MIDI pitches by the resolved per-part octave offset (override > profile default > 0).
  3. Humanizer::humanize adds Gaussian-jittered velocity + uniform timing noise. Per-part overrides win over the profile’s humanizer settings; when neither is set the engine is a no-op.

The result is fed to MidiWriter::writePerTrack (one Format 0 file per part) or MidiWriter::writeMerged (single Format 1 file with a meta track plus one track per part).

UI shell

Source: src/private/main/

JUCE-native UI rooted at MainWindow. Owns an AppController that holds the active Project + Score and broadcasts changes to listeners. Components: TrackListPanel (per-part profile picker), ProfileEditorPanel (per-profile editing, including the Phase 19 humanizer controls), TempoMapPanel (per-measure / sub-beat tempo overrides), ExportDialog (export config + run), and UnknownTechniquesBanner (warns when the parser sees something it doesn’t know how to handle).

Cross-platform persistence

Source: src/public/util/PersistenceRoot.h

Util::getPersistenceRoot() returns the directory profiles and prefs live in:

  • Windows: the executable’s parent directory (portable mode — the app ships as an extracted ZIP and stores config next to itself).
  • macOS / Linux: ~/Library/Application Support/Guitar Prometheus (or the XDG equivalent).

Key Design Principles

  • ImmutabilityProfile, Project, Score, and the engraving model are constructor-only. Mutators rebuild rather than edit in place.
  • Constructor-sink — collections are passed by value and std::moved into the field, so call sites read top-to-bottom and never share state with the model.
  • Pure functional core — parser, injector, transposer, humanizer, writer are pure functions taking inputs by const-ref and returning results. The UI layer is the only one with mutable state.
  • Schema-driven persistenceProfile and Project JSON formats have versioned schemas. Old documents migrate forward via the F9.1 framework; the schema literal in code matches a resource file byte-for-byte (enforced by tests).
  • Out-parameter purge — Phase 19 removed multi-out-parameter functions in favour of return-by-value structs. New code follows the same pattern; accumulator helpers (e.g. walk-and-add) are the only exception.