scsp: incomplete midi example

This commit is contained in:
Zack Buhman 2023-06-26 01:47:37 +00:00
parent f7a178384c
commit c26bdd2630
11 changed files with 1324 additions and 2 deletions

View File

@ -101,11 +101,11 @@ scsp/sine-44100-s16be-1ch-1sec.pcm:
mv $@.raw $@
# 200 bytes
scsp/sine-44100-s16be-1ch-100sample.pcm:
scsp/%-44100-s16be-1ch-100sample.pcm:
sox \
-r 44100 -e signed-integer -b 16 -c 1 -n -B \
$@.raw \
synth 100s sin 440 vol -10dB
synth 100s $* 440 vol -10dB
mv $@.raw $@
scsp/slot.elf: scsp/slot.o scsp/sine-44100-s16be-1ch-1sec.pcm.o
@ -121,6 +121,8 @@ scsp/sound_cpu__interrupt.elf: scsp/sound_cpu__interrupt.o m68k/interrupt.bin.o
scsp/fm.elf: scsp/fm.o res/nec.bitmap.bin.o sh/lib1funcs.o saturn/start.o scsp/sine-44100-s16be-1ch-100sample.pcm.o
scsp/midi.elf: scsp/midi.o res/nec.bitmap.bin.o sh/lib1funcs.o saturn/start.o scsp/sine-44100-s16be-1ch-100sample.pcm.o
res/sperrypc.bitmap.bin: tools/ttf-bitmap
./tools/ttf-bitmap 20 7f res/Bm437_SperryPC_CGA.otb $@

103
midi/dump.cpp Normal file
View File

@ -0,0 +1,103 @@
#include <iostream>
#include <fstream>
#include <tuple>
#include <cassert>
#include "parse.hpp"
#include "strings.hpp"
int parse(uint8_t const * start)
{
uint8_t const * buf = &start[0];
auto header_o = midi::parse::header(buf);
if (!header_o) {
std::cerr << "invalid header\n";
return -1;
}
midi::header_t header;
std::tie(buf, header) = *header_o;
std::cout << "header.format: " << midi::strings::header_format(header.format) << '\n';
std::cout << "header.ntrks: " << header.ntrks << '\n';
//while header.n
// while header.ntrks:
//
// for event in events:
// ev
for (int32_t i = 0; i < header.ntrks; i++) {
std::cout << "track[" << i << "]:\n";
auto track_o = midi::parse::track(buf);
if (!track_o) {
std::cerr << "invalid track\n";
return -1;
}
uint32_t track_length;
std::tie(buf, track_length) = *track_o;
std::cout << " track_length: " << track_length << '\n';
uint8_t const * track_start = buf;
while (buf - track_start < track_length) {
std::cout << " event:\n";
auto mtrk_event_o = midi::parse::mtrk_event(buf);
if (!mtrk_event_o) {
std::cout << " invalid mtrk_event\n";
std::cout << std::hex << buf[0] << ' ' << buf[1] << ' ' << buf[2] << ' ' << buf[3];
return -1;
}
midi::mtrk_event_t mtrk_event;
std::tie(buf, mtrk_event) = *mtrk_event_o;
std::cout << " delta_time: " << mtrk_event.delta_time << '\n';
switch (mtrk_event.event.type) {
case midi::event_t::type_t::midi:
std::cout << " midi: " << '\n';
break;
case midi::event_t::type_t::sysex:
std::cout << " sysex: " << '\n';
break;
case midi::event_t::type_t::meta:
std::cout << " meta: " << '\n';
break;
default:
assert(false);
}
}
assert(buf - track_start == track_length);
}
std::cout << "trailing/unparsed data: " << size - (buf - start) << '\n';
}
int main(int argc, char *argv[])
{
if (argc < 2) {
std::cerr << "argc < 2\n";
return -1;
}
std::cerr << argv[1] << '\n';
std::ifstream ifs;
ifs.open(argv[1], std::ios::binary | std::ios::ate);
if (!ifs.is_open()) {
std::cerr << "ifstream\n";
return -1;
}
auto size = static_cast<int32_t>(ifs.tellg());
uint8_t start[size];
ifs.seekg(0);
if (!ifs.read(reinterpret_cast<char*>(&start[0]), size)) {
std::cerr << "read\n";
return -1;
}
parse(start);
return 0;
}

131
midi/midi.hpp Normal file
View File

@ -0,0 +1,131 @@
#pragma once
#include <cstdint>
namespace midi {
struct division_t {
enum struct type_t : uint8_t {
time_code,
metrical,
} type;
union {
struct {
int8_t smpte;
uint8_t ticks_per_frame;
} time_code;
struct {
uint16_t ticks_per_quarter_note;
} metrical;
};
};
struct header_t {
enum struct format_t : uint8_t {
_0,
_1,
_2,
} format;
uint16_t ntrks;
division_t division;
};
struct midi_event_t {
struct note_off_t {
uint8_t channel;
uint8_t note;
uint8_t velocity;
};
struct note_on_t {
uint8_t channel;
uint8_t note;
uint8_t velocity;
};
struct polyphonic_key_pressure_t {
uint8_t channel;
uint8_t note;
uint8_t pressure;
};
struct control_change_t {
uint8_t channel;
uint8_t control;
uint8_t value;
};
struct program_change_t {
uint8_t channel;
uint8_t program;
};
struct channel_pressure_t {
uint8_t channel;
uint8_t pressure;
};
struct pitch_bend_change_t {
uint8_t channel;
uint8_t lsb;
uint8_t msb;
};
struct channel_mode_t {
uint8_t channel;
uint8_t controller;
uint8_t value;
};
enum struct type_t {
note_off,
note_on,
polyphonic_key_pressure,
control_change,
program_change,
channel_pressure,
pitch_bend_change,
channel_mode,
} type;
union event_t {
note_off_t note_off;
note_on_t note_on;
polyphonic_key_pressure_t polyphonic_key_pressure;
control_change_t control_change;
program_change_t program_change;
channel_pressure_t channel_pressure;
pitch_bend_change_t pitch_bend_change;
channel_mode_t channel_mode;
} data;
};
struct sysex_event_t {
const uint8_t * data;
uint32_t length;
};
struct meta_event_t {
const uint8_t * data;
uint32_t length;
uint8_t type;
};
struct event_t {
enum struct type_t {
midi,
sysex,
meta,
} type;
union _event_t {
midi_event_t midi;
sysex_event_t sysex;
meta_event_t meta;
} event;
};
struct mtrk_event_t {
uint32_t delta_time;
event_t event;
};
} // midi

Binary file not shown.

244
midi/parse.cpp Normal file
View File

