Skip to content

gramster/md2electraone

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

md2electraone

md2electraone is a Python tool that converts between Markdown documents and Electra One preset JSON files.

Instead of hand-building Electra presets, you write (or reuse) a clean, readable Markdown spec of CC/NRPN mappings — and this tool turns it into an importable Electra One preset JSON. You can also convert existing Electra One presets back to Markdown for documentation or editing.

Example input specs live in the specs/ folder.


What this does (and why)

  • ✔ Convert Markdown tables → Electra One preset JSON
  • ✔ Import midi.guide CSV files → Electra One preset JSON
  • ✔ Convert Electra One preset JSON → Markdown (reverse conversion)
  • ✔ Enforce consistent layout and labeling
  • ✔ Make MIDI implementations readable and executable
  • ✔ Enable rapid iteration and sharing of controller mappings

I prefer this approach to designing a preset in a GUI editor, as it is easier to audit, make quick bulk rearrangements, and potentially use the markdown doc elsewhere.

A Note about Group Labels

For some reason, the JSON generated by this app can get modified in when imported into the Electra One editor. I have seen groups get their bounding boxes switched with other groups.

Tracking this here: #2

This has been confirmed as a bug in Electra One's editor, and is apparently being fixed (perhaps it is fixed by the time you read this).


Quick Start

If you already use Python:

python3 -m venv .venv
source .venv/bin/activate      # macOS / Linux
# .venv\Scripts\activate       # Windows

pip install -e .

Then:

python3 -m md2electraone specs/ndlr2.md \
  -o NDLR_ElectraOne_Preset.json \
  --debug

Upload the generated JSON to the Electra One web editor and sync it to your device.


Setting up Python (beginner-friendly)

If you’re not especially tech-savvy, follow this step by step:

  1. Install Python
    Download from https://python.org (Python 3.12+ required).

  2. Download this repository

  3. Open a terminal / shell

    • macOS: Terminal
    • Windows: PowerShell
    • Linux: your terminal of choice
  4. Change into the project folder

    cd md2electraone
  5. Create and activate a virtual environment

    python3 -m venv .venv

    Activate it:

    • macOS / Linux:
      source .venv/bin/activate
    • Windows:
      .venv\Scripts\activate
  6. Install the tool

    python3 -m ensurepip
    python3 -m pip install -e .

After this, the commands below should work as long as you run that within that folder. If you need to use the tool again later, change to the folder and run the activate line only first. If you want to get the latest update first:

  1. Update the tool
    git pull
    python3 -m pip install -e .

If this seems like a big hassle to you, go vote on #1


Usage

Markdown → JSON (Generate preset)

Generate preset JSON only:

python3 -m md2electraone specs/ndlr2.md \
  -o NDLR_ElectraOne_Preset.json \
  --debug

Generate preset JSON and cleaned Markdown:

python3 -m md2electraone specs/ndlr2.md \
  -o NDLR_ElectraOne_Preset.json \
  --clean-md NDLR_MIDI.cleaned.md \
  --debug

The cleaned Markdown is useful if your source spec is messy or inconsistent.

midi.guide CSV → JSON

Import a https://midi.guide contributor CSV directly:

python3 -m md2electraone midi.guide.template.csv \
  -o preset.json \
  --clean-md imported.md

The importer maps midi.guide sections to Markdown sections, preserves manufacturer and device metadata, and converts usage notes into Electra One choices when the usage definition is discrete. If a midi.guide row defines both CC and NRPN access for the same parameter, the importer uses the CC.

JSON → Markdown (Reverse conversion)

Convert an Electra One preset JSON back to Markdown:

python3 -m md2electraone NDLR_ElectraOne_Preset.json \
  --to-markdown \
  -o NDLR_spec.md \
  --debug

This is useful for:

  • Documenting existing presets
  • Extracting MIDI implementation from presets
  • Editing presets in Markdown format
  • Sharing preset specifications in a readable format

Note: The reverse conversion supports the subset of Electra One features that md2electraone can generate (7-bit CC messages, faders, lists, and pads). Any unsupported features will trigger warnings on stderr and will be dropped from the output.

JSON output formatting

By default, the generated JSON is minified (compact, no whitespace) for optimal file size. For debugging or readability, use the --pretty flag to format the JSON with indentation:

python3 -m md2electraone specs/ndlr2.md \
  -o NDLR_ElectraOne_Preset.json \
  --pretty

Note: The Electra One accepts both minified and pretty-printed JSON, so use whichever format suits your workflow.


Markdown Format

