Skip to content

GuitarPrometheus::Midi

GuitarPrometheus::Midi

Classes

Name
structGuitarPrometheus::Midi::ExportConfig
Top-level user-facing render configuration.
structGuitarPrometheus::Midi::HumanizerConfig
Per-part humanizer settings.
structGuitarPrometheus::Midi::InjectionResult
Output of inject().
structGuitarPrometheus::Midi::MidiEvent
Internal tick-space MIDI event produced by KeyswitchInjector and consumed by OctaveTransposer, Humanizer, and MidiWriter.
structGuitarPrometheus::Midi::PipelineRunResult
Aggregate output of a complete pipeline run minus the writer step.
structGuitarPrometheus::Midi::WriteResult
Result of a write call.

Types

Name
enum classOctaveCorrectionMode { Manual, Inferred}
Per-part octave correction strategy.
enum classMidiEventType { NoteOn, NoteOff, ProgramChange, BankSelectMsb, BankSelectLsb, ControlChange, PitchBend, Meta}
Categorizes a [MidiEvent]().

Functions

Name
std::stringgmPatchName(int programByte)
Lookup the General MIDI patch name for a 0-based MIDI program byte (0..127).
std::vector< std::string >humanize(std::map< juce::Uuid, std::vector< MidiEvent > > & events, const ExportConfig & config)
Apply the per-part [HumanizerConfig]() in config.humanizerByPart in place.
InjectionResultinject(const SongStructure::Score & score, const Profiles::Profile & profile, const std::map< juce::Uuid, bool > & stackPickDirectionByPart ={})
Convert a parsed Score and a Profile into tick-space MIDI events per part.
WriteResultwritePerTrack(const std::map< juce::Uuid, std::vector< MidiEvent > > & events, const SongStructure::Score & score, const juce::File & outputDir, const std::optional< Project::TempoOverrides > & tempoOverride =std::nullopt)
Write one SMF Format 0 file per part into outputDir.
WriteResultwriteMerged(const std::map< juce::Uuid, std::vector< MidiEvent > > & events, const SongStructure::Score & score, const juce::File & outputFile, const std::optional< Project::TempoOverrides > & tempoOverride =std::nullopt)
Write one SMF Format 1 file containing every part.
std::vector< std::string >applyOctaveOffsets(std::map< juce::Uuid, std::vector< MidiEvent > > & events, const SongStructure::Score & score, const std::map< juce::Uuid, int > & effectiveOctaveByPart)
Apply per-part octave offsets in place.
intinferOctaveOffset(const SongStructure::Part & part, const MusicModels::KeySignature & key)
Infer a per-octave correction offset for one Part from its tuning + per-note string/fret data.
PipelineRunResultassemblePipelineRun(const SongStructure::Score & score, const std::map< juce::Uuid, Profiles::Profile > & profilesByPart, const Profiles::Profile & defaultProfile, const ExportConfig & exportConfig, const std::optional< Project::TempoOverrides > & tempoOverride =std::nullopt)
Run the full Phase 5/6 pipeline up to (but not including) MIDI writing: inject → applyOctaveOffsets → humanize.

Types Documentation

enum OctaveCorrectionMode

EnumeratorValueDescription
Manual
Inferred

Per-part octave correction strategy.

Mirrors Project::OctaveCorrectionMode.

  • Manual: the integer offset stored in octaveOffsetByPart (or the bound profile’s octaveOffset) is applied as-is.
  • Inferred: OctaveTransposer computes the offset from the part’s tuning + each note’s string / fret data. Useful for bass parts written an octave above sounding pitch.

enum MidiEventType

EnumeratorValueDescription
NoteOn
NoteOff
ProgramChange
BankSelectMsb
BankSelectLsb
ControlChange
PitchBend
Meta

Categorizes a [MidiEvent]().

The injector emits note-on/note-off pairs for both music notes and keyswitches (keyswitches are just notes at profile-defined pitches); program/bank events establish patch at tick 0; CC and pitch-bend are reserved for future effects (humanizer CC output, bend-alter interpolation).

Functions Documentation

function gmPatchName

std::string gmPatchName(
int programByte
)

Lookup the General MIDI patch name for a 0-based MIDI program byte (0..127).

Returns “Unknown” for out-of-range input.

MusicXML’s <midi-program> is 1-based (1..128); convert with programByte = midiProgram - 1 before calling this. The injector already does that conversion when emitting MIDI ProgramChange events.

function humanize

std::vector< std::string > humanize(
std::map< juce::Uuid, std::vector< MidiEvent > > & events,
const ExportConfig & config
)

Apply the per-part [HumanizerConfig]() in config.humanizerByPart in place.

Return: Vector of non-fatal warnings (empty in the common path).

Behavior:

  • Missing entry for a part, or a default-constructed (velocitySigma=0, timingJitterTicks=0) config, leaves that part’s events byte-identical. This is the regression-safe path — Phase 5’s regression test still passes when the humanizer is run with an empty config.
  • Only music notes (sourceNoteId.has_value()) are perturbed. Keyswitches, CC, program/bank changes, and meta events stay untouched so the profile’s expressive control stream remains predictable.
  • Velocity jitter: velocity += round(Gaussian(0, sigma)), clamped to [velocityFloor, velocityCeiling].
  • Timing jitter: Uniform(-timingJitterTicks, +timingJitterTicks) applied identically to a note’s NoteOn and paired NoteOff (same pitch + channel, nearest later tick). Duration is preserved exactly.
  • After jitter, each per-part vector is re-sorted by (tick, priority) because timing jitter can change relative order.

Determinism: a non-zero config.seed produces byte-identical output across runs. seed=0 derives the seed from a hash of the events themselves, so re-running on identical events is still reproducible but different inputs get different jitter patterns.

function inject

InjectionResult inject(
const SongStructure::Score & score,
const Profiles::Profile & profile,
const std::map< juce::Uuid, bool > & stackPickDirectionByPart ={}
)

Convert a parsed Score and a Profile into tick-space MIDI events per part.

The walk is five passes:

  1. ProgramChange + BankSelect (LSB+MSB) at tick 0 for each part.
  2. Per-note NoteOn/NoteOff using Profile::velocityForDynamic for base velocity, with ArticulationMod velocity / length adjustments applied. For drum parts, the pitch comes from Profile::drumMap[scoreInstruments[instrumentRef]] when all three resolve; otherwise the Engraving’s raw display pitch is used.
  3. Sustained techniques (PalmMute, Vibrato, VibratoWTremBar): a single keyswitch NoteOn/NoteOff pair spans every run of consecutive notes that all carry the technique. Absent CC mapping → skipped.
  4. PerNote techniques (Tap, LhTap, directional slides, etc.): fire-and-forget keyswitch at note start.
  5. Span techniques (HammerOn, Slur, Slide, Bend): keyswitch held from TechniqueSpan::startTick to TechniqueSpan::endTick.
  6. Articulation keyswitches (Staccato, Accent, StrongAccent, Grace): fire at note start, release at note end, if the profile maps the articulation.

Identity policy: keyswitch events have sourceNoteId == std::nullopt; music notes carry the producing Engraving’s part-scoped surrogate id (fresh juce::Uuid per note) so downstream stages can distinguish music from control data.

function writePerTrack

WriteResult writePerTrack(
const std::map< juce::Uuid, std::vector< MidiEvent > > & events,
const SongStructure::Score & score,
const juce::File & outputDir,
const std::optional< Project::TempoOverrides > & tempoOverride =std::nullopt
)

Write one SMF Format 0 file per part into outputDir.

