Skip to content

MeesCode/ABC-parser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ABC Music Notation Parser

A lightweight, embedded-friendly parser for ABC music notation written in C99. Parses ABC notation into note pools with frequency, duration, and MIDI data ready for synthesis.

Note: This project was almost exclusively written by AI (Claude), with human guidance and review.

Features

  • Zero dynamic allocation - uses pre-allocated memory pools
  • Runtime configurable - set note count, voices, and chord size per pool at init time
  • Multi-voice support - parse multiple voices into separate note pools
  • Chord support - [CEG] notation with configurable simultaneous pitches
  • Tuplet support - triplets (3CDE, duplets (2CD, and more (2-9)
  • Repeat unfolding - |: ... :| sections are expanded inline
  • Key signature support - major and minor keys with correct accidentals
  • Tempo with note value - Q:1/4=120 (quarter=120) or Q:1/8=120 (eighth=120)
  • Embedded-ready - no stdlib dependencies except <string.h> and <stdint.h>

Memory Usage

Memory usage depends on your configuration. With 2 voices, 128 notes per voice, 4 notes per chord:

Component Size
Note struct 8 bytes
Sheet struct 96 bytes
NotePool header 40 bytes
Note storage (128 notes) 1,024 bytes
Total (2 voices) ~2.2 KB

Notes store only MIDI note numbers and duration in MIDI ticks (PPQ=48). Frequency, note name, and octave are computed on demand via API functions.

Building

mkdir build && cd build
cmake ..
cmake --build .

Or compile directly:

gcc -O2 -o abcparser main.c abc_parser.c

Usage

Basic (Single Voice)

#include "abc_parser.h"

// Define your limits
#define MAX_VOICES 2
#define MAX_NOTES 128

// Pre-allocate memory (static or global)
static NotePool g_pools[MAX_VOICES];
static struct note g_storage[MAX_VOICES][MAX_NOTES];
static struct sheet g_sheet;

int main(void) {
    // Initialize pools with storage
    for (int i = 0; i < MAX_VOICES; i++) {
        note_pool_init(&g_pools[i], g_storage[i], MAX_NOTES, ABC_MAX_CHORD_NOTES);
    }
    sheet_init(&g_sheet, g_pools, MAX_VOICES);

    // Parse ABC notation
    const char *music = "L:1/4\nK:C\nC D E F | G A B c |";
    int result = abc_parse(&g_sheet, music);

    if (result < 0) {
        // -1: invalid input, -2: pool exhausted
        return 1;
    }

    // Iterate through notes (first voice)
    struct note *n = sheet_first_note(&g_sheet);
    while (n) {
        uint8_t midi = n->midi_note[0];                      // MIDI note number
        float freq = midi_to_frequency_x10(midi) / 10.0f;    // Hz (computed from MIDI)
        uint8_t ticks = n->duration;                         // Duration in MIDI ticks
        uint16_t ms = ticks_to_ms(ticks, g_sheet.tempo_bpm); // Convert to milliseconds

        // Use for synthesis...

        n = note_next(&g_pools[0], n);
    }

    // Reuse memory for another parse
    sheet_reset(&g_sheet);

    return 0;
}

Multi-Voice

const char *music =
    "L:1/8\nK:C\n"
    "V:MELODY\n"
    "C D E F | G A B c |\n"
    "V:BASS\n"
    "C,4 G,4 | C,4 G,4 |";

abc_parse(&g_sheet, music);

// Access each voice
for (int v = 0; v < g_sheet.voice_count; v++) {
    NotePool *pool = &g_pools[v];
    printf("Voice: %s\n", pool->voice_id);

    struct note *n = pool_first_note(pool);
    while (n) {
        // Process notes...
        n = note_next(pool, n);
    }
}

Chords

const char *music = "K:C\n[CEG] [FAc] [GBd]";  // C major, F major, G major chords

abc_parse(&g_sheet, music);

struct note *n = sheet_first_note(&g_sheet);
while (n) {
    printf("Chord with %d notes:\n", n->chord_size);
    for (int i = 0; i < n->chord_size; i++) {
        uint8_t midi = n->midi_note[i];
        printf("  %s%d @ %.1f Hz\n",
               note_name_to_string(midi_to_note_name(midi)),
               midi_to_octave(midi),
               midi_to_frequency_x10(midi) / 10.0f);
    }
    n = note_next(&g_pools[0], n);
}

Different Pool Sizes

You can create pools with different capacities:

// Small pool for simple melodies
static NotePool melody_pool;
static struct note melody_storage[64];
note_pool_init(&melody_pool, melody_storage, 64, 1);  // 64 notes, single notes only

// Large pool for complex compositions
static NotePool complex_pool;
static struct note complex_storage[1024];
note_pool_init(&complex_pool, complex_storage, 1024, 4);  // 1024 notes, up to 4-note chords

Supported ABC Notation

Element Syntax Example
Notes C D E F G A B (octave 4), c d e f g a b (octave 5) C D E F
Octave up/down ' / , c' (octave 6), C, (octave 3)
Sharps ^ ^F (F#)
Flats _ _B (Bb)
Naturals = =F (F natural)
Double sharp/flat ^^ / __ ^^C (C##)
Duration multiplier number after note C2 (double), C4 (quadruple)
Duration fraction / after note C/2 (half), C/ (half), C// (quarter)
Rests z or Z z2 (rest, double length)
Chords [notes] [CEG] (C major chord)
Tuplets (n before notes (3CDE (triplet), (2CD (duplet)
Voices V:id V:MELODY, V:BASS
Repeats |: ... :| |:C D E F:|
Bar lines |, ||, |] C D | E F

Tuplet Reference

Syntax Effect Duration
(2CD Duplet: 2 notes in time of 3 1.5x normal
(3CDE Triplet: 3 notes in time of 2 0.67x normal
(4CDEF Quadruplet: 4 notes in time of 3 0.75x normal
(5... Quintuplet: 5 notes in time of 4 0.8x normal
(6... Sextuplet: 6 notes in time of 2 0.33x normal
(7...-(9... 7-9 notes in time of n-1 varies

Header Fields

Field Description Example
X: Reference number X:1
T: Title T:Greensleeves
C: Composer C:Traditional
M: Meter M:4/4
L: Default note length L:1/8
Q: Tempo (BPM) Q:120 or Q:1/4=120
K: Key signature K:G, K:Amin, K:Bb
V: Voice V:MELODY

Tempo Note Values

The tempo field supports specifying which note value gets the beat:

  • Q:120 - 120 BPM (quarter note assumed)
  • Q:1/4=120 - quarter note = 120 BPM
  • Q:1/8=120 - eighth note = 120 BPM (half the speed of Q:1/4=120)
  • Q:3/8=120 - dotted quarter = 120 BPM

Configuration

Compile-time defines in abc_parser.h (affects struct sizes):

#define ABC_MAX_CHORD_NOTES   4  // Max simultaneous notes in a chord
#define ABC_MAX_TITLE_LEN    32  // Title string buffer
#define ABC_MAX_COMPOSER_LEN 32  // Composer string buffer
#define ABC_MAX_KEY_LEN       8  // Key string buffer
#define ABC_MAX_VOICE_ID_LEN 16  // Voice ID string buffer
#define ABC_PPQ              48  // Pulses per quarter note (MIDI ticks)

Runtime parameters (passed to note_pool_init()):

  • capacity - max notes per pool (no compile-time limit)
  • max_chord_notes - max notes per chord for this pool (clamped to ABC_MAX_CHORD_NOTES)

Data Structures

Note Structure

struct note {
    int16_t next_index;                         // Index of next note (-1 = end)
    uint8_t duration;                           // Duration in MIDI ticks (PPQ=48)
    uint8_t chord_size;                         // Number of notes (1 = single note)
    uint8_t midi_note[ABC_MAX_CHORD_NOTES];     // MIDI note numbers (0-127, 0 = rest)
};

Note properties (frequency, note name, octave) are computed on demand from MIDI values using the utility functions. Duration is stored as tempo-independent MIDI ticks; use ticks_to_ms() to convert to milliseconds.

Note Pool Structure

typedef struct {
    struct note *notes;       // Pointer to note storage (user-provided)
    char voice_id[ABC_MAX_VOICE_ID_LEN];  // Voice identifier
    int16_t head_index;       // First note index
    int16_t tail_index;       // Last note index
    uint16_t count;           // Notes in use
    uint16_t capacity;        // Max notes (from init)
    uint32_t total_ticks;     // Total duration in MIDI ticks
    uint8_t max_chord_notes;  // Max chord size (from init)
} NotePool;

API Reference

Initialization

// Initialize a note pool with external storage
void note_pool_init(NotePool *pool, struct note *buffer, uint16_t capacity, uint8_t max_chord_notes);

// Initialize sheet with array of pools
void sheet_init(struct sheet *s, NotePool *pools, uint8_t pool_count);

// Reset for reuse (clears all pools)
void sheet_reset(struct sheet *s);

Parsing

int abc_parse(struct sheet *s, const char *abc);
// Returns: 0 = success, -1 = invalid input, -2 = pool exhausted

Iteration

struct note *sheet_first_note(const struct sheet *s);              // First note (voice 0)
struct note *pool_first_note(const NotePool *pool);                // First note in pool
struct note *note_next(const NotePool *pool, const struct note *n); // Next note
struct note *note_get(const NotePool *pool, int index);            // Note by index

MIDI Conversion (compute note properties from stored MIDI)

uint16_t midi_to_frequency_x10(uint8_t midi);  // Returns frequency * 10 in Hz
NoteName midi_to_note_name(uint8_t midi);       // Returns note name (C, D, E, etc.)
uint8_t midi_to_octave(uint8_t midi);           // Returns octave (0-10)
int midi_is_rest(uint8_t midi);                 // Returns 1 if rest (midi == 0)

Duration Conversion

uint16_t ticks_to_ms(uint8_t ticks, uint16_t bpm);       // Convert ticks to milliseconds
uint32_t pool_total_ms(const NotePool *pool, uint16_t bpm); // Total pool duration in ms

Other Utilities

float note_to_frequency(NoteName name, int octave, int8_t acc);
int note_to_midi(NoteName name, int octave, int8_t acc);
const char *note_name_to_string(NoteName name);
const char *accidental_to_string(int8_t acc);
int note_pool_available(const NotePool *pool);

Debug

void sheet_print(const struct sheet *s);  // Print sheet to stdout

Testing

Run the test suite:

cd build
ctest
# or
./test_parser

72 tests covering notes, octaves, accidentals, durations, tuplets, rests, key signatures, header fields, repeats, frequencies, MIDI notes, chords, and voices.

License

MIT

About

Parser for ABC music notation targeting embedded platforms

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published