Each Markdown file should consist of:

  • Optional YAML frontmatter (for metadata)
  • Section headers (## Pad, ## Drone, etc.)
  • Followed by Markdown tables

Optional frontmatter

You can include YAML frontmatter at the start of your Markdown file to specify device metadata:

---
name: Moog Subsequent 37        # Device name (used in preset and devices array)
version: 2                       # Preset version (default: 2)
port: 1                          # MIDI port (default: 1)
channel: 5                       # MIDI channel (default: 1)
manufacturer: Moog Music         # Manufacturer (informational)
groups: highlighted              # Group variant (e.g., "highlighted")
---

All fields are optional. If not specified, defaults will be used.

Multi-Device Support

For presets that control multiple devices (e.g., two synths on different MIDI channels), you can specify multiple devices in the frontmatter:

---
devices:
  - name: Synth A
    port: 1
    channel: 1
  - name: Synth B
    port: 1
    channel: 2
---

When using multiple devices, you have three options for specifying which device a control belongs to:

  1. Device prefix in Control column: deviceIndex:ccNumber (e.g., 1:10, 2:42)
  2. Device declaration at page level: device: Device Name before the table
  3. Device expansion with <device> token (see below)
| Control (Dec) | Label | Range | Choices | Color  |
|---------------|-------|-------|---------|--------|
| 1:10 | Synth A Filter | 0-127 |         | FF0000 |
| 2:10 | Synth B Filter | 0-127 |         | 00FF00 |
| 1:20 | Synth A Res    | 0-127 |         | FF0000 |
| 2:20 | Synth B Res    | 0-127 |         | 00FF00 |

Device prefix syntax:

  • Format: deviceIndex:ccNumber (e.g., 1:10, 2:42)
  • Device indices are 1-based and correspond to the order in the devices list
  • Device prefixes are only required when you have multiple devices
  • For single-device presets, no prefix is needed (backward compatible)
  • Device prefixes work with all message types (e.g., 2:N100 for NRPN on device 2)

Device declaration syntax:

You can declare a device for an entire page by adding device: Device Name before the table:

## Part 1 Controls

device: Synth A

| Control (Dec) | Label | Range | Choices | Color  |
|---------------|-------|-------|---------|--------|
| 10 | Filter | 0-127 |         | FF0000 |
| 20 | Resonance | 0-127 |         | FF0000 |

All controls in that section will use the specified device (unless they have an explicit device prefix).

Device Expansion with <device> Token

For devices with multiple identical parts (e.g., a 6-voice synth where each voice has the same controls), you can use the <device> token to avoid duplication:

---
name: Redshift 6
devices:
  device count: 6
  - name: Redshift 6 Part <device>
    port: 1
    channel: <device>
  - name: Redshift 6 Global
    port: 1
    channel: 15
    id: 7
---

## Part <device>: Waveform

device: Redshift 6 Part <device>

| Control (Dec) | Label | Range | Choices | Color  |
|---------------|-------|-------|---------|--------|
| N:128 | Osc 1 Wave | 0-4 | Saw, Pulse, Saw R, Pulse R, None | |
| N:129 | Osc 2 Wave | 0-4 | Saw, Pulse, Saw R, Pulse R, None | |

This will automatically expand to:

  • 6 device entries (Redshift 6 Part 1 through Part 6, plus Global)
  • 6 sections (Part 1: Waveform through Part 6: Waveform)
  • Each section's controls will be assigned to the corresponding device

Device expansion features:

  • device count: N specifies how many times to expand <device> tokens
  • <device> in device names and channels gets replaced with 1, 2, 3, etc.
  • <device> in section headers causes the section to be duplicated N times
  • Consecutive sections with <device> are grouped and expanded together
  • device: Name <device> declarations are expanded and mapped to device IDs
  • Explicit id: fields in device entries are preserved and checked for conflicts
  • Use --expand-only flag to see the expanded markdown for debugging

Requirements:

  • PyYAML is required for proper YAML parsing of complex frontmatter (automatically installed with pip install -e .)

Group variants:

  • The groups field sets the visual variant for all group labels in the preset
  • Common values: highlighted (makes group labels more prominent)
  • This is applied to all groups in the preset

Required table columns

Column Description
Control (Dec) MIDI CC/NRPN number in decimal, or group identifier. Optional prefix: C for CC (default), N for NRPN, S for SysEx (future). For envelope controls, use comma-separated CC numbers (e.g. 1,2,3,4 for ADSR). Hexadecimal format also supported (e.g. 0x10).
Label Label shown on the Electra One control, or group display name
Range Numeric range (e.g. 0-127 for 7-bit, 0-16383 for 14-bit). Optional default value in parentheses (e.g. 0-127 (64)). If not specified, defaults to 0 if in range, otherwise the minimum value. For groups: number of contiguous controls (optional).
Choices For lists/buttons: comma-separated labels. If needed, specify values in parentheses (Minor(2)). For envelope controls: ADSR or ADR.
Color RGB hex color (e.g. #FF8800). Persists until changed. Can be left empty. For groups: inherited by all group members that don't have an explicit color.

Groups

You can organize controls into labeled groups that appear as headers above the controls. Groups use an internal identifier (in the CC column) and a display label (in the Label column). This allows multiple groups to have the same display label while maintaining unique identifiers.

Range-based groups (contiguous controls)

For groups where all controls are contiguous in the top row:

| Control (Dec) | Label      | Range | Choices | Color   |
|---------------|------------|-------|---------|---------|
| osc           | OSCILLATOR | 3     |         | #FF0000 |
| 10            | Waveform   | 0-3   | Sine,Tri,Saw,Square | |
| 11            | Octave     | -2-2  |         |         |
| 12            | Detune     | 0-127 |         |         |

Explicit group membership (multi-row or non-contiguous)

For groups that span multiple rows or have non-contiguous controls, use the groupid: prefix:

| Control (Dec) | Label                  | Range | Choices | Color |
|---------------|------------------------|-------|---------|-------|
| osc           | OSCILLATOR             |       |         |       |
| 10            | osc: Waveform          | 0-3   | Sine,Tri,Saw,Square | |
| 11            | osc: Octave            | -2-2  |         |       |
| 12            | Filter Cutoff          | 0-127 |         |       |
| 13            | osc: Detune            | 0-127 |         |       |
| 14            | Filter Resonance       | 0-127 |         |       |
| 15            | osc: Level             | 0-127 |         |       |

Group syntax:

  • Use an internal group identifier (e.g., osc, grp1, target1) in the Control column to define a group
  • The Label column specifies the display label shown on the Electra One
  • The Range column (optional) specifies how many controls in the top row belong to this group
    • If blank, use explicit groupid: prefixes on control labels
    • If specified, the next N controls are automatically assigned to the group
  • The Color column (optional) sets the group label color and is inherited by all group members
    • Group members without an explicit color will inherit the group's color
    • Group members with an explicit color will use their own color (overrides group color)
  • Group labels are positioned above the controls, and the group bounding box surrounds all controls in the group

Note: The internal group identifier must be a valid identifier (letters, numbers, underscores; must start with a letter or underscore). This allows you to have multiple groups with the same display label (e.g., "TARGET") by using different identifiers (e.g., target1, target2).

Backward compatibility: The old format using Group in the Control column is still supported, but the new format with explicit group identifiers is recommended.

Groups are purely visual organizational elements - they don't affect MIDI functionality.

Group color inheritance example

| Control (Dec) | Label                  | Range | Choices | Color   |
|---------------|------------------------|-------|---------|---------|
| osc           | OSCILLATOR             |       |         | #FF0000 |
| 10            | osc: Waveform          | 0-3   | Sine,Tri,Saw,Square | |
| 11            | osc: Octave            | -2-2  |         |         |
| 12            | Filter Cutoff          | 0-127 |         | #00FF00 |
| 13            | osc: Detune            | 0-127 |         | #0000FF |
| 14            | Filter Resonance       | 0-127 |         |         |
| 15            | osc: Level             | 0-127 |         |         |

In this example:

  • Waveform and Octave inherit the group color #FF0000 (no explicit color)
  • Filter Cutoff has its own color #00FF00 (not in the group)
  • Detune has an explicit color #0000FF that overrides the group color
  • Filter Resonance inherits #0000FF from the previous row (standard color persistence)
  • Level inherits the group color #FF0000 (no explicit color)

Message Type Prefixes

The Control column supports optional prefixes to specify the MIDI message type:

  • C: or c: CC message (default if no prefix)
    • Automatically uses 7-bit (cc7) if range ≤ 127
    • Automatically uses 14-bit (cc14) if range > 127
  • N: or n: NRPN message (always 14-bit)
  • P:: prgram change message
  • S: or s: SysEx message (future support)

Examples:

  • 10 or C:10 → 7-bit CC #10 (if range ≤ 127)
  • 20 → 14-bit CC #20 (if range > 127)
  • N:100 → NRPN #100
  • 2:N:100 → NRPN #100 on device 2
  • 0x1A → 7-bit CC #26 (hex notation)

Default Values

You can specify initial default values for controls by adding them in parentheses after the range:

| Control (Dec) | Label  | Range       | Choices             | Color |
|---------------|--------|-------------|---------------------|-------|
| 1             | Volume | 0-127 (64)  |                     |       |
| 2             | Filter | 20-100 (50) |                     |       |
| 3             | Mode   | 0-3 (1)     | Off, Low, Med, High |       |

Default value behavior:

  • If specified (e.g., 0-127 (64)), that value is used as the initial value
  • If not specified, the default is 0 if 0 is within the range, otherwise the minimum value
  • Default values are included in the JSON defaultValue field and used in startup messages
  • When converting JSON back to Markdown, default values are only shown if they differ from the automatic default

Layout notes

  • Blank rows in tables create blank spaces in the Electra layout
  • Color values persist until overridden
  • Section boundaries define logical control groupings

See the specs/ directory for complete, working examples.


Supported Control Types

md2electraone supports the following Electra One control types:

  • Faders - Continuous value controls (default for controls without choices)
  • Lists - Multi-valued selection controls (when choices are specified)
  • Pads - Toggle buttons for on/off controls (when exactly 2 choices with on/off semantics)
  • ADSR Envelopes - Attack, Decay, Sustain, Release envelope controls (specify ADSR in Choices column with 4 comma-separated CCs)
  • ADR Envelopes - Attack, Decay, Release envelope controls (specify ADR in Choices column with 3 comma-separated CCs)

Control Modes

Control modes are automatically inferred from the control's characteristics:

  • Unipolar - Default mode for faders with non-negative ranges (e.g., 0-127)
  • Bipolar - Automatically applied to faders with negative minimum values (e.g., -64-63)
  • Toggle - Automatically applied to 2-choice controls with on/off semantics (e.g., Off, On)
  • Momentary - Automatically applied to 2-choice controls with momentary semantics (e.g., Released, Momentary)
  • Default - Used for list controls and envelopes

The mode is inferred during conversion and preserved in roundtrip conversions (Markdown → JSON → Markdown).

Control Mode Examples

| Control (Dec) | Label | Range | Choices | Color |
|---------------|-------|-------|---------|-------|
| 1 | Volume | 0-127 | | |
| 2 | Pan | -64-63 | | |
| 3 | Mute | 0-127 | Off, On | |
| 4 | Sustain Pedal | 0-127 | Released, Momentary | |
| 5 | Waveform | 0-3 | Sine, Triangle, Saw, Square | |

This creates:

  • Volume: Unipolar fader (0-127)
  • Pan: Bipolar fader (-64 to 63, automatically inferred from negative min)
  • Mute: Toggle pad (Off/On)
  • Sustain Pedal: Momentary pad (automatically inferred from Released/Momentary labels)
  • Waveform: List control with 4 options

Envelope Control Example

| Control (Dec) | Label  | Range | Choices | Color |
|---------------|--------|-------|---------|-------|
| 1,2,3,4       | Filter | 0-127 | ADSR    |       |
| 5,6,7         | Amp    | 0-127 | ADR     |       |

Envelope controls automatically span 2 grid positions and create the appropriate multi-value structure with inputs mapped to the envelope components.


Current Limitations

Markdown → JSON conversion:

  • SysEx messages not yet supported (CC7, CC14, and NRPN are supported)
  • One-way output only (does not read or sync from instruments)

JSON → Markdown conversion:

  • Only supports the subset of Electra One features that md2electraone can generate
  • Unsupported features (SysEx, etc.) will be dropped with warnings
  • Control positioning information is lost (regenerated based on page order)

Testing

The project includes a comprehensive test suite using pytest.

Running Tests

To run all tests:

pytest tests/ -v

To run specific test files:

pytest tests/test_parser.py -v
pytest tests/test_roundtrip.py -v
pytest tests/test_json2md.py -v

Test Coverage

The test suite includes:

  • Parser tests (tests/test_parser.py) - Test markdown parsing functionality including:

    • CC number parsing (decimal, hex, NRPN)
    • Range parsing with default values
    • Choices/options parsing
    • Color parsing
    • Frontmatter parsing
    • Control mode inference
    • Group color inheritance
  • Roundtrip tests (tests/test_roundtrip.py) - Test MD → JSON → MD conversions preserve:

    • Default values
    • Message types (CC7, CC14, NRPN)
    • Control modes (bipolar, toggle, momentary)
    • Group structures
    • Blank rows
  • JSON to Markdown tests (tests/test_json2md.py) - Test JSON → MD conversion:

    • Simple presets
    • Presets with groups
    • NRPN messages
    • Control count preservation

Installing Test Dependencies

If you haven't already installed pytest:

pip install pytest

Or install the project in development mode (recommended):

pip install -e .

Contributing

Contributions are welcome!

  • New Markdown specs
  • Bug reports
  • Improvements to layout rules or parsing
  • New tests for edge cases

About

A Python program to convert a Markdown document describing a MIDI device to a preset for the Electra One. Also supports CSV files from https://midi.guide

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages