scsp: incomplete midi example
This commit is contained in:
parent
f7a178384c
commit
c26bdd2630
6
Makefile
6
Makefile
@ -101,11 +101,11 @@ scsp/sine-44100-s16be-1ch-1sec.pcm:
|
|||||||
mv $@.raw $@
|
mv $@.raw $@
|
||||||
|
|
||||||
# 200 bytes
|
# 200 bytes
|
||||||
scsp/sine-44100-s16be-1ch-100sample.pcm:
|
scsp/%-44100-s16be-1ch-100sample.pcm:
|
||||||
sox \
|
sox \
|
||||||
-r 44100 -e signed-integer -b 16 -c 1 -n -B \
|
-r 44100 -e signed-integer -b 16 -c 1 -n -B \
|
||||||
$@.raw \
|
$@.raw \
|
||||||
synth 100s sin 440 vol -10dB
|
synth 100s $* 440 vol -10dB
|
||||||
mv $@.raw $@
|
mv $@.raw $@
|
||||||
|
|
||||||
scsp/slot.elf: scsp/slot.o scsp/sine-44100-s16be-1ch-1sec.pcm.o
|
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/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
|
res/sperrypc.bitmap.bin: tools/ttf-bitmap
|
||||||
./tools/ttf-bitmap 20 7f res/Bm437_SperryPC_CGA.otb $@
|
./tools/ttf-bitmap 20 7f res/Bm437_SperryPC_CGA.otb $@
|
||||||
|
|
||||||
|
103
midi/dump.cpp
Normal file
103
midi/dump.cpp
Normal 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
131
midi/midi.hpp
Normal 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
|
BIN
midi/midi_test-c-major-scale.mid
Normal file
BIN
midi/midi_test-c-major-scale.mid
Normal file
Binary file not shown.
244
midi/parse.cpp
Normal file
244
midi/parse.cpp
Normal 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
24
midi/parse.hpp
Normal 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
403
midi/parser.py
Normal 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
22
midi/strings.hpp
Normal 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
34
midi/test_parse.cpp
Normal 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);
|
@ -658,6 +658,15 @@ void v_blank_in_int()
|
|||||||
|
|
||||||
void init_slots()
|
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);
|
while ((smpc.reg.SF & 1) != 0);
|
||||||
smpc.reg.SF = 1;
|
smpc.reg.SF = 1;
|
||||||
smpc.reg.COMREG = COMREG__SNDON;
|
smpc.reg.COMREG = COMREG__SNDON;
|
||||||
|
350
scsp/midi.cpp
Normal file
350
scsp/midi.cpp
Normal 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);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user