@ -0,0 +1,244 @@
#include <tuple>
#include <optional>
#include <cstdint>
#include <bit>
#include "parse.hpp"
namespace midi {
namespace parse {
static constexpr inline std::optional<std::tuple<buf_t, uint32_t>>
int_variable_length(buf_t buf)
{
uint32_t n = 0;
int32_t i = 0;
while (i < 4) {
n <<= 7;
uint8_t b = buf[i++];
n |= (b & 0x7f);
if ((b & 0x80) == 0)
return {{buf + i, n}};
}
return std::nullopt;
}
static constexpr inline std::tuple<buf_t, uint16_t>
int_fixed_length16(buf_t buf)
{
uint16_t n;
if constexpr (std::endian::native == std::endian::big) {
n = *reinterpret_cast<const uint16_t*>(buf);
} else {
n = (buf[0] << 8 | buf[1] << 0);
}
return {buf + (sizeof (uint16_t)), n};
}
static constexpr inline std::tuple<buf_t, uint32_t>
int_fixed_length32(buf_t buf)
{
uint32_t n;
if constexpr (std::endian::native == std::endian::big) {
n = *reinterpret_cast<const uint32_t*>(buf);
} else {
n = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3] << 0);
}
return {buf + (sizeof (uint32_t)), n};
}
static constexpr inline std::optional<buf_t>
header_chunk_type(buf_t buf)
{
if ( buf[0] == 'M'
&& buf[1] == 'T'
&& buf[2] == 'h'
&& buf[3] == 'd')
return {buf + 4};
else
return std::nullopt;
}
static constexpr inline std::tuple<buf_t, division_t>
division(buf_t buf)
{
uint16_t n;
std::tie(buf, n) = int_fixed_length16(buf);
if ((n & (1 << 15)) != 0) {
int8_t smpte = ((n >> 8) & 0x7f) - 0x80; // sign-extend
uint8_t ticks_per_frame = n & 0xff;
return { buf, { .type = division_t::type_t::time_code,
.time_code = { smpte, ticks_per_frame }
} };
} else {
return { buf, { .type = division_t::type_t::metrical,
.metrical = { n }
} };
}
}
std::optional<std::tuple<buf_t, header_t>>
header(buf_t buf)
{
auto buf_o = header_chunk_type(buf);
if (!buf_o) return std::nullopt;
buf = *buf_o;
uint32_t header_length;
std::tie(buf, header_length) = int_fixed_length32(buf);
if (header_length != 6) return std::nullopt;
uint16_t format_num;
std::tie(buf, format_num) = int_fixed_length16(buf);
header_t::format_t format;
switch (format_num) {
case 0: format = header_t::format_t::_0; break;
case 1: format = header_t::format_t::_1; break;
case 2: format = header_t::format_t::_2; break;
default: return std::nullopt;
}
uint16_t ntrks;
std::tie(buf, ntrks) = int_fixed_length16(buf);
division_t division;
std::tie(buf, division) = parse::division(buf);
return {{buf, {format, ntrks, division}}};
}
static constexpr inline std::optional<std::tuple<buf_t, midi_event_t::type_t>>
midi_event_type(buf_t buf)
{
uint8_t n = buf[0] & 0xf0;
// do not increment buf; the caller needs to parse channel
switch (n) {
case 0x80: return {{buf, midi_event_t::type_t::note_off}};
case 0x90: return {{buf, midi_event_t::type_t::note_on}};
case 0xa0: return {{buf, midi_event_t::type_t::polyphonic_key_pressure}};
case 0xb0:
if (buf[0] >= 121 && buf[0] <= 127)
return {{buf, midi_event_t::type_t::channel_mode}};
else
return {{buf, midi_event_t::type_t::control_change}};
case 0xc0: return {{buf, midi_event_t::type_t::program_change}};
case 0xd0: return {{buf, midi_event_t::type_t::channel_pressure}};
case 0xe0: return {{buf, midi_event_t::type_t::pitch_bend_change}};
default: return std::nullopt;
};
}
static constexpr inline int32_t
midi_event_message_length(midi_event_t::type_t type)
{
if (type == midi_event_t::type_t::program_change || type == midi_event_t::type_t::channel_pressure)
return 1;
else
return 2;
}
static constexpr inline std::optional<std::tuple<buf_t, midi_event_t>>
midi_event(buf_t buf)
{
// it doesn't matter which union is used, as long as it is one with
// 3 items. note_off is used throughout, even though it could
// represent a type other than note_off.
midi_event_t event;
auto type_o = midi_event_type(buf); // does not increment buf
if (!type_o) return std::nullopt;
std::tie(buf, event.type) = *type_o;
event.data.note_off.channel = buf[0] & 0x0f;
event.data.note_off.note = buf[1];
// possibly-initializing the third field on a 2-field midi_event
// does not matter, as the caller should ignore it anyway. This
// harmless extraneous initialization means a branch is not needed.
event.data.note_off.velocity = buf[2];
buf += 1 + midi_event_message_length(event.type);
// this does not validate that buf[1]/buf[2] are <= 0x7f
return {{buf, event}};
}
static constexpr inline std::optional<std::tuple<buf_t, sysex_event_t>>
sysex_event(buf_t buf)
{
if (buf[0] != 0xf0 && buf[0] != 0xf7) return std::nullopt;
buf++;
auto length_o = int_variable_length(buf);
if (!length_o) return std::nullopt;
uint32_t length;
std::tie(buf, length) = *length_o;
return {{buf + length, {buf, length}}};
}
static constexpr inline std::optional<std::tuple<buf_t, meta_event_t>>
meta_event(buf_t buf)
{
if (buf[0] != 0xff) return std::nullopt;
uint8_t type = buf[1];
buf += 2;
auto length_o = int_variable_length(buf);
if (!length_o) return std::nullopt;
uint32_t length;
std::tie(buf, length) = *length_o;
return {{buf + length, {buf, length, type}}};
}
static constexpr inline std::optional<std::tuple<buf_t, event_t>>
event(buf_t buf)
{
if (auto midi_o = midi_event(buf)) {
midi_event_t midi;
std::tie(buf, midi) = *midi_o;
return {{buf, {event_t::type_t::midi, {.midi = midi}}}};
} else if (auto meta_o = meta_event(buf)) {
meta_event_t meta;
std::tie(buf, meta) = *meta_o;
return {{buf, {event_t::type_t::meta, {.meta = meta}}}};
} else if (auto sysex_o = sysex_event(buf)) {
sysex_event_t sysex;
std::tie(buf, sysex) = *sysex_o;
return {{buf, {event_t::type_t::sysex, {.sysex = sysex}}}};
} else {
return std::nullopt;
}
}
std::optional<std::tuple<buf_t, mtrk_event_t>>
mtrk_event(buf_t buf)
{
auto delta_time_o = int_variable_length(buf);
if (!delta_time_o) return std::nullopt;
uint32_t delta_time;
std::tie(buf, delta_time) = *delta_time_o;
auto event_o = event(buf);
if (!event_o) return std::nullopt;
event_t event;
std::tie(buf, event) = *event_o;
return {{buf, {delta_time, event}}};
}
static constexpr inline std::optional<buf_t>
track_chunk_type(buf_t buf)
{
if ( buf[0] == 'M'
&& buf[1] == 'T'
&& buf[2] == 'r'
&& buf[3] == 'k')
return {buf + 4};
else
return std::nullopt;
}
std::optional<std::tuple<buf_t, uint32_t>>
track(buf_t buf)
{
auto buf_o = track_chunk_type(buf);
if (!buf_o) return std::nullopt;
buf = *buf_o;
uint32_t length;
std::tie(buf, length) = int_fixed_length32(buf);
return {{buf, length}};
}
} // namespace parse
} // namespace midi

24
midi/parse.hpp Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <optional>
#include <tuple>
#include <cstdint>
#include "midi.hpp"
namespace midi {
namespace parse {
using buf_t = uint8_t const *;
std::optional<std::tuple<buf_t, header_t>>
header(buf_t buf);
std::optional<std::tuple<buf_t, uint32_t>>
track(buf_t buf);
std::optional<std::tuple<buf_t, mtrk_event_t>>
mtrk_event(buf_t buf);
}
}

403
midi/parser.py Normal file
View File