Parameters:

  • events Per-part MIDI events keyed by [Part::getId()](/api-reference/classes/classguitarprometheus_1_1songstructure_1_1part/#function-getid). Produced by KeyswitchInjector::inject and possibly post-processed by OctaveTransposer / Humanizer.
  • score The score the events were derived from. Provides PPQN, tempo timeline, time signature, and part ordering.
  • outputDir Directory to write per-part .mid files into. Created if missing.
  • tempoOverride Optional (measure, tickWithinMeasure) -> BPM map. When non-empty, replaces Score::getTempoByMeasure() for tempo emission; the Score itself is untouched. Empty / nullopt falls back to the Score’s tempos.

File names are {zero-padded-index}-{sanitized-part-name}.mid, preserving the Score’s part order. Each file carries the score’s PPQN, one tempo event and one time-signature event at tick 0, and the part’s events.

outputDir is created if it doesn’t already exist.

function writeMerged

WriteResult writeMerged(
const std::map< juce::Uuid, std::vector< MidiEvent > > & events,
const SongStructure::Score & score,
const juce::File & outputFile,
const std::optional< Project::TempoOverrides > & tempoOverride =std::nullopt
)

Write one SMF Format 1 file containing every part.

Parameters:

  • events Per-part MIDI events keyed by [Part::getId()](/api-reference/classes/classguitarprometheus_1_1songstructure_1_1part/#function-getid). Produced by KeyswitchInjector::inject and possibly post-processed by OctaveTransposer / Humanizer.
  • score The score the events were derived from. Provides PPQN, tempo timeline, time signature, and part ordering.
  • outputFile Target .mid file. Parent directory is created if missing.
  • tempoOverride Optional (measure, tickWithinMeasure) -> BPM map. When non-empty, replaces Score::getTempoByMeasure() for tempo emission; the Score itself is untouched. Empty / nullopt falls back to the Score’s tempos.

Track 0 carries PPQN, tempo, and time-signature meta events at tick 0. Tracks 1..N hold the parts in [Score::getParts()](/api-reference/classes/classguitarprometheus_1_1songstructure_1_1score/#function-getparts) order, each preceded by a track-name meta event.

function applyOctaveOffsets

std::vector< std::string > applyOctaveOffsets(
std::map< juce::Uuid, std::vector< MidiEvent > > & events,
const SongStructure::Score & score,
const std::map< juce::Uuid, int > & effectiveOctaveByPart
)

Apply per-part octave offsets in place.

Return: Vector of non-fatal warnings (empty if every transposition lands cleanly inside the MIDI range).

effectiveOctaveByPart is a flat scorePartId → octaveOffset map computed upstream (in PipelineOrchestration via AppController::effectiveOctaveForPart) that resolves the precedence between per-part overrides ([ExportConfig::octaveOffsetByPart](/api-reference/classes/structguitarprometheus_1_1midi_1_1exportconfig/#variable-octaveoffsetbypart)), profile-level defaults ([Profile::getOctaveOffset](/api-reference/classes/classguitarprometheus_1_1profiles_1_1profile/#function-getoctaveoffset)), and the default of 0. Keeping the resolver outside this function lets the transposer stay format-agnostic and easily unit-testable.

Only events whose sourceNoteId.has_value() (music notes — NoteOn/NoteOff pairs produced by KeyswitchInjector) are transposed. Keyswitches, program/bank events, and CC / pitch-bend events are preserved byte-identical because they live in the profile’s CC-number space, not the pitch space.

Drum parts — detected via [SongStructure::Part::isDrumPart()](/api-reference/classes/classguitarprometheus_1_1songstructure_1_1part/#function-isdrumpart) — are skipped regardless of the configured offset, because drum pitches are specific drum-piece selectors in GM (36 = Kick, 38 = Snare, …); shifting them by octaves turns “kick drum” into “nothing recognizable”.

Post-clamp pitch is bounded to [0, 127]. Every clamp produces one warning message with the part id and the clamp direction.

function inferOctaveOffset

int inferOctaveOffset(
const SongStructure::Part & part,
const MusicModels::KeySignature & key
)

Infer a per-octave correction offset for one Part from its tuning + per-note string/fret data.

For each Engraving with getGuitarString().has_value() and [Part::getTuningMidiPitches()](/api-reference/classes/classguitarprometheus_1_1songstructure_1_1part/#function-gettuningmidipitches) non-empty, compute the expected sounding pitch as tuning[tuning.size() - guitarString] + guitarFret and compare to Engraving::getMidiNoteNumber(key). The dominant delta (rounded to the nearest multiple of 12) is returned as the inferred offset. Returns 0 if the part lacks tuning data, lacks per-note string/fret tags, or the deltas don’t form a consistent octave-multiple bias.

Baritone-in-standard vs regular-guitar-in-drop-tuning yield the same tuning[] and can’t be disambiguated — for those cases the user should stay in Manual mode.

function assemblePipelineRun

PipelineRunResult assemblePipelineRun(
const SongStructure::Score & score,
const std::map< juce::Uuid, Profiles::Profile > & profilesByPart,
const Profiles::Profile & defaultProfile,
const ExportConfig & exportConfig,
const std::optional< Project::TempoOverrides > & tempoOverride =std::nullopt
)

Run the full Phase 5/6 pipeline up to (but not including) MIDI writing: inject → applyOctaveOffsets → humanize.

Pure function so callers can unit-test the orchestration without instantiating widgets. Profiles are passed in as a per-Part map; missing entries fall through to defaultProfile (typically an empty profile that just produces raw MIDI without keyswitches).

tempoOverride is currently unused (the writer reads tempo straight from Score), but accepted as a parameter so F6 can plug in tempo edits without changing this signature.


Updated on 2026-05-29 at 07:39:52 +0000