Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
mpe_expression.hpp File Reference

Per-note expression → MIDI 1.0 synthesis shared by the format adapters. More...

#include <pulp/midi/message.hpp>
#include <pulp/midi/mpe_voice_tracker.hpp>
#include <algorithm>
#include <cmath>
#include <cstdint>

Go to the source code of this file.

Namespaces

namespace  pulp
namespace  pulp::format
 What the product grants a device: the quota table indexed by capability tier and live thermal state.
namespace  pulp::format::mpe

Functions

std::uint8_t pulp::format::mpe::to_7bit (double norm) noexcept
 Quantize a normalized [0,1] axis value to a 7-bit MIDI data byte.
midi::MidiEvent pulp::format::mpe::pitch_bend_from_semitones (std::uint8_t channel, double semitones)
 Per-note pitch as a channel-wide 14-bit pitch bend.
midi::MidiEvent pulp::format::mpe::channel_pressure (std::uint8_t channel, double norm)
 Per-note pressure as channel pressure (status 0xD0).
midi::MidiEvent pulp::format::mpe::timbre_cc (std::uint8_t channel, double norm)
 Per-note timbre as CC 74.

Variables

constexpr std::uint8_t pulp::format::mpe::kTimbreController = 74
 MPE timbre controller.

Detailed Description

Per-note expression → MIDI 1.0 synthesis shared by the format adapters.

Every host format carries per-note expression in its own currency — CLAP clap_event_note_expression_t, VST3 kNoteExpressionValueEvent, AU v3 AUParameterEvent ramps — but Pulp's per-note model is MPE: one note per member channel, fed by MpeVoiceTracker from a channel-wide MIDI 1.0 stream. So each adapter decodes its own event struct and then synthesizes the same three channel-wide messages for the same three MPE axes:

  • pitch → 14-bit pitch bend, normalized to the tracker's default member-bend range so the tracker expands it back to the right per-note bend;
  • pressure → channel pressure (status 0xD0);
  • timbre → CC 74, the MPE timbre controller.

The per-format decode (which type ID means which axis, and how a raw host value maps onto the axis) stays in the adapter — the formats genuinely disagree there. What lives here is only the synthesis, which was copy-pasted per adapter with nothing that noticed when a copy drifted.

Scoping. Bridging noteId/key-targeted expression onto channel-wide messages is exact when each note lives on its own MPE member channel (the expressive-controller case MPE is built for); when several notes share one channel the expression collapses to channel-wide. That is a property of the MPE model, not of this mapping.

Real-time contract. Pure functions over scalars; allocation-, lock-, and branch-light. Callers set sample_offset on the returned event themselves, because each format reads the block offset from a different field.