@ -0,0 +1,403 @@
import struct
from dataclasses import dataclass
from typing import *
import enum
def parse_variable_length(buf):
n = 0
i = 0
while i < 4:
n <<= 7
b = buf[i]
i += 1
n |= (b & 0x7f)
if not b & 0x80:
break
else:
assert False, bytes(buf[0:5])
return buf[i:], n
def parse_header_chunk_type(buf):
assert buf[0] == ord('M'), bytes(buf[0:4])
assert buf[1] == ord('T'), bytes(buf[0:4])
assert buf[2] == ord('h'), bytes(buf[0:4])
assert buf[3] == ord('d'), bytes(buf[0:4])
return buf[4:], None
def parse_uint32(buf):
n, = struct.unpack('>L', buf[:4])
return buf[4:], n
def parse_uint16(buf):
n, = struct.unpack('>H', buf[:2])
return buf[2:], n
@dataclass
class MetricalDivision:
ticks_per_quarter_note: int
@dataclass
class TimeCodeDivision:
smpte: int
ticks_per_frame: int
@dataclass
class Header:
format: int
ntrks: int
division: Union[MetricalDivision, TimeCodeDivision]
def parse_division(buf):
buf, n = parse_uint16(buf)
if n & (1 << 15):
smpte = ((n >> 8) & 0x7f) - 0x80 # sign-extend
ticks_per_frame = n & 0xff
division = TimeCodeDivision(smpte, ticks_per_frame)
else:
ticks_per_quarter_note = n
division = MetricalDivision(ticks_per_quarter_note)
return buf, division
def parse_header(buf):
buf, _ = parse_header_chunk_type(buf)
buf, length = parse_uint32(buf)
assert length == 6, length
buf, format = parse_uint16(buf)
assert format in {0, 1, 2}, format
buf, ntrks = parse_uint16(buf)
buf, division = parse_division(buf)
return buf, Header(format, ntrks, division)
@dataclass
class NoteOff:
channel: int
note: int
velocity: int
@dataclass
class NoteOn:
channel: int
note: int
velocity: int
@dataclass
class PolyphonicKeyPressure:
channel: int
note: int
pressure: int
control_change_description = dict([
(0x00, "Bank Select"),
(0x01, "Modulation wheel or lever"),
(0x02, "Breath Controller"),
(0x04, "Foot controller"),
(0x05, "Portamento time"),
(0x06, "Data entry MSB"),
(0x07, "Channel Volume"),
(0x08, "Balance"),
(0x0A, "Pan"),
(0x0B, "Expression Controller"),
(0x0C, "Effect Control 1"),
(0x0D, "Effect Control 2"),
(0x10, "General Purpose Controller 1"),
(0x11, "General Purpose Controller 2"),
(0x12, "General Purpose Controller 3"),
(0x13, "General Purpose Controller 4"),
(0x40, "Damper pedal (sustain)"),
(0x41, "Portamento On/Off"),
(0x42, "Sostenuto"),
(0x43, "Soft pedal"),
(0x44, "Legato Footswitch (vv = 00-3F:Normal, 40-7F=Legatto)"),
(0x45, "Hold 2"),
(0x46, "Sound Controller 1 (default: Sound Variation)"),
(0x47, "Sound Controller 2 (default: Timbre/Harmonic Intensity)"),
(0x48, "Sound Controller 3 (default: Release Time)"),
(0x49, "Sound Controller 4 (default: Attack Time)"),
(0x4A, "Sound Controller 5 (default: Brightness)"),
(0x4B, "Sound Controller 6 (default: no default)"),
(0x4C, "Sound Controller 7 (default: no default)"),
(0x4D, "Sound Controller 8 (default: no default)"),
(0x4E, "Sound Controller 9 (default: no default)"),
(0x4F, "Sound Controller 10 (default: no default)"),
(0x50, "General Purpose Controller 5"),
(0x51, "General Purpose Controller 6"),
(0x52, "General Purpose Controller 7"),
(0x53, "General Purpose Controller 9"),
(0x54, "Portamento Control"),
(0x5B, "Effects 1 Depth"),
(0x5C, "Effects 2 Depth"),
(0x5D, "Effects 3 Depth"),
(0x5E, "Effects 4 Depth"),
(0x5F, "Effects 5 Depth"),
(0x60, "Data increment"),
(0x61, "Data decrement"),
(0x62, "Non-Registered Parameter Number LSB"),
(0x63, "Non-Registered Parameter Number MSB"),
(0x64, "Registered Parameter Number LSB"),
(0x65, "Registered Parameter Number MSB"),
])
@dataclass
class ControlChange:
channel: int
control: int
value: int
def __init__(self, channel, control, value):
self.channel = channel
self.control = (control, control_change_description.get(control, "Undefined"))
self.value = value
@dataclass
class ProgramChange:
channel: int
program: int
@dataclass
class ChannelPressure:
channel: int
pressure: int
@dataclass
class PitchBendChange:
channel: int
lsb: int
msb: int
@dataclass
class ChannelMode:
channel: int
controller: int
value: int
@dataclass
class MIDIEvent:
event = Union[
NoteOff,
NoteOn,
PolyphonicKeyPressure,
ControlChange,
ProgramChange,
ChannelPressure,
PitchBendChange,
ChannelMode,
]
@dataclass
class SysexEvent:
data: bytes
class MetaType(enum.Enum):
SequenceNumber = enum.auto()
TextEvent = enum.auto()
CopyrightNotice = enum.auto()
SequenceTrackName = enum.auto()
InstrumentName = enum.auto()
Lyric = enum.auto()
Marker = enum.auto()
CuePoint = enum.auto()
MIDIChannelPrefix = enum.auto()
EndOfTrack = enum.auto()
SetTempo = enum.auto()
SMPTEOffset = enum.auto()
TimeSignature = enum.auto()
KeySignature = enum.auto()
SequencerSpecific = enum.auto()
def __repr__(self):
return self.name
def _sequence_number(b):
assert len(b) == 2, b
n, = struct.unpack('<H', b[:2])
return n
def _midi_channel_prefix(b):
assert len(b) == 1, b
return b[0]
def _set_tempo(b):
assert len(b) == 3, b
n, = struct.unpack('<L', bytes([0, *b[:3]]))
return n
@dataclass
class SMPTEOffsetValue:
hr: int
mn: int
se: int
fr: int
ff: int
def _smpte_offset(b):
assert len(b) == 5, b
return SMPTEOffsetValue(*b)
@dataclass
class TimeSignature:
nn: int
dd: int
cc: int
bb: int
def _time_signature(b):
assert len(b) == 4, b
return TimeSignature(*b)
@dataclass
class KeySignature:
sf: int
mi: int
def _key_signature(b):
assert len(b) == 2
return KeySignature(*b)
def _bytes(b):
return bytes(b)
def _none(b):
return None
meta_nums = dict([
(0x00, (MetaType.SequenceNumber, _sequence_number)),
(0x01, (MetaType.TextEvent, _bytes)),
(0x02, (MetaType.CopyrightNotice, _bytes)),
(0x03, (MetaType.SequenceTrackName, _bytes)),
(0x04, (MetaType.InstrumentName, _bytes)),
(0x05, (MetaType.Lyric, _bytes)),
(0x06, (MetaType.Marker, _bytes)),
(0x07, (MetaType.CuePoint, _bytes)),
(0x20, (MetaType.MIDIChannelPrefix, _midi_channel_prefix)),
(0x2f, (MetaType.EndOfTrack, _none)),
(0x51, (MetaType.SetTempo, _set_tempo)),
(0x54, (MetaType.SMPTEOffset, _smpte_offset)),
(0x58, (MetaType.TimeSignature, _time_signature)),
(0x59, (MetaType.KeySignature, _key_signature)),
(0x7f, (MetaType.SequencerSpecific, _bytes)),
])
@dataclass
class MetaEvent:
type: MetaType
value: Any
def parse_sysex_event(buf):
if buf[0] not in {0xf0, 0xf7}:
return None
buf = buf[1:]
buf, length = parse_variable_length(buf)
data = buf[:length]
buf = buf[length:]
return buf, SysexEvent(bytes(data))
def parse_meta_event(buf):
if buf[0] != 0xff:
return None
type_n = buf[1]
type, value_parser = meta_nums[type_n]
buf, length = parse_variable_length(buf[2:])
data = buf[:length]
print("meta", length, bytes(data))
buf = buf[length:]
return buf, MetaEvent(type, value_parser(data))
midi_messages = dict([
(0x80, (2, NoteOff)),
(0x90, (2, NoteOn)),
(0xa0, (2, PolyphonicKeyPressure)),
(0xb0, (2, ControlChange)),
(0xc0, (1, ProgramChange)),
(0xd0, (1, ChannelPressure)),
(0xe0, (2, PitchBendChange)),
# ChannelMode handled specially
])
def parse_midi_event(buf):
message_type = buf[0] & 0xf0
#assert message_type != 0xf0, hex(message_type)
if message_type not in midi_messages:
return None
channel = buf[0] & 0x0f
message_length, cls = midi_messages[message_type]
buf = buf[1:]
data = buf[:message_length]
# handle channel mode specially
if cls is ControlChange and data[0] >= 121 and data[0] <= 127:
# 0xb0 is overloaded for both control change and channel mode
cls = ChannelMode
message = cls(channel, *data)
buf = buf[message_length:]
return buf, message
def parse_event(buf):
while True:
b = buf[0]
if (sysex := parse_sysex_event(buf)) is not None:
buf, sysex = sysex
return buf, sysex
elif (meta := parse_meta_event(buf)) is not None:
buf, meta = meta
return buf, meta
elif (midi := parse_midi_event(buf)) is not None:
buf, midi = midi
return buf, midi
else:
print(hex(buf[0]), file=sys.stderr)
buf = buf[1:]
while (buf[0] & 0x80) == 0:
print(hex(buf[0]), file=sys.stderr)
buf = buf[1:]
@dataclass
class Event:
delta_time: int
event: Union[MIDIEvent, SysexEvent, MetaEvent]
def parse_mtrk_event(buf):
buf, delta_time = parse_variable_length(buf)
buf, event = parse_event(buf)
return buf, Event(delta_time, event)
def parse_track_chunk_type(buf):
assert buf[0] == ord('M'), bytes(buf[0:4])
assert buf[1] == ord('T'), bytes(buf[0:4])
assert buf[2] == ord('r'), bytes(buf[0:4])
assert buf[3] == ord('k'), bytes(buf[0:4])
return buf[4:], None
@dataclass
class Track:
events: list[Event]
def parse_track(buf):
buf, _ = parse_track_chunk_type(buf)
buf, length = parse_uint32(buf)
offset = len(buf)
events = []
while (offset - len(buf)) < length:
buf, event = parse_mtrk_event(buf)
events.append(event)
return buf, Track(events)
def parse_file(buf):
buf, header = parse_header(buf)
print(header)
assert header.ntrks > 0
tracks = []
for track_num in range(header.ntrks):
buf, track = parse_track(buf)
tracks.append(track)
print(f"track {track_num}:")
for event in track.events:
print(' ' + repr(event))
print("remaining data:", len(buf))
import sys
with open(sys.argv[1], 'rb') as f:
b = memoryview(f.read())
parse_file(b)

