diff --git a/assets/test_loud.fur b/assets/test_loud.fur new file mode 100644 index 0000000..2cc8e28 Binary files /dev/null and b/assets/test_loud.fur differ diff --git a/assets/test_loud.zsm b/assets/test_loud.zsm new file mode 100644 index 0000000..5a92685 Binary files /dev/null and b/assets/test_loud.zsm differ diff --git a/backends/playback/fluidsynth/CMakeLists.txt b/backends/playback/fluidsynth/CMakeLists.txt new file mode 100644 index 0000000..21bead9 --- /dev/null +++ b/backends/playback/fluidsynth/CMakeLists.txt @@ -0,0 +1,7 @@ +set(BACKEND_FLUIDSYNTH_SRC ${CMAKE_CURRENT_SOURCE_DIR}/fluidsynth_backend.cpp) +set(BACKEND_FLUIDSYNTH_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) +add_playback_backend(fluidsynth_backend ${BACKEND_FLUIDSYNTH_SRC}) +target_include_directories(fluidsynth_backend PRIVATE ${BACKEND_FLUIDSYNTH_INC}) +find_package(OpenMP) +find_package(FluidSynth) +target_link_libraries(fluidsynth_backend PUBLIC OpenMP::OpenMP_CXX FluidSynth::libfluidsynth) diff --git a/backends/playback/fluidsynth/backend.json b/backends/playback/fluidsynth/backend.json new file mode 100644 index 0000000..f61b7b9 --- /dev/null +++ b/backends/playback/fluidsynth/backend.json @@ -0,0 +1,4 @@ +{ + "class_name": "FluidSynthBackend", + "include_path": "fluidsynth_backend.hpp" +} diff --git a/backends/playback/fluidsynth/fluidsynth_backend.cpp b/backends/playback/fluidsynth/fluidsynth_backend.cpp new file mode 100644 index 0000000..852900e --- /dev/null +++ b/backends/playback/fluidsynth/fluidsynth_backend.cpp @@ -0,0 +1,470 @@ +#include "fluidsynth_backend.hpp" +#include +#include +#include +#include +#include "file_backend.hpp" +#include +#include +#include +#include +type FluidSynthBackend::name() { \ + std::optional value_maybe = get(#name); \ + if (value_maybe.has_value()) { \ + return resolve_value(value_maybe.value()); \ + } \ + return default_value; \ +} +void fluidsynth_get_property_list_wrapper(void *udata, const char *name, int type) { + ((FluidSynthBackend*)udata)->fluidsynth_get_property_list(name, type); +} +std::vector FluidSynthBackend::get_property_list() { + fluid_settings_foreach(settings, (void*)this, &fluidsynth_get_property_list_wrapper); + return properties; +} +void FluidSynthBackend::load(const char *filename) { + memset(&spec, 0, sizeof(spec)); + current_file = filename; + spec.format = AUDIO_S16SYS; + spec.samples = 100; + spec.channels = 2; + spec.freq = PSG_FREQ; + spec.size = 100 * 2 * sizeof(int16_t); + file = open_file(filename); + char magic[2]; + file->read(magic, 2, 1); + if (magic[0] != 0x7a || magic[1] != 0x6d) { + throw std::exception(); + } + uint8_t version; + file->read(&version, 1, 1); + uint8_t loop_point[3]; + file->read(loop_point, 3, 1); + this->loop_point = loop_point[0] | ((uint32_t)(loop_point[1]) << 8) | ((uint32_t)(loop_point[2]) << 16); + file->read(loop_point, 3, 1); + this->pcm_offset = loop_point[0] | ((uint32_t)(loop_point[1]) << 8) | ((uint32_t)(loop_point[2]) << 16); + pcm_offset += 3; + file->read(&fm_mask, 1, 1); + file->read(loop_point, 2, 1); + this->psg_channel_mask = loop_point[0] | ((uint16_t)(loop_point[1]) << 8); + file->read(loop_point, 2, 1); + this->tick_rate = loop_point[0] | ((uint16_t)(loop_point[1]) << 8); + file->read(loop_point, 2, 1); // Reserved. + music_data_start = file->get_pos(); + this->loop_point += music_data_start; + file->seek(pcm_offset, SeekType::SET); + file->read(loop_point, 1, 1); + pcm_offset++; + pcm_data_offs = ((((uint16_t)loop_point[0]) + 1) * 16) + pcm_offset; + for (uint8_t i = 0; i <= loop_point[0]; i++) { + uint16_t instdef = (i * 16) + 1; + pcm_instrument *inst = new pcm_instrument(); + file->seek(pcm_offset + instdef, SeekType::SET); + file->read(&inst->geom, 1, 1); + uint8_t bytes[10]; + file->read(bytes, 10, 1); + inst->loop_rem = bytes[9]; + inst->loop_rem <<= 8; + inst->loop_rem |= bytes[8]; + inst->loop_rem <<= 8; + inst->loop_rem |= bytes[7]; + inst->loop = loop_rem; + inst->islooped = bytes[6] & 0x80; + inst->remain = bytes[5]; + inst->remain <<= 8; + inst->remain |= bytes[4]; + inst->remain <<= 8; + inst->remain |= bytes[3]; + uint32_t cur = bytes[2]; + cur <<= 8; + cur |= bytes[1]; + cur <<= 8; + cur |= bytes[0]; + cur += pcm_data_offs; + inst->data = (uint8_t*)malloc(inst->remain); + file->seek(cur, SeekType::SET); + file->read(inst->data, 1, inst->remain); + inst->loop_rem = inst->remain - inst->loop_rem; + instruments.push_back(inst); + } + file->seek(music_data_start, SeekType::SET); + this->loop_point = std::max(this->loop_point, (uint32_t)music_data_start); + double prev_time = 0.0; + double time = 0.0; + double tmpDelayTicks = 0.0; + loop_pos = -1.0; + uint32_t prev_pos = music_data_start; + while (true) { + tmpDelayTicks -= get_delay_per_frame(); + if (tmpDelayTicks < 0.0) { + ZsmCommand cmd = get_command(); + size_t cur_pos = file->get_pos(); + if (cur_pos >= this->loop_point && this->loop_pos < 0) { + loop_pos = time; + this->loop_point = cur_pos; + } + if (cmd.id == ZsmEOF) { + break; + } else if (cmd.id == Delay) { + time += ((double)cmd.delay) / ((double)(tick_rate)); + tmpDelayTicks += cmd.delay; + } + prev_pos = file->get_pos(); + prev_time = time; + } + } + if (this->loop_pos < 0.0) { + this->loop_pos = 0.0; + this->loop_point = music_data_start; + } + length = time; + music_data_len = file->get_pos(); + switch_stream(0); + loop_end = length; + loop_start = this->loop_pos; + fm_stream = SDL_NewAudioStream(AUDIO_S16SYS, 2, YM_FREQ, AUDIO_S16SYS, 2, PSG_FREQ); + DEBUG.writefln("fm_stream: %ld -> %ld", YM_FREQ, PSG_FREQ); +#define _PROPERTY(name, type, default_value) \ + { \ + std::string type_str = #type; \ + google::protobuf::Any value; \ + if (type_str == "bool") { \ + BooleanProperty value_b; \ + value_b.set_value(default_value); \ + value.PackFrom(value_b); \ + } else if (type_str == "double") { \ + DoubleProperty value_d; \ + value_d.set_value(default_value); \ + value.PackFrom(value_d); \ + } \ + property_defaults[#name] = value; \ + } +#include "properties.inc" +} +extern SDL_AudioSpec obtained; +void ZsmBackend::switch_stream(int idx) { + YM_Create(YM_FREQ); + YM_init(YM_FREQ/64, 60); + YM_reset(); + psg_reset(); + pcm_reset(); + for (uint8_t i = 0; i < 16; i++) { + psg_writereg(i * 4 + 2, 0); + } + file->seek(music_data_start, SeekType::SET); + this->cpuClocks = 0.0; + this->delayTicks = 0.0; + this->ticks = 0.0; +} +void ZsmBackend::cleanup() { + delete file; + file = nullptr; + audio_buf.clear(); + SDL_FreeAudioStream(fm_stream); + fm_stream = nullptr; + audio_sample = nullptr; + for (auto inst : instruments) { + delete inst; + } + instruments.clear(); +} +void ZsmBackend::tick(bool step) { + delayTicks -= 1; + const double ClocksPerTick = ((double)HZ) / ((double)tick_rate); + double prevCpuClocks = cpuClocks; + double nextCpuClocks = cpuClocks + ClocksPerTick; + double ticks_remaining = ClocksPerTick; + while (delayTicks <= 0) { + ZsmCommand cmd = get_command(); + switch (cmd.id) { + case ZsmEOF: { + if (step) { + file->seek(this->loop_point, SeekType::SET); + this->position = loop_pos; + } else { + throw std::exception(); + } + } break; + case PsgWrite: { + psg_writereg(cmd.psg_write.reg, cmd.psg_write.val); + } break; + case FmWrite: { + for (uint8_t i = 0; i < cmd.fm_write.len; i++) { + YM_write_reg(cmd.fm_write.regs[i].reg, cmd.fm_write.regs[i].val); + while (YM_read_status()) { + size_t clocksToAddForYm = 1; + ticks_remaining -= clocksToAddForYm; + if (ticks_remaining < 0) { + delayTicks -= 1; + nextCpuClocks += ClocksPerTick; + ticks_remaining += ClocksPerTick; + } + audio_step(clocksToAddForYm); + } + } + } break; + case Delay: { + delayTicks += cmd.delay; + position += ((double)cmd.delay) / ((double)(tick_rate)); + } break; + case ExtCmd: { + //cmd.extcmd.channel + switch (cmd.extcmd.channel) { + case 0: { + for (size_t i = 0; i < cmd.extcmd.bytes; i += 2) { + switch (cmd.extcmd.pcm[i]) { + case 0: { // ctrl + uint8_t ctrl = cmd.extcmd.pcm[i + 1]; + if (ctrl & 0x80) { + remain = 0; + } + pcm_write_ctrl(ctrl); + } break; + case 1: { // rate + pcm_write_rate(cmd.extcmd.pcm[i + 1]); + } break; + default: { // trigger + size_t file_pos = file->get_pos(); + uint8_t ctrl = pcm_read_ctrl(); + pcm_write_ctrl(ctrl | 0x80); + uint16_t pcm_idx = cmd.extcmd.pcm[i + 1]; + pcm_instrument *inst = instruments[pcm_idx]; + ctrl = pcm_read_ctrl() & 0x0F; + ctrl |= inst->geom & 0x30; + pcm_write_ctrl(ctrl); + audio_sample = inst->data; + loop = inst->loop; + loop_rem = inst->loop_rem; + remain = inst->remain; + islooped = inst->islooped; + cur = 0; + } break; + } + } break; + } + // Nothing handled yet. + } + } break; + } + } + size_t nextCpuClocksInt = std::floor(nextCpuClocks); + size_t prevCpuClocksInt = std::floor(prevCpuClocks); + size_t cpuClocksIntDelta = nextCpuClocksInt - prevCpuClocksInt; + audio_step(ticks_remaining); + cpuClocks = std::fmod(nextCpuClocks, ClocksPerTick); +} +size_t ZsmBackend::render(void *buf, size_t maxlen) { + size_t sample_type_len = 2; + maxlen /= sample_type_len; + while (audio_buf.size() <= maxlen) { + tick(true); + } + size_t copied = copy_out(buf, maxlen) * sample_type_len; + maxlen *= sample_type_len; + return copied; +} +uint64_t ZsmBackend::get_min_samples() { + return spec.size; +} +std::optional ZsmBackend::get_max_samples() { + return get_min_samples(); +} +ZsmCommand ZsmBackend::get_command() { + ZsmCommandId cmdid; + uint8_t cmd_byte; + file->read(&cmd_byte, 1, 1); + if (cmd_byte == 0x80) { + cmdid = ZsmEOF; + } else { + if ((cmd_byte >> 6) == 0) { + cmdid = PsgWrite; + } else if ((cmd_byte >> 6) == 0b01) { + if (cmd_byte == 0b01000000) { + cmdid = ExtCmd; + } else { + cmdid = FmWrite; + } + } else { + cmdid = Delay; + } + } + ZsmCommand output; + output.id = cmdid; + if (cmdid == ZsmEOF) { + return output; + } else if (cmdid == PsgWrite) { + uint8_t value; + file->read(&value, 1, 1); + output.psg_write.reg = cmd_byte & 0x3F; + output.psg_write.val = value; + } else if (cmdid == FmWrite) { + uint16_t _value; + uint8_t *value = (uint8_t*)(void*)(&_value); + uint8_t pairs = cmd_byte & 0b111111; + output.fm_write.len = pairs; + output.fm_write.regs = (reg_pair*)malloc((sizeof(reg_pair))*pairs); + for (uint8_t i = 0; i < pairs; i++) { + file->read(value, 2, 1); + output.fm_write.regs[i].reg = value[0]; + output.fm_write.regs[i].val = value[1]; + } + } else if (cmdid == ExtCmd) { + uint8_t ext_cmd_byte; + file->read(&ext_cmd_byte, 1, 1); + uint8_t bytes = ext_cmd_byte & 0x3F; + uint8_t ch = ext_cmd_byte >> 6; + output.extcmd.channel = ch; + output.extcmd.bytes = bytes; + if (ch == 1) { + output.extcmd.expansion.write_bytes = NULL; + } else { + output.extcmd.pcm = (uint8_t*)malloc(bytes); // Handles all other cases due to them being in a union, and each having the same type. + } + for (size_t i = 0; i < bytes; i++) { + uint8_t byte; + file->read(&byte, 1, 1); + switch (ch) { + case 0: { + output.extcmd.pcm[i] = byte; + } break; + case 1: { + if (i == 0) { + output.extcmd.expansion.chip_id = byte; + } else if (i == 1) { + output.extcmd.expansion.writes = byte; + output.extcmd.expansion.write_bytes = (uint8_t*)malloc(byte); + } else { + output.extcmd.expansion.write_bytes[i - 2] = byte; + } + } break; + case 2: { + output.extcmd.sync[i] = byte; + } break; + case 3: { + output.extcmd.custom[i] = byte; + } break; + } + } + } else if (cmdid == Delay) { + output.delay = cmd_byte & 0x7F; + } + return output; +} +ZsmCommand::~ZsmCommand() { + switch (id) { + case ExtCmd: { + if (extcmd.channel == 1) { + if (extcmd.expansion.write_bytes != NULL) { + free(extcmd.expansion.write_bytes); + } + } else { + free(extcmd.pcm); + } + } break; + case FmWrite: { + free(fm_write.regs); + } + } +} +void ZsmBackend::seek_internal(double position, bool loop) { + this->position = std::floor(this->position * PSG_FREQ) / PSG_FREQ; + position = std::floor(position * PSG_FREQ) / PSG_FREQ; + if (this->position > position) { + switch_stream(0); + file->seek(music_data_start, SeekType::SET); + this->cpuClocks = 0.0; + this->delayTicks = 0; + this->ticks = 0.0; + this->position = 0.0; + } else if (this->position == position) { + audio_buf.clear(); + return; + } + while (this->position < position) { + audio_buf.clear(); + try { + tick(false); + } catch (std::exception) { + switch_stream(0); + file->seek(music_data_start, SeekType::SET); + this->cpuClocks = 0.0; + this->delayTicks = 0; + this->ticks = 0.0; + this->position = 0.0; + audio_buf.clear(); + return; + } + } + size_t samples = std::min((size_t)((this->position - position) * PSG_FREQ), audio_buf.size()); + while (samples--) { + audio_buf.pop(); + } + this->position = position; +} +void ZsmBackend::seek(double position) { + seek_internal(position, false); +} +double ZsmBackend::get_position() { + return position; +} +int ZsmBackend::get_stream_idx() { + return 0; +} + +void ZsmBackend::audio_step(size_t samples) { + if (samples == 0) return; + while (remain != 0 && pcm_fifo_avail() < samples) { + if (pcm_read_rate() == 0) break; + if ((--remain) == 0) { + if (islooped) { + cur = loop; + remain = loop_rem; + } else { + break; + } + } + size_t oldpos = file->get_pos(); + uint8_t sample = audio_sample[cur++]; + pcm_write_fifo(sample); + } + samples *= 2; + int16_t *psg_ptr = psg_buf.get_item_sized(samples); + int16_t *pcm_ptr = pcm_buf.get_item_sized(samples); + psg_render(psg_ptr, samples / 2); + pcm_render(pcm_ptr, samples / 2); + int16_t *out_ptr = out_buf.get_item_sized(samples); + // The exact amount of samples needed for the stream. + double ratio = ((double)YM_FREQ) / ((double)PSG_FREQ); + size_t needed_samples = ((size_t)std::floor(samples * ratio)) / 2; + int16_t *ym_ptr = ym_buf.get_item_sized(needed_samples * 2); + YM_stream_update(ym_ptr, needed_samples); + assert(SDL_AudioStreamPut(fm_stream, ym_ptr, needed_samples * 2 * sizeof(int16_t)) == 0); + while (SDL_AudioStreamAvailable(fm_stream) < ((samples + 2) * sizeof(int16_t))) { + YM_stream_update(ym_ptr, 1); + assert(SDL_AudioStreamPut(fm_stream, ym_ptr, 2 * sizeof(int16_t)) == 0); + } + int16_t *ym_resample_ptr = ym_resample_buf.get_item_sized(samples); + ssize_t ym_resample_len = SDL_AudioStreamGet(fm_stream, ym_resample_ptr, (samples + 2) * sizeof(int16_t)); + assert(ym_resample_len >= 0); + ym_resample_len /= sizeof(int16_t); + for (size_t i = 0; i < samples / 2; i++) { + size_t j = i * 2; + int16_t psg[2] = {(int16_t)(psg_ptr[j] >> 1), (int16_t)(psg_ptr[j + 1] >> 1)}; + int16_t pcm[2] = {(int16_t)(pcm_ptr[j] >> 2), (int16_t)(pcm_ptr[j + 1] >> 2)}; + if (!pcm_enable()) memset(pcm, 0, sizeof(pcm)); + if (!psg_enable()) memset(psg, 0, sizeof(psg)); + pcm[0] *= pcm_volume(); + pcm[1] *= pcm_volume(); + psg[0] *= psg_volume(); + psg[1] *= psg_volume(); + int16_t vera[2] = {(int16_t)(psg[0] + pcm[0]), (int16_t)(psg[1] + pcm[1])}; + int16_t fm[2] = {ym_resample_ptr[j], ym_resample_ptr[j + 1]}; + if (!fm_enable()) memset(fm, 0, sizeof(fm)); + fm[0] *= fm_volume(); + fm[1] *= fm_volume(); + int16_t mix[2] = {(int16_t)(vera[0] + (fm[0] >> 1)), (int16_t)(vera[1] + (fm[1] >> 1))}; + out_ptr[j++] = mix[0]; + out_ptr[j++] = mix[1]; + } + audio_buf.push(out_ptr, samples); +} diff --git a/backends/playback/fluidsynth/fluidsynth_backend.hpp b/backends/playback/fluidsynth/fluidsynth_backend.hpp new file mode 100644 index 0000000..4e4b4d1 --- /dev/null +++ b/backends/playback/fluidsynth/fluidsynth_backend.hpp @@ -0,0 +1,47 @@ +#pragma once +#include "playback_backend.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "file_backend.hpp" +class FluidSynthBackend : public PlaybackBackend { + File *file; + void seek_internal(double position, bool loop = true); + std::vector fluidsynth_properties; + fluid_settings_t *settings; + void fluidsynth_get_property_list(const char *name, int type); + public: + uint64_t get_min_samples() override; + void set_fluidsynth_property_str(std::string path, std::string val); + void set_fluidsynth_property_num(std::string path, double val); + void set_fluidsynth_property_int(std::string path, int val); + std::string get_fluidsynth_property_str(std::string path); + double get_fluidsynth_property_num(std::string path); + int get_fluidsynth_property_int(std::string path); + std::optional get_max_samples() override; + inline std::string get_id() override { + return "fluidsynth"; + } + inline std::string get_name() override { + return "MIDI player"; + } + std::vector get_property_list() override; + void seek(double position) override; + void load(const char *filename) override; + void switch_stream(int idx) override; + void cleanup() override; + int get_stream_idx() override; + size_t render(void *buf, size_t maxlen) override; + double get_position() override; + inline double get_length() override { + return length; + } + inline ~ZsmBackend() override { } +}; diff --git a/backends/playback/zsm/zsm_backend.cpp b/backends/playback/zsm/zsm_backend.cpp index 214cbc1..f3c30d4 100644 --- a/backends/playback/zsm/zsm_backend.cpp +++ b/backends/playback/zsm/zsm_backend.cpp @@ -205,17 +205,11 @@ void ZsmBackend::tick(bool step) { } break; case FmWrite: { for (uint8_t i = 0; i < cmd.fm_write.len; i++) { - YM_write_reg(cmd.fm_write.regs[i].reg, cmd.fm_write.regs[i].val); - while (YM_read_status()) { - size_t clocksToAddForYm = 1; - ticks_remaining -= clocksToAddForYm; - if (ticks_remaining < 0) { - delayTicks -= 1; - nextCpuClocks += ClocksPerTick; - ticks_remaining += ClocksPerTick; - } - audio_step(clocksToAddForYm); - } + DEBUG.writefln2("YM Pair index: {0}", ym_pairs.size()); + reg_pair pair; + memcpy(&pair, cmd.fm_write.regs + i, sizeof(reg_pair)); + ym_pairs.push(pair); + DEBUG.writefln2("Writing {1} to FM reg {0} later.", pair.reg, pair.val); } } break; case Delay: { @@ -262,6 +256,20 @@ void ZsmBackend::tick(bool step) { } break; } } + while (ym_pairs.size() > 0) { + reg_pair pair = ym_pairs.pop(); + YM_write_reg(pair.reg, pair.val); + while (YM_read_status()) { + size_t clocksToAddForYm = (size_t)std::ceil(((double)YM_FREQ)/((double)PSG_FREQ)); + ticks_remaining -= clocksToAddForYm; + if (ticks_remaining < 0) { + delayTicks -= 1; + nextCpuClocks += ClocksPerTick; + ticks_remaining += ClocksPerTick; + } + audio_step(clocksToAddForYm); + } + } size_t nextCpuClocksInt = std::floor(nextCpuClocks); size_t prevCpuClocksInt = std::floor(prevCpuClocks); size_t cpuClocksIntDelta = nextCpuClocksInt - prevCpuClocksInt; diff --git a/backends/playback/zsm/zsm_backend.hpp b/backends/playback/zsm/zsm_backend.hpp index 90ecfa7..e5879a4 100644 --- a/backends/playback/zsm/zsm_backend.hpp +++ b/backends/playback/zsm/zsm_backend.hpp @@ -81,6 +81,7 @@ class ZsmBackend : public PlaybackBackend { DynPtr ym_resample_buf; bool ym_recorded = false; uint8_t ym_data[256]; + Fifo ym_pairs; uint32_t loop_rem; uint32_t pcm_data_offs; uint8_t pcm_data_instruments; diff --git a/sdl-android-project/.idea/caches/deviceStreaming.xml b/sdl-android-project/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..5a2f866 --- /dev/null +++ b/sdl-android-project/.idea/caches/deviceStreaming.xml @@ -0,0 +1,329 @@ + + + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/compiler.xml b/sdl-android-project/.idea/compiler.xml index b589d56..b86273d 100644 --- a/sdl-android-project/.idea/compiler.xml +++ b/sdl-android-project/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/sdl-android-project/.idea/deploymentTargetSelector.xml b/sdl-android-project/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/sdl-android-project/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/gradle.xml b/sdl-android-project/.idea/gradle.xml index 0897082..049b0eb 100644 --- a/sdl-android-project/.idea/gradle.xml +++ b/sdl-android-project/.idea/gradle.xml @@ -5,7 +5,7 @@