From c26bdd26301f12b269604f1c5702d7abb6215f68 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Mon, 26 Jun 2023 01:47:37 +0000 Subject: [PATCH] scsp: incomplete midi example --- Makefile | 6 +- midi/dump.cpp | 103 ++++++++ midi/midi.hpp | 131 ++++++++++ midi/midi_test-c-major-scale.mid | Bin 0 -> 473 bytes midi/parse.cpp | 244 +++++++++++++++++++ midi/parse.hpp | 24 ++ midi/parser.py | 403 +++++++++++++++++++++++++++++++ midi/strings.hpp | 22 ++ midi/test_parse.cpp | 34 +++ scsp/fm.cpp | 9 + scsp/midi.cpp | 350 +++++++++++++++++++++++++++ 11 files changed, 1324 insertions(+), 2 deletions(-) create mode 100644 midi/dump.cpp create mode 100644 midi/midi.hpp create mode 100644 midi/midi_test-c-major-scale.mid create mode 100644 midi/parse.cpp create mode 100644 midi/parse.hpp create mode 100644 midi/parser.py create mode 100644 midi/strings.hpp create mode 100644 midi/test_parse.cpp create mode 100644 scsp/midi.cpp diff --git a/Makefile b/Makefile index b831005..641c935 100644 --- a/Makefile +++ b/Makefile @@ -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 $@ diff --git a/midi/dump.cpp b/midi/dump.cpp new file mode 100644 index 0000000..d78e8e3 --- /dev/null +++ b/midi/dump.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include + +#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(ifs.tellg()); + uint8_t start[size]; + ifs.seekg(0); + if (!ifs.read(reinterpret_cast(&start[0]), size)) { + std::cerr << "read\n"; + return -1; + } + + parse(start); + + return 0; +} diff --git a/midi/midi.hpp b/midi/midi.hpp new file mode 100644 index 0000000..79e5aac --- /dev/null +++ b/midi/midi.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include + +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 diff --git a/midi/midi_test-c-major-scale.mid b/midi/midi_test-c-major-scale.mid new file mode 100644 index 0000000000000000000000000000000000000000..4f61e8ddffd2e05d2803bdd275eb340ff1e9ca6a GIT binary patch literal 473 zcmZ|Lu};G<5C&iuB!-SiFrw=NG?`#PRh8g|v=U*#3tLUO$ZeCl@;M;7z$5TZeXwwb ziNTUB`E=iZI=NEjRS0p$cOaIPa$9~JMSprOq0$@c;Ay2B1cl^9e{x~G-_vcHZuH?W zCR_Vthuj!ED6=Nu;|)S<$-|4L^$M!RYyqCb;4KikHw4XfyKl5dsCj;C9YRM~cg-)P zWXSWbo^1zh$2o)AaYl~+)}gRY!r~Gh?K^z@WAb_=z9*l{uSpgWvmmA{rZG_jk+H~P v;y#F+MIIAn5R!$Ai9CpcMG+HsL6j`Yn2 +#include +#include +#include + +#include "parse.hpp" + +namespace midi { +namespace parse { + +static constexpr inline std::optional> +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 +int_fixed_length16(buf_t buf) +{ + uint16_t n; + if constexpr (std::endian::native == std::endian::big) { + n = *reinterpret_cast(buf); + } else { + n = (buf[0] << 8 | buf[1] << 0); + } + return {buf + (sizeof (uint16_t)), n}; +} + +static constexpr inline std::tuple +int_fixed_length32(buf_t buf) +{ + uint32_t n; + if constexpr (std::endian::native == std::endian::big) { + n = *reinterpret_cast(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 +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 +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> +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> +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> +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> +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> +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> +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> +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 +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> +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 diff --git a/midi/parse.hpp b/midi/parse.hpp new file mode 100644 index 0000000..7d84874 --- /dev/null +++ b/midi/parse.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#include "midi.hpp" + +namespace midi { +namespace parse { + +using buf_t = uint8_t const *; + +std::optional> +header(buf_t buf); + +std::optional> +track(buf_t buf); + +std::optional> +mtrk_event(buf_t buf); + +} +} diff --git a/midi/parser.py b/midi/parser.py new file mode 100644 index 0000000..07e6ee3 --- /dev/null +++ b/midi/parser.py @@ -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('= 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) diff --git a/midi/strings.hpp b/midi/strings.hpp new file mode 100644 index 0000000..0b2a070 --- /dev/null +++ b/midi/strings.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#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); +} + +} +} diff --git a/midi/test_parse.cpp b/midi/test_parse.cpp new file mode 100644 index 0000000..6dc95aa --- /dev/null +++ b/midi/test_parse.cpp @@ -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({&test_ifl16[2], 0x1234})); + +constexpr buf_a_t test_ifl32 = {0x12, 0x34, 0x56, 0x78}; +static_assert(int_fixed_length32(test_ifl32) + == + std::tuple({&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); diff --git a/scsp/fm.cpp b/scsp/fm.cpp index 3b04c7d..9710ee7 100644 --- a/scsp/fm.cpp +++ b/scsp/fm.cpp @@ -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(&(scsp.reg.dsp.STEP[0].MPRO[0])); + fill(dsp_steps, 0, (sizeof (scsp.reg.dsp.STEP))); + while ((smpc.reg.SF & 1) != 0); smpc.reg.SF = 1; smpc.reg.COMREG = COMREG__SNDON; diff --git a/scsp/midi.cpp b/scsp/midi.cpp new file mode 100644 index 0000000..ae5a07b --- /dev/null +++ b/scsp/midi.cpp @@ -0,0 +1,350 @@ +#include +#include + +#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(&_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(&(scsp.reg.dsp.STEP[0].MPRO[0])); + fill(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(&_sine_start); + const uint32_t size = reinterpret_cast(&_sine_size); + copy(&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(&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(&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); +}