22
midi/strings.hpp Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#include <string>
#include "midi.hpp"
namespace midi {
namespace strings {
constexpr inline std::string
header_format(header_t::format_t format)
{
switch (format) {
case header_t::format_t::_0: return "0";
case header_t::format_t::_1: return "1";
case header_t::format_t::_2: return "2";
}
while (1);
}
}
}

34
midi/test_parse.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "parse.cpp"
using buf_a_t = uint8_t const [];
using namespace midi;
using namespace midi::parse;
constexpr buf_a_t test_ifl16 = {0x12, 0x34};
static_assert(int_fixed_length16(test_ifl16)
==
std::tuple<buf_t, uint16_t>({&test_ifl16[2], 0x1234}));
constexpr buf_a_t test_ifl32 = {0x12, 0x34, 0x56, 0x78};
static_assert(int_fixed_length32(test_ifl32)
==
std::tuple<buf_t, uint32_t>({&test_ifl32[4], 0x12345678}));
constexpr buf_a_t test_header = {0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x60};
static_assert(header(test_header) != std::nullopt);
constexpr header_t h1 = std::get<1>(*header(test_header));
static_assert(h1.format == header_t::format_t::_0);
static_assert(h1.ntrks == 1);
static_assert(h1.division.type == division_t::type_t::metrical);
static_assert(h1.division.metrical.ticks_per_quarter_note == 96);
constexpr buf_a_t test_met1 = {0x8a};
constexpr buf_a_t test_met2 = {0xb2, 121};
constexpr buf_a_t test_met3 = {0xb3, 0};
constexpr midi_event_t::type_t met1 = std::get<1>(*midi_event_type(test_met1));
constexpr midi_event_t::type_t met2 = std::get<1>(*midi_event_type(test_met2));
constexpr midi_event_t::type_t met3 = std::get<1>(*midi_event_type(test_met3));
static_assert(met1 == midi_event_t::type_t::note_off);
static_assert(met2 == midi_event_t::type_t::channel_mode);
static_assert(met3 == midi_event_t::type_t::control_change);

