Skip to content

Commit a557785

Browse files
committed
Add MIDI file decoder per MIDI 1.0 specification
This implements Standard MIDI File (SMF) parser supporting format 0/1: - Parse MThd header (format, tracks, division/SMPTE timing) - Parse MTrk track chunks with variable-length quantities - Handle running status for channel messages - Support meta events (tempo, end of track) and SysEx - Convert ticks to milliseconds/samples with tempo tracking New tools: - midiparse: Convert .mid files to text or C arrays - txt2midi: Convert text melodies to Standard MIDI Files Web interface updated to load .mid/.midi files directly. Close #2
1 parent 61b7f8b commit a557785

File tree

9 files changed

+1982
-11
lines changed

9 files changed

+1982
-11
lines changed

Makefile

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ EXAMPLE_TARGET = example
1313
TEST_DIR = tests
1414
TEST_SRCS = $(TEST_DIR)/driver.c $(TEST_DIR)/test-q15.c \
1515
$(TEST_DIR)/test-waveform.c $(TEST_DIR)/test-envelope.c \
16-
$(TEST_DIR)/test-synth.c
16+
$(TEST_DIR)/test-synth.c $(TEST_DIR)/test-midi.c
1717
TEST_TARGET = test_runner
1818

1919
# Melody selection: set MELODY to change the song
@@ -22,8 +22,14 @@ MELODY ?= happy_birthday
2222
MELODY_SRC = web/assets/melodies/$(MELODY).txt
2323
MELODY_HDR = melody.h
2424

25-
# Melody converter tool
25+
# Melody converter tools
2626
MIDI2C = tools/midi2c
27+
MIDIPARSE = tools/midiparse
28+
TXT2MIDI = tools/txt2midi
29+
30+
# MIDI file parser source
31+
MIDI_SRC = src/midifile.c
32+
MIDI_HDR = include/midifile.h
2733

2834
TARGET = example
2935

@@ -48,6 +54,14 @@ all: $(TARGET)
4854
$(MIDI2C): tools/midi2c.c
4955
$(CC) -Wall -Wextra -o $@ $<
5056

57+
# Build the MIDI file parser tool
58+
$(MIDIPARSE): tools/midiparse.c $(MIDI_SRC) $(MIDI_HDR)
59+
$(CC) $(CFLAGS) tools/midiparse.c $(MIDI_SRC) -o $@
60+
61+
# Build the text-to-MIDI converter tool
62+
$(TXT2MIDI): tools/txt2midi.c
63+
$(CC) -Wall -Wextra -o $@ $<
64+
5165
# Generate melody.h from selected melody file
5266
$(MELODY_HDR): $(MELODY_SRC) $(MIDI2C)
5367
$(MIDI2C) $(MELODY_SRC) > $@
@@ -56,8 +70,8 @@ $(TARGET): $(EXAMPLE_SRC) $(SRCS) $(HDRS) $(MELODY_HDR)
5670
$(CC) $(CFLAGS) $(EXAMPLE_SRC) $(SRCS) -o $@ $(LDLIBS)
5771

5872
# Build unit test runner
59-
$(TEST_TARGET): $(TEST_SRCS) $(SRCS) $(HDRS) $(TEST_DIR)/test.h
60-
$(CC) $(CFLAGS) -I $(TEST_DIR) $(TEST_SRCS) $(SRCS) -o $@ $(LDLIBS)
73+
$(TEST_TARGET): $(TEST_SRCS) $(SRCS) $(MIDI_SRC) $(HDRS) $(MIDI_HDR) $(TEST_DIR)/test.h
74+
$(CC) $(CFLAGS) -I $(TEST_DIR) $(TEST_SRCS) $(SRCS) $(MIDI_SRC) -o $@ $(LDLIBS)
6175

6276
# Run the example program
6377
run: $(TARGET)
@@ -72,7 +86,7 @@ clean:
7286
$(RM) $(TARGET) $(TEST_TARGET) output.wav $(MELODY_HDR)
7387

7488
# Build tools (explicit target, also built automatically as dependency)
75-
tools: $(MIDI2C)
89+
tools: $(MIDI2C) $(MIDIPARSE) $(TXT2MIDI)
7690

7791
# WebAssembly build
7892
wasm: $(WASM_OUT) copy-melodies
@@ -92,12 +106,12 @@ wasm-clean:
92106

93107
# Remove all generated files
94108
distclean: clean wasm-clean
95-
$(RM) $(MIDI2C)
109+
$(RM) $(MIDI2C) $(MIDIPARSE) $(TXT2MIDI)
96110

97111
# Local development server
98112
serve: wasm
99113
@echo "Starting local server at http://127.0.0.1:8080"
100-
@cd $(WASM_DIR) && python3 -m http.server 8080 --bind 127.0.0.1
114+
@cd $(WASM_DIR) && python3 -c 'exec("""import http.server\nimport socketserver\nhandler = http.server.SimpleHTTPRequestHandler\nhttpd = socketserver.TCPServer(("127.0.0.1", 8080), handler)\nprint("Serving at http://127.0.0.1:8080")\nhttpd.serve_forever()""")'
101115