View File

@ -658,6 +658,15 @@ void v_blank_in_int()
void init_slots()
{
/*
The Saturn BIOS does not (un)initialize the DSP. Without zeroizing the DSP
program, the SCSP DSP appears to have a program that continuously writes to
0x30000 through 0x3ffff in sound RAM, which has the effect of destroying any
samples stored there.
*/
reg32 * dsp_steps = reinterpret_cast<reg32*>(&(scsp.reg.dsp.STEP[0].MPRO[0]));
fill<reg32>(dsp_steps, 0, (sizeof (scsp.reg.dsp.STEP)));
while ((smpc.reg.SF & 1) != 0);
smpc.reg.SF = 1;
smpc.reg.COMREG = COMREG__SNDON;

350
scsp/midi.cpp Normal file
View File

@ -0,0 +1,350 @@
#include <cstdint>
#include <optional>
#include "vdp2.h"
#include "smpc.h"
#include "scu.h"
#include "sh2.h"
#include "scsp.h"
#include "../common/copy.hpp"
#include "../common/intback.hpp"
#include "../common/vdp2_func.hpp"
#include "../common/string.hpp"
extern void * _sine_start __asm("_binary_scsp_sine_44100_s16be_1ch_100sample_pcm_start");
extern void * _sine_size __asm("_binary_scsp_sine_44100_s16be_1ch_100sample_pcm_size");
extern void * _nec_bitmap_start __asm("_binary_res_nec_bitmap_bin_start");
constexpr inline uint16_t rgb15(int32_t r, int32_t g, int32_t b)
{
return ((b & 31) << 10) | ((g & 31) << 5) | ((r & 31) << 0);
}
void palette_data()
{
vdp2.cram.u16[1 + 0 ] = rgb15( 0, 0, 0);
vdp2.cram.u16[2 + 0 ] = rgb15(31, 31, 31);
vdp2.cram.u16[1 + 16] = rgb15(31, 31, 31);
vdp2.cram.u16[2 + 16] = rgb15( 0, 0, 0);
vdp2.cram.u16[1 + 32] = rgb15(10, 10, 10);
vdp2.cram.u16[2 + 32] = rgb15(31, 31, 31);
}
namespace pix_fmt_4bpp
{
constexpr inline uint32_t
bit(uint8_t n, int32_t i)
{
i &= 7;
auto b = (n >> (7 - i)) & 1;
return ((b + 1) << ((7 - i) * 4));
}
constexpr inline uint32_t
bits(uint8_t n)
{
return
bit(n, 0) | bit(n, 1) | bit(n, 2) | bit(n, 3)
| bit(n, 4) | bit(n, 5) | bit(n, 6) | bit(n, 7);
}
static_assert(bits(0b1100'1110) == 0x2211'2221);
static_assert(bits(0b1010'0101) == 0x2121'1212);
static_assert(bits(0b1000'0000) == 0x2111'1111);
}
void cell_data()
{
const uint8_t * normal = reinterpret_cast<uint8_t*>(&_nec_bitmap_start);
for (int ix = 0; ix <= (0x7f - 0x20); ix++) {
for (int y = 0; y < 8; y++) {
const uint8_t row_n = normal[ix * 8 + y];
vdp2.vram.u32[ 0 + (ix * 8) + y] = pix_fmt_4bpp::bits(row_n);
}
}
}
struct count_flop {
s8 count;
u8 flop;
u8 das;
u8 repeat;
};
struct input {
count_flop right;
count_flop left;
count_flop down;
count_flop up;
count_flop start;
count_flop a;
count_flop b;
count_flop c;
count_flop r;
count_flop x;
count_flop y;
count_flop z;
count_flop l;
};
constexpr int input_arr = 10;
constexpr int input_das = 20;
constexpr int input_debounce = 2;
static inline void
input_count(count_flop& button, uint32_t input, uint32_t mask)
{
if ((input & mask) == 0) {
if (button.count < input_debounce)
button.count += 1;
else
button.das += 1;
} else {
if (button.count == 0) {
button.flop = 0;
button.das = 0;
button.repeat = 0;
}
else if (button.count > 0)
button.count -= 1;
}
}
static inline int32_t
input_flopped(count_flop& button)
{
if (button.count == input_debounce && button.flop == 0) {
button.flop = 1;
return 1;
} else if (button.flop == 1 && button.das == input_das && button.repeat == 0) {
button.repeat = 1;
button.das = 0;
return 2;
} else if (button.repeat == 1 && (button.das == input_arr)) {
button.das = 0;
return 2;
} else {
return 0;
}
}
struct state {
struct input input;
};
static struct state state = { 0 };
void digital_callback(uint8_t fsm_state, uint8_t data)
{
switch (fsm_state) {
case intback::DATA1:
input_count(state.input.right, data, DIGITAL__1__RIGHT);
input_count(state.input.left, data, DIGITAL__1__LEFT);
input_count(state.input.down, data, DIGITAL__1__DOWN);
input_count(state.input.up, data, DIGITAL__1__UP);
input_count(state.input.start, data, DIGITAL__1__START);
input_count(state.input.a, data, DIGITAL__1__A);
input_count(state.input.c, data, DIGITAL__1__C);
input_count(state.input.b, data, DIGITAL__1__B);
break;
case intback::DATA2:
input_count(state.input.r, data, DIGITAL__2__R);
input_count(state.input.x, data, DIGITAL__2__X);
input_count(state.input.y, data, DIGITAL__2__Y);
input_count(state.input.z, data, DIGITAL__2__Z);
input_count(state.input.l, data, DIGITAL__2__L);
break;
default: break;
}
}
extern "C"
void smpc_int(void) __attribute__ ((interrupt_handler));
void smpc_int(void)
{
scu.reg.IST &= ~(IST__SMPC);
scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN);
intback::fsm(digital_callback, nullptr);
}
constexpr int32_t plane_a = 2;
constexpr inline int32_t plane_offset(int32_t n) { return n * 0x2000; }
constexpr int32_t page_size = 64 * 64 * 2; // N0PNB__1WORD (16-bit)
constexpr int32_t plane_size = page_size * 1;
constexpr int32_t cell_size = (8 * 8) / 2; // N0CHCN__16_COLOR (4-bit)
constexpr int32_t character_size = cell_size * (1 * 1); // N0CHSZ__1x1_CELL
constexpr int32_t page_width = 64;
static int plane_ix = 0;
inline void
set_char(int32_t x, int32_t y, uint8_t palette, uint8_t c)
{
const auto ix = (plane_offset(plane_a + plane_ix) / 2) + (y * page_width) + x;
vdp2.vram.u16[ix] =
PATTERN_NAME_TABLE_1WORD__PALETTE(palette)
| PATTERN_NAME_TABLE_1WORD__CHARACTER((c - 0x20));
}
void render()
{
// 012345678901234
uint8_t label[] = "midi";
for (uint32_t i = 0; label[i] != 0; i++) {
set_char(0 + i, 1, 0, label[i]);
}
}
void update()
{
}
extern "C"
void v_blank_in_int(void) __attribute__ ((interrupt_handler));
void v_blank_in_int()
{
scu.reg.IST &= ~(IST__V_BLANK_IN);
scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN);
// flip planes;
vdp2.reg.MPABN0 = MPABN0__N0MPB(0) | MPABN0__N0MPA(plane_a + plane_ix);
plane_ix = !plane_ix;
// wait at least 300us, as specified in the SMPC manual.
// It appears reading FRC.H is mandatory and *must* occur before FRC.L on real
// hardware.
while ((sh2.reg.FTCSR & FTCSR__OVF) == 0 && sh2.reg.FRC.H == 0 && sh2.reg.FRC.L < 63);
if ((vdp2.reg.TVSTAT & TVSTAT__VBLANK) != 0) {
// on real hardware, SF contains uninitialized garbage bits other than the
// lsb.
while ((smpc.reg.SF & 1) != 0);
smpc.reg.SF = 0;
smpc.reg.IREG[0].val = INTBACK__IREG0__STATUS_DISABLE;
smpc.reg.IREG[1].val = ( INTBACK__IREG1__PERIPHERAL_DATA_ENABLE
| INTBACK__IREG1__PORT2_15BYTE
| INTBACK__IREG1__PORT1_15BYTE
);
smpc.reg.IREG[2].val = INTBACK__IREG2__MAGIC;
smpc.reg.COMREG = COMREG__INTBACK;
}
update();
render();
}
void init_slots()
{
/*
The Saturn BIOS does not (un)initialize the DSP. Without zeroizing the DSP
program, the SCSP DSP appears to have a program that continuously writes to
0x30000 through 0x3ffff in sound RAM, which has the effect of destroying any
samples stored there.
*/
reg32 * dsp_steps = reinterpret_cast<reg32*>(&(scsp.reg.dsp.STEP[0].MPRO[0]));
fill<reg32>(dsp_steps, 0, (sizeof (scsp.reg.dsp.STEP)));
while ((smpc.reg.SF & 1) != 0);
smpc.reg.SF = 1;
smpc.reg.COMREG = COMREG__SNDON;
while (smpc.reg.OREG[31].val != 0b00000110);
for (long i = 0; i < 807; i++) { asm volatile ("nop"); } // wait for (way) more than 30µs
scsp.reg.ctrl.MIXER = MIXER__MEM4MB | MIXER__MVOL(0);
const uint32_t * buf = reinterpret_cast<uint32_t*>(&_sine_start);
const uint32_t size = reinterpret_cast<uint32_t>(&_sine_size);
copy<uint32_t>(&scsp.ram.u32[0], buf, size);
for (int i = 0; i < 32; i++) {
scsp_slot& slot = scsp.reg.slot[i];
// start address (bytes)
slot.SA = SA__KYONB | SA__LPCTL__NORMAL | SA__SA(0); // kx kb sbctl[1:0] ssctl[1:0] lpctl[1:0] 8b sa[19:0]
slot.LSA = 0; // loop start address (samples)
slot.LEA = 100; // loop end address (samples)
slot.EG = EG__EGHOLD; // d2r d1r ho ar krs dl rr
slot.FM = 0; // stwinh sdir tl mdl mdxsl mdysl
slot.PITCH = PITCH__OCT(0) | PITCH__FNS(0); // oct fns
slot.LFO = 0; // lfof plfows
slot.MIXER = MIXER__DISDL(0b101); // disdl dipan efsdl efpan
}
scsp.reg.ctrl.MIXER = MIXER__MEM4MB | MIXER__MVOL(0xf);
}
void main()
{
init_slots();
v_blank_in();
// DISP: Please make sure to change this bit from 0 to 1 during V blank.
vdp2.reg.TVMD = ( TVMD__DISP | TVMD__LSMD__NON_INTERLACE
| TVMD__VRESO__240 | TVMD__HRESO__NORMAL_320);
/* set the color mode to 5bits per channel, 1024 colors */
vdp2.reg.RAMCTL = RAMCTL__CRMD__RGB_5BIT_1024;
/* enable display of NBG0 */
vdp2.reg.BGON = BGON__N0ON;
/* set character format for NBG0 to palettized 16 color
set enable "cell format" for NBG0
set character size for NBG0 to 1x1 cell */
vdp2.reg.CHCTLA = CHCTLA__N0CHCN__16_COLOR
| CHCTLA__N0BMEN__CELL_FORMAT
| CHCTLA__N0CHSZ__1x1_CELL;
/* "Note: In color RAM modes 0 and 2, 2048-color becomes 1024-color" */
/* use 1-word (16-bit) pattern names */
vdp2.reg.PNCN0 = PNCN0__N0PNB__1WORD;
/* plane size */
vdp2.reg.PLSZ = PLSZ__N0PLSZ__1x1;
/* map plane offset
1-word: value of bit 6-0 * 0x2000
2-word: value of bit 5-0 * 0x4000
*/
vdp2.reg.MPOFN = MPOFN__N0MP(0); // bits 8~6
vdp2.reg.MPABN0 = MPABN0__N0MPB(0) | MPABN0__N0MPA(plane_a); // bits 5~0
vdp2.reg.MPCDN0 = MPABN0__N0MPD(0) | MPABN0__N0MPC(0); // bits 5~0
// zeroize character/cell data from 0 up to plane_a_offset
fill<uint32_t>(&vdp2.vram.u32[(0 / 4)], 0, plane_offset(plane_a));
// zeroize plane_a; `0` is the ascii 0x20 ("space") which doubles as
// "transparency" character.
fill<uint32_t>(&vdp2.vram.u32[(plane_offset(plane_a) / 4)], 0, plane_size * 2);
palette_data();
cell_data();
// free-running timer
sh2.reg.TCR = TCR__CKS__INTERNAL_DIV128;
sh2.reg.FTCSR = 0;
// initialize smpc
smpc.reg.DDR1 = 0; // INPUT
smpc.reg.DDR2 = 0; // INPUT
smpc.reg.IOSEL = 0; // SMPC control
smpc.reg.EXLE = 0; //
sh2_vec[SCU_VEC__SMPC] = (u32)(&smpc_int);
sh2_vec[SCU_VEC__V_BLANK_IN] = (u32)(&v_blank_in_int);
scu.reg.IST = 0;
scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN);
}