102116
# List available melodies
103117
list-melodies:
@@ -109,4 +123,4 @@ list-melodies:
109123

110124
# Format all C source and header files
111125
indent:
112-
clang-format -i $(SRCS) $(HDRS) $(EXAMPLE_SRC) $(TEST_SRCS) $(TEST_DIR)/test.h $(WASM_DIR)/wasm.c tools/midi2c.c
126+
clang-format -i $(SRCS) $(HDRS) $(MIDI_SRC) $(MIDI_HDR) $(EXAMPLE_SRC) $(TEST_SRCS) $(TEST_DIR)/test.h $(WASM_DIR)/wasm.c tools/midi2c.c tools/midiparse.c tools/txt2midi.c

include/midifile.h

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* midifile.h - Lightweight MIDI File Decoder
3+
*
4+
* Parses Standard MIDI File (SMF) format 0 and 1 per MIDI 1.0 specification.
5+
* Designed for resource-constrained systems: no dynamic allocation in core
6+
* parsing, callback-based event delivery, minimal memory footprint.
7+
*
8+
* Usage:
9+
* midi_file_t mf;
10+
* if (midi_file_open(&mf, buffer, size) == MIDI_OK) {
11+
* midi_event_t evt;
12+
* while (midi_file_next_event(&mf, &evt) == MIDI_OK) {
13+
* // Process evt
14+
* }
15+
* }
16+
*
17+
* Reference: MIDI 1.0 Detailed Specification (midi.org)
18+
*/
19+
20+
#ifndef MIDIFILE_H_
21+
#define MIDIFILE_H_
22+
23+
#include <stddef.h>
24+
#include <stdint.h>
25+
26+
/* Error codes */
27+
typedef enum {
28+
MIDI_OK = 0,
29+
MIDI_ERR_INVALID_HEADER, /* Not a valid MIDI file (missing MThd) */
30+
MIDI_ERR_UNSUPPORTED_FMT, /* Unsupported MIDI format (e.g., type 2) */
31+
MIDI_ERR_TRUNCATED, /* Unexpected end of data */
32+
MIDI_ERR_INVALID_TRACK, /* Invalid track header or data */
33+
MIDI_ERR_INVALID_EVENT, /* Malformed event data */
34+
MIDI_ERR_END_OF_TRACK, /* No more events in current track */
35+
MIDI_ERR_END_OF_FILE, /* No more tracks to process */
36+
} midi_error_t;
37+
38+
/* MIDI event types (status byte high nibble) */
39+
typedef enum {
40+
MIDI_STATUS_NOTE_OFF = 0x80,
41+
MIDI_STATUS_NOTE_ON = 0x90,
42+
MIDI_STATUS_POLY_PRESSURE = 0xA0,
43+
MIDI_STATUS_CONTROL_CHANGE = 0xB0,
44+
MIDI_STATUS_PROGRAM_CHANGE = 0xC0,
45+
MIDI_STATUS_CHANNEL_PRESSURE = 0xD0,
46+
MIDI_STATUS_PITCH_BEND = 0xE0,
47+
MIDI_STATUS_SYSTEM = 0xF0,
48+
} midi_status_t;
49+
50+
/* Meta event types (following 0xFF status) */
51+
typedef enum {
52+
MIDI_META_SEQUENCE_NUM = 0x00,
53+
MIDI_META_TEXT = 0x01,
54+
MIDI_META_COPYRIGHT = 0x02,
55+
MIDI_META_TRACK_NAME = 0x03,
56+
MIDI_META_INSTRUMENT = 0x04,
57+
MIDI_META_LYRIC = 0x05,
58+
MIDI_META_MARKER = 0x06,
59+
MIDI_META_CUE_POINT = 0x07,
60+
MIDI_META_CHANNEL_PREFIX = 0x20,
61+
MIDI_META_END_OF_TRACK = 0x2F,
62+
MIDI_META_TEMPO = 0x51,
63+
MIDI_META_SMPTE_OFFSET = 0x54,
64+
MIDI_META_TIME_SIG = 0x58,
65+
MIDI_META_KEY_SIG = 0x59,
66+
MIDI_META_SEQUENCER_SPECIFIC = 0x7F,
67+
} midi_meta_type_t;
68+
69+
/* Parsed MIDI event */
70+
typedef struct {
71+
uint32_t delta_time; /* Delta time in ticks since last event */
72+
uint32_t abs_time; /* Absolute time in ticks from track start */
73+
uint8_t status; /* Status byte (type | channel for channel msgs) */
74+
uint8_t type; /* Event type (status & 0xF0, or meta type) */
75+
uint8_t channel; /* Channel (0-15) for channel messages */
76+
uint8_t data1; /* First data byte (note, controller, etc.) */
77+
uint8_t data2; /* Second data byte (velocity, value, etc.) */
78+
uint8_t meta_type; /* Meta event type (when status == 0xFF) */
79+
uint32_t meta_length; /* Length of meta event data */
80+
const uint8_t *meta_data; /* Pointer to meta event data (in buffer) */
81+
} midi_event_t;
82+
83+
/* MIDI file header info */
84+
typedef struct {
85+
uint16_t format; /* 0 = single track, 1 = multi-track sync, 2 = async */
86+
uint16_t ntracks; /* Number of tracks */
87+
uint16_t division; /* Ticks per quarter note (if positive) */
88+
uint8_t uses_smpte; /* Non-zero if SMPTE timing used */
89+
uint8_t smpte_fps; /* SMPTE frames per second */
90+
uint8_t smpte_res; /* SMPTE ticks per frame */
91+
} midi_header_t;
92+
93+
/* MIDI file parser state */
94+
typedef struct {
95+
const uint8_t *buffer; /* File data buffer (not owned) */
96+
size_t buf_len; /* Total buffer length */
97+
size_t buf_pos; /* Current read position */
98+
99+
midi_header_t header; /* Parsed file header */
100+
101+
/* Track state */
102+
uint16_t current_track; /* Current track index (0-based) */
103+
size_t track_start; /* Start position of current track data */
104+
size_t track_end; /* End position of current track data */
105+
uint32_t track_time; /* Accumulated time in current track */
106+
uint8_t running_status; /* Running status byte */
107+
uint8_t track_ended; /* End of track flag */
108+
109+
/* Tempo tracking (microseconds per quarter note) */
110+
uint32_t tempo; /* Default: 500000 (120 BPM) */
111+
} midi_file_t;
112+
113+
/**
114+
* Open and parse MIDI file header.
115+
* @param mf Parser state (caller-allocated)
116+
* @param buffer MIDI file data (caller retains ownership)
117+
* @param length Size of buffer in bytes
118+
* @return MIDI_OK on success, error code otherwise
119+
*/
120+
midi_error_t midi_file_open(midi_file_t *mf,
121+
const uint8_t *buffer,
122+
size_t length);
123+
124+
/**
125+
* Get file header information.
126+
* @param mf Parser state
127+
* @return Pointer to header info (valid while mf is valid)
128+
*/
129+
const midi_header_t *midi_file_get_header(const midi_file_t *mf);
130+
131+
/**
132+
* Start reading a specific track.
133+
* @param mf Parser state
134+
* @param track Track index (0-based, must be < ntracks)
135+
* @return MIDI_OK on success, error code otherwise
136+
*/
137+
midi_error_t midi_file_select_track(midi_file_t *mf, uint16_t track);
138+
139+
/**
140+
* Read next event from current track.
141+
* @param mf Parser state
142+
* @param evt Event structure to fill (caller-allocated)
143+
* @return MIDI_OK on success, MIDI_ERR_END_OF_TRACK when done
144+
*/
145+
midi_error_t midi_file_next_event(midi_file_t *mf, midi_event_t *evt);
146+
147+
/**
148+
* Convert ticks to milliseconds using current tempo.
149+
* @param mf Parser state (for division and tempo)
150+
* @param ticks Time in ticks
151+
* @return Time in milliseconds
152+
*/
153+
uint32_t midi_ticks_to_ms(const midi_file_t *mf, uint32_t ticks);
154+
155+
/**
156+
* Convert ticks to sample count at given sample rate.
157+
* @param mf Parser state (for division and tempo)
158+
* @param ticks Time in ticks
159+
* @param sample_rate Sample rate in Hz
160+
* @return Time in samples
161+
*/
162+
uint32_t midi_ticks_to_samples(const midi_file_t *mf,
163+
uint32_t ticks,
164+
uint32_t sample_rate);
165+
166+
/**
167+
* Check if event is a note-on with velocity > 0.
168+
* Note: MIDI note-on with velocity 0 is equivalent to note-off.
169+
*/
170+
static inline int midi_is_note_on(const midi_event_t *evt)
171+
{
172+
return (evt->type == MIDI_STATUS_NOTE_ON) && (evt->data2 > 0);
173+
}
174+
175+
/**
176+
* Check if event is a note-off (or note-on with velocity 0).
177+
*/
178+
static inline int midi_is_note_off(const midi_event_t *evt)
179+
{
180+
return (evt->type == MIDI_STATUS_NOTE_OFF) ||
181+
((evt->type == MIDI_STATUS_NOTE_ON) && (evt->data2 == 0));
182+
}
183+
184+
/**
185+
* Get note number from note event.
186+
*/
187+
static inline uint8_t midi_note_number(const midi_event_t *evt)
188+
{
189+
return evt->data1;
190+
}
191+
192+
/**
193+
* Get velocity from note event.
194+
*/
195+
static inline uint8_t midi_note_velocity(const midi_event_t *evt)
196+
{
197+
return evt->data2;
198+
}
199+
200+
#endif /* MIDIFILE_H_ */

0 commit comments

Comments
 (0)