diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c90be6b..e549d7c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,7 +40,7 @@ jobs: with: msystem: MINGW64 update: true - install: make git mingw-w64-x86_64-toolchain mingw-w64-x86_64-libelf mingw-w64-x86_64-SDL2 + install: make git mingw-w64-x86_64-toolchain mingw-w64-x86_64-libelf mingw-w64-x86_64-SDL2 mingw-w64-x86_64-fluidsynth - uses: actions/setup-python@v5 with: python-version: '3.9' @@ -65,11 +65,31 @@ jobs: run: | git reset --hard git diff - TRACE=1 CROSS_COMPILE_WINDOWS=1 SDL2CONFIG=sdl2-config make V=1 -j2 + TRACE=1 FLUIDSYNTH=1 CROSS_COMPILE_WINDOWS=1 SDL2CONFIG=sdl2-config make V=1 -j2 mkdir emu_binaries cp $(which SDL2.dll) emu_binaries/. cp $(which zlib1.dll) emu_binaries/. cp $(which libwinpthread-1.dll) emu_binaries/. + cp $(which libfluidsynth-3.dll) emu_binaries/. + cp $(which libgcc_s_seh-1.dll) emu_binaries/. + cp $(which libglib-2.0-0.dll) emu_binaries/. + cp $(which libgmodule-2.0-0.dll) emu_binaries/. + cp $(which libportaudio.dll) emu_binaries/. + cp $(which libintl-8.dll) emu_binaries/. + cp $(which libstdc++-6.dll) emu_binaries/. + cp $(which libgomp-1.dll) emu_binaries/. + cp $(which libreadline8.dll) emu_binaries/. + cp $(which libsndfile-1.dll) emu_binaries/. + cp $(which libpcre2-8-0.dll) emu_binaries/. + cp $(which libiconv-2.dll) emu_binaries/. + cp $(which libtermcap-0.dll) emu_binaries/. + cp $(which libFLAC.dll) emu_binaries/. + cp $(which libmp3lame-0.dll) emu_binaries/. + cp $(which libmpg123-0.dll) emu_binaries/. + cp $(which libogg-0.dll) emu_binaries/. + cp $(which libopus-0.dll) emu_binaries/. + cp $(which libvorbis-0.dll) emu_binaries/. + cp $(which libvorbisenc-2.dll) emu_binaries/. cp sdcard.img.zip emu_binaries/. cp x16emu.exe emu_binaries/. cp makecart.exe emu_binaries/. @@ -103,7 +123,7 @@ jobs: with: msystem: MINGW64 update: true - install: make git mingw-w64-i686-toolchain mingw-w64-i686-libelf mingw-w64-i686-SDL2 + install: make git mingw-w64-i686-toolchain mingw-w64-i686-libelf mingw-w64-i686-SDL2 mingw-w64-i686-fluidsynth path-type: inherit - uses: actions/setup-python@v5 with: @@ -131,11 +151,31 @@ jobs: run: | git reset --hard git diff - TRACE=1 WIN_SDL2=/mingw32 TARGET_CPU=x86 CROSS_COMPILE_WINDOWS=1 make V=1 -j2 + TRACE=1 FLUIDSYNTH=1 WIN_SDL2=/mingw32 TARGET_CPU=x86 CROSS_COMPILE_WINDOWS=1 make V=1 -j2 mkdir emu_binaries cp $(which SDL2.dll) emu_binaries/. cp $(which zlib1.dll) emu_binaries/. cp $(which libwinpthread-1.dll) emu_binaries/. + cp $(which libfluidsynth-3.dll) emu_binaries/. + cp $(which libgcc_s_seh-1.dll) emu_binaries/. + cp $(which libglib-2.0-0.dll) emu_binaries/. + cp $(which libgmodule-2.0-0.dll) emu_binaries/. + cp $(which libportaudio.dll) emu_binaries/. + cp $(which libintl-8.dll) emu_binaries/. + cp $(which libstdc++-6.dll) emu_binaries/. + cp $(which libgomp-1.dll) emu_binaries/. + cp $(which libreadline8.dll) emu_binaries/. + cp $(which libsndfile-1.dll) emu_binaries/. + cp $(which libpcre2-8-0.dll) emu_binaries/. + cp $(which libiconv-2.dll) emu_binaries/. + cp $(which libtermcap-0.dll) emu_binaries/. + cp $(which libFLAC.dll) emu_binaries/. + cp $(which libmp3lame-0.dll) emu_binaries/. + cp $(which libmpg123-0.dll) emu_binaries/. + cp $(which libogg-0.dll) emu_binaries/. + cp $(which libopus-0.dll) emu_binaries/. + cp $(which libvorbis-0.dll) emu_binaries/. + cp $(which libvorbisenc-2.dll) emu_binaries/. cp sdcard.img.zip emu_binaries/. cp x16emu.exe emu_binaries/. cp makecart.exe emu_binaries/. @@ -165,7 +205,11 @@ jobs: with: python-version: '3.9' - name: Install Dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential make libsdl2-dev + run: | + sudo cp /etc/apt/sources.list /etc/apt/sources.list~ + sudo sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list + sudo apt-get update + sudo apt-get install -y build-essential make libsdl2-dev libfluidsynth-dev - name: Fetch latest ROM if: startsWith(github.ref, 'refs/tags/r') != true run: | @@ -183,7 +227,7 @@ jobs: cp latest_rom/*.h src/. - name: Build Emulator run: | - TRACE=1 make V=1 -j3 + TRACE=1 FLUIDSYNTH=1 make V=1 -j3 mkdir emu_binaries cp sdcard.img.zip emu_binaries/. cp x16emu emu_binaries/. @@ -241,8 +285,8 @@ jobs: copy_artifact_path: emu_binaries commands: | apt-get update - apt-get install -y build-essential make libsdl2-dev file git - TRACE=1 make V=1 -j3 + apt-get install -y build-essential make libsdl2-dev file git libfluidsynth-dev + TRACE=1 FLUIDSYNTH=1 make V=1 -j3 mkdir emu_binaries cp sdcard.img.zip emu_binaries/. cp x16emu emu_binaries/. @@ -301,8 +345,8 @@ jobs: copy_artifact_path: emu_binaries commands: | apt-get update - apt-get install -y build-essential make libsdl2-dev file git - TRACE=1 make V=1 -j3 + apt-get install -y build-essential make libsdl2-dev file git libfluidsynth-dev + TRACE=1 FLUIDSYNTH=1 make V=1 -j3 mkdir emu_binaries cp sdcard.img.zip emu_binaries/. cp x16emu emu_binaries/. @@ -333,7 +377,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Dependencies run: | - brew install make sdl2 + brew install make sdl2 fluid-synth - uses: actions/setup-python@v5 with: python-version: '3.9' @@ -354,7 +398,7 @@ jobs: cp latest_rom/*.h src/. - name: Build Emulator run: | - TRACE=1 MAC_STATIC=1 LIBSDL_FILE=/usr/local/Cellar/sdl2/*/lib/libSDL2.a make V=1 -j3 + TRACE=1 MAC_STATIC=1 LIBSDL_FILE=/usr/local/Cellar/sdl2/*/lib/libSDL2.a HOMEBREW_LIB=/usr/local/lib make V=1 -j3 mkdir emu_binaries cp sdcard.img.zip emu_binaries/. cp x16emu emu_binaries/. @@ -383,7 +427,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Dependencies run: | - brew install make sdl2 + brew install make sdl2 fluid-synth - uses: actions/setup-python@v5 with: python-version: '3.10' @@ -404,7 +448,7 @@ jobs: cp latest_rom/*.h src/. - name: Build Emulator run: | - TRACE=1 MAC_STATIC=1 LIBSDL_FILE=/opt/homebrew/Cellar/sdl2/*/lib/libSDL2.a make V=1 -j3 + TRACE=1 MAC_STATIC=1 LIBSDL_FILE=/opt/homebrew/Cellar/sdl2/*/lib/libSDL2.a ADDL_INCLUDE=/opt/homebrew/include HOMEBREW_LIB=/opt/homebrew/lib FLUIDSYNTH=1 make V=1 -j3 mkdir emu_binaries cp sdcard.img.zip emu_binaries/. cp x16emu emu_binaries/. diff --git a/Makefile b/Makefile index c62a088..0bba141 100644 --- a/Makefile +++ b/Makefile @@ -18,12 +18,10 @@ endif CFLAGS=-std=c99 -O3 -Wall -Werror -g $(shell $(SDL2CONFIG) --cflags) -Isrc/extern/include CXXFLAGS=-std=c++17 -O3 -Wall -Werror -Isrc/extern/ymfm/src -LDFLAGS=$(shell $(SDL2CONFIG) --libs) -lm -lz +LDFLAGS=$(shell $(SDL2CONFIG) --libs) -lm -lz -pthread -# build with link time optimization -ifndef NOLTO - CFLAGS+=-flto - LDFLAGS+=-flto +ifdef ADDL_INCLUDE + CFLAGS+=-I$(ADDL_INCLUDE) endif X16_ODIR = build/x16emu @@ -44,10 +42,9 @@ GIT_REV=$(shell git diff --quiet && /bin/echo -n $$(git rev-parse --short=8 HEAD CFLAGS+=-D GIT_REV='"$(GIT_REV)"' ifeq ($(MAC_STATIC),1) - LDFLAGS=$(LIBSDL_FILE) -lm -liconv -lz -Wl,-framework,CoreAudio -Wl,-framework,AudioToolbox -Wl,-framework,ForceFeedback -lobjc -Wl,-framework,CoreVideo -Wl,-framework,Cocoa -Wl,-framework,Carbon -Wl,-framework,IOKit -Wl,-weak_framework,QuartzCore -Wl,-weak_framework,Metal -Wl,-weak_framework,CoreHaptics -Wl,-weak_framework,GameController -endif - -ifeq ($(CROSS_COMPILE_WINDOWS),1) + LDFLAGS=$(LIBSDL_FILE) -lm -liconv -lz -Wl,-framework,CoreAudio -Wl,-framework,AudioToolbox -Wl,-framework,ForceFeedback -lobjc -Wl,-framework,CoreVideo -Wl,-framework,Cocoa -Wl,-framework,Carbon -Wl,-framework,IOKit -Wl,-weak_framework,QuartzCore -Wl,-weak_framework,Metal -Wl,-weak_framework,CoreHaptics -Wl,-weak_framework,GameController -pthread + LDEMU=-ldl -Wl,-rpath,$(HOMEBREW_LIB) +else ifeq ($(CROSS_COMPILE_WINDOWS),1) LDFLAGS+=-L$(MINGW32)/lib # this enables printf() to show, but also forces a console window LDFLAGS+=-Wl,--subsystem,console @@ -59,6 +56,14 @@ else CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ endif +else # Not Mac, not Windows, probably Linux + LDEMU=-ldl +endif + +# build with link time optimization +ifndef NOLTO + CFLAGS+=-flto + LDFLAGS+=-flto endif ifdef TARGET_WIN32 @@ -74,7 +79,11 @@ ifdef EMSCRIPTEN MAKECART_OUTPUT=makecart.html endif -_X16_OBJS = cpu/fake6502.o memory.o disasm.o video.o i2c.o smc.o rtc.o via.o serial.o ieee.o vera_spi.o audio.o vera_pcm.o vera_psg.o sdcard.o main.o debugger.o javascript_interface.o joystick.o rendertext.o keyboard.o icon.o timing.o wav_recorder.o testbench.o files.o cartridge.o iso_8859_15.o ymglue.o +ifeq ($(FLUIDSYNTH),1) + CFLAGS+=-DHAS_FLUIDSYNTH +endif + +_X16_OBJS = cpu/fake6502.o memory.o disasm.o video.o i2c.o smc.o rtc.o via.o serial.o ieee.o vera_spi.o audio.o vera_pcm.o vera_psg.o sdcard.o main.o debugger.o javascript_interface.o joystick.o rendertext.o keyboard.o icon.o timing.o wav_recorder.o testbench.o files.o cartridge.o iso_8859_15.o ymglue.o midi.o _X16_OBJS += extern/ymfm/src/ymfm_opm.o ifdef TARGET_WIN32 @@ -93,7 +102,7 @@ MAKECART_DEPS := $(MAKECART_OBJS:.o=.d) all: x16emu makecart x16emu: $(X16_OBJS) - $(CXX) -o $(X16_OUTPUT) $(X16_OBJS) $(LDFLAGS) + $(CXX) -o $(X16_OUTPUT) $(X16_OBJS) $(LDFLAGS) $(LDEMU) $(X16_ODIR)/%.o: $(X16_SDIR)/%.c @mkdir -p $$(dirname $@) diff --git a/src/audio.c b/src/audio.c index 4171aba..c2bfae7 100644 --- a/src/audio.c +++ b/src/audio.c @@ -8,6 +8,7 @@ #include "vera_pcm.h" #include "wav_recorder.h" #include "ymglue.h" +#include "midi.h" #include #include #include @@ -23,6 +24,7 @@ #define VERA_SAMP_CLKS_PER_CPU_CLK ((25000000ULL << SAMP_POS_FRAC_BITS) / 512 / MHZ / 1000000) #define YM_SAMP_CLKS_PER_CPU_CLK ((3579545ULL << SAMP_POS_FRAC_BITS) / 64 / MHZ / 1000000) +#define FS_SAMP_CLKS_PER_CPU_CLK VERA_SAMP_CLKS_PER_CPU_CLK #define SAMPLE_BYTES (2 * sizeof(int16_t)) #define SAMP_POS_MASK (SAMPLES_PER_BUFFER - 1) #define SAMP_POS_MASK_FRAC (((uint32_t)SAMPLES_PER_BUFFER << SAMP_POS_FRAC_BITS) - 1) @@ -75,13 +77,18 @@ static uint32_t vera_samp_pos_hd = 0; static uint32_t ym_samp_pos_rd = 0; static uint32_t ym_samp_pos_wr = 0; static uint32_t ym_samp_pos_hd = 0; +static uint32_t fs_samp_pos_rd = 0; +static uint32_t fs_samp_pos_wr = 0; +static uint32_t fs_samp_pos_hd = 0; static uint32_t vera_samps_per_host_samps = 0; static uint32_t ym_samps_per_host_samps = 0; +static uint32_t fs_samps_per_host_samps = 0; static uint32_t limiter_amp = 0; static int16_t psg_buf[2 * SAMPLES_PER_BUFFER]; static int16_t pcm_buf[2 * SAMPLES_PER_BUFFER]; static int16_t ym_buf[2 * SAMPLES_PER_BUFFER]; +static int16_t fs_buf[2 * SAMPLES_PER_BUFFER]; uint32_t host_sample_rate = 0; @@ -181,17 +188,22 @@ audio_init(const char *dev_name, int num_audio_buffers) host_sample_rate = obtained.freq; vera_samps_per_host_samps = ((25000000ULL << SAMP_POS_FRAC_BITS) / 512 / host_sample_rate); ym_samps_per_host_samps = ((3579545ULL << SAMP_POS_FRAC_BITS) / 64 / host_sample_rate); + fs_samps_per_host_samps = vera_samps_per_host_samps; vera_samp_pos_rd = 0; vera_samp_pos_wr = 0; vera_samp_pos_hd = 0; ym_samp_pos_rd = 0; ym_samp_pos_wr = 0; ym_samp_pos_hd = 0; + fs_samp_pos_rd = 0; + fs_samp_pos_wr = 0; + fs_samp_pos_hd = 0; limiter_amp = (1 << 16); psg_buf[0] = psg_buf[1] = 0; pcm_buf[0] = pcm_buf[1] = 0; ym_buf[0] = ym_buf[1] = 0; + fs_buf[0] = fs_buf[1] = 0; // Start playback SDL_PauseAudioDevice(audio_dev, 0); @@ -228,6 +240,7 @@ audio_step(int cpu_clocks) uint32_t max_cpu_clks = SDL_min(cpu_clocks, max_cpu_clks_ym); vera_samp_pos_hd = (vera_samp_pos_hd + max_cpu_clks * VERA_SAMP_CLKS_PER_CPU_CLK) & SAMP_POS_MASK_FRAC; ym_samp_pos_hd = (ym_samp_pos_hd + max_cpu_clks * YM_SAMP_CLKS_PER_CPU_CLK) & SAMP_POS_MASK_FRAC; + fs_samp_pos_hd = (fs_samp_pos_hd + max_cpu_clks * FS_SAMP_CLKS_PER_CPU_CLK) & SAMP_POS_MASK_FRAC; cpu_clocks -= max_cpu_clks; if (cpu_clocks > 0) audio_render(); } @@ -271,16 +284,31 @@ audio_render() YM_stream_update((uint16_t *)&ym_buf[pos * 2], len); } + pos = (fs_samp_pos_wr + 1) & SAMP_POS_MASK; + len = ((fs_samp_pos_hd >> SAMP_POS_FRAC_BITS) - fs_samp_pos_wr) & SAMP_POS_MASK; + fs_samp_pos_wr = fs_samp_pos_hd >> SAMP_POS_FRAC_BITS; + if ((pos + len) > SAMPLES_PER_BUFFER) { + midi_synth_render(&fs_buf[pos * 2], SAMPLES_PER_BUFFER - pos); + len -= SAMPLES_PER_BUFFER - pos; + pos = 0; + } + if (len > 0) { + midi_synth_render(&fs_buf[pos * 2], len); + } + uint32_t wridx_old = wridx; uint32_t len_vera = (vera_samp_pos_hd - vera_samp_pos_rd) & SAMP_POS_MASK_FRAC; uint32_t len_ym = (ym_samp_pos_hd - ym_samp_pos_rd) & SAMP_POS_MASK_FRAC; - if (len_vera < (4 << SAMP_POS_FRAC_BITS) || len_ym < (4 << SAMP_POS_FRAC_BITS)) { + uint32_t len_fs = (fs_samp_pos_hd - fs_samp_pos_rd) & SAMP_POS_MASK_FRAC; + if (len_vera < (4 << SAMP_POS_FRAC_BITS) || len_ym < (4 << SAMP_POS_FRAC_BITS) || len_fs < (4 << SAMP_POS_FRAC_BITS)) { // not enough samples yet, at least 4 are needed for the filter return; } len_vera = (len_vera - (4 << SAMP_POS_FRAC_BITS)) / vera_samps_per_host_samps; len_ym = (len_ym - (4 << SAMP_POS_FRAC_BITS)) / ym_samps_per_host_samps; + len_fs = (len_fs - (4 << SAMP_POS_FRAC_BITS)) / fs_samps_per_host_samps; len = SDL_min(len_vera, len_ym); + len = SDL_min(len, len_fs); SDL_LockAudioDevice(audio_dev); for (int i = 0; i < len; i++) { int32_t samp[8]; @@ -289,6 +317,8 @@ audio_render() int32_t vera_out_r = 0; int32_t ym_out_l = 0; int32_t ym_out_r = 0; + int32_t fs_out_l = 0; + int32_t fs_out_r = 0; // Don't resample VERA outputs if the host sample rate is as desired if (host_sample_rate == AUDIO_SAMPLERATE) { pos = (vera_samp_pos_rd >> SAMP_POS_FRAC_BITS) * 2; @@ -326,11 +356,33 @@ audio_render() ym_out_r += samp[5] * filter[255 - filter_idx]; ym_out_l += samp[6] * filter[511 - filter_idx]; ym_out_r += samp[7] * filter[511 - filter_idx]; - // Mixing is according to the Developer Board + // Don't resample MIDI synth outputs if the host sample rate matches it + if (host_sample_rate == AUDIO_SAMPLERATE) { + pos = (fs_samp_pos_rd >> SAMP_POS_FRAC_BITS) * 2; + fs_out_l = (uint32_t)fs_buf[pos] << 14; + fs_out_r = (uint32_t)fs_buf[pos + 1] << 14; + } else { + filter_idx = (fs_samp_pos_rd >> (SAMP_POS_FRAC_BITS - 8)) & 0xff; + pos = (fs_samp_pos_rd >> SAMP_POS_FRAC_BITS) * 2; + for (int j = 0; j < 8; j += 2) { + samp[j] = fs_buf[pos]; + samp[j + 1] = fs_buf[pos + 1]; + pos = (pos + 2) & (SAMP_POS_MASK * 2); + } + fs_out_l += samp[0] * filter[256 + filter_idx]; + fs_out_r += samp[1] * filter[256 + filter_idx]; + fs_out_l += samp[2] * filter[ 0 + filter_idx]; + fs_out_r += samp[3] * filter[ 0 + filter_idx]; + fs_out_l += samp[4] * filter[255 - filter_idx]; + fs_out_r += samp[5] * filter[255 - filter_idx]; + fs_out_l += samp[6] * filter[511 - filter_idx]; + fs_out_r += samp[7] * filter[511 - filter_idx]; + } + // VERA+YM mixing is according to the Developer Board // Loudest single PSG channel is 1/8 times the max output - // mix = (psg + pcm) * 2 + ym - int32_t mix_l = (vera_out_l >> 13) + (ym_out_l >> 15); - int32_t mix_r = (vera_out_r >> 13) + (ym_out_r >> 15); + // mix = (psg + pcm) * 2 + ym + fs * 4 + int32_t mix_l = (vera_out_l >> 13) + (ym_out_l >> 15) + (fs_out_l >> 12); + int32_t mix_r = (vera_out_r >> 13) + (ym_out_r >> 15) + (fs_out_r >> 12); uint32_t amp = SDL_max(SDL_abs(mix_l), SDL_abs(mix_r)); if (amp > 32767) { uint32_t limiter_amp_new = (32767 << 16) / amp; @@ -341,6 +393,7 @@ audio_render() if (limiter_amp < (1 << 16)) limiter_amp++; vera_samp_pos_rd = (vera_samp_pos_rd + vera_samps_per_host_samps) & SAMP_POS_MASK_FRAC; ym_samp_pos_rd = (ym_samp_pos_rd + ym_samps_per_host_samps) & SAMP_POS_MASK_FRAC; + fs_samp_pos_rd = (fs_samp_pos_rd + fs_samps_per_host_samps) & SAMP_POS_MASK_FRAC; if (wridx == buffer_size) { wav_recorder_process(&buffer[wridx_old], (buffer_size - wridx_old) / 2); wridx = 0; @@ -368,6 +421,10 @@ audio_render() if (skip > 1) { ym_samp_pos_rd = (ym_samp_pos_rd + ym_samps_per_host_samps) & SAMP_POS_MASK_FRAC; } + skip = len_fs - len; + if (skip > 1) { + fs_samp_pos_rd = (fs_samp_pos_rd + fs_samps_per_host_samps) & SAMP_POS_MASK_FRAC; + } } void diff --git a/src/cpu/registers.h b/src/cpu/registers.h index b1f9195..a35d5b2 100644 --- a/src/cpu/registers.h +++ b/src/cpu/registers.h @@ -4,6 +4,7 @@ #include #include +#include "../endian.h" #define FLAG_CARRY 0x01 #define FLAG_ZERO 0x02 @@ -20,26 +21,6 @@ //6502 CPU registers -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ -#define LOW_HIGH_UNION(name, low, high) \ - union { \ - struct { \ - uint8_t low; \ - uint8_t high; \ - }; \ - uint16_t name; \ - } -#else -#define LOW_HIGH_UNION(name, low, high) \ - union { \ - struct { \ - uint8_t high; \ - uint8_t low; \ - }; \ - uint16_t name; \ - } -#endif - struct regs { LOW_HIGH_UNION(c, a, b); @@ -59,8 +40,6 @@ struct regs bool is65c816; }; -#undef LOW_HIGH_UNION - void increment_wrap_at_page_boundary(uint16_t *value); void decrement_wrap_at_page_boundary(uint16_t *value); uint16_t direct_page_add(uint16_t offset); diff --git a/src/endian.h b/src/endian.h new file mode 100644 index 0000000..03a7278 --- /dev/null +++ b/src/endian.h @@ -0,0 +1,21 @@ +#pragma once + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define LOW_HIGH_UNION(name, low, high) \ + union { \ + struct { \ + uint8_t low; \ + uint8_t high; \ + }; \ + uint16_t name; \ + } +#else +#define LOW_HIGH_UNION(name, low, high) \ + union { \ + struct { \ + uint8_t high; \ + uint8_t low; \ + }; \ + uint16_t name; \ + } +#endif diff --git a/src/glue.h b/src/glue.h index 813f841..ad61f23 100644 --- a/src/glue.h +++ b/src/glue.h @@ -79,6 +79,9 @@ extern bool has_via2; extern uint32_t host_sample_rate; extern bool enable_midline; +extern bool has_midi_card; +extern uint16_t midi_card_addr; + extern void machine_dump(const char* reason); extern void machine_reset(); extern void machine_nmi(); diff --git a/src/main.c b/src/main.c index b7b0a1f..2a15a4d 100644 --- a/src/main.c +++ b/src/main.c @@ -42,6 +42,7 @@ #include "wav_recorder.h" #include "testbench.h" #include "cartridge.h" +#include "midi.h" #ifdef __EMSCRIPTEN__ #include @@ -130,6 +131,9 @@ bool enable_midline = false; bool ym2151_irq_support = false; char *cartridge_path = NULL; +bool has_midi_card = false; +uint16_t midi_card_addr; + bool using_hostfs = true; uint8_t MHZ = 8; @@ -332,6 +336,7 @@ machine_reset() video_reset(); mouse_state_init(); reset6502(regs.is65c816); + midi_serial_init(); } void @@ -526,6 +531,16 @@ usage() printf("\tSuppress warning emitted when encountering a Rockwell extension on the 65C02\n"); printf("-longpwron\n"); printf("\tSimulate a long press of the power button at system power-on.\n"); + printf("-midicard [
]\n"); + printf("\tInstall a serial MIDI card at the specified address, or at $9F60 by default.\n"); + printf("\tThe -sf2 option must be specified along with this option.\n"); + printf("-sf2 \n"); + printf("\tInitialize MIDI synth with the specified SoundFont.\n"); + printf("\tThe -midicard option must be specified along with this option.\n"); + printf("-midi-in\n"); + printf("\tConnect the system MIDI input devices to the input of the first UART\n"); + printf("\tof the emulated MIDI card. The -midicard option is required for this\n"); + printf("\toption to have any effect.\n"); #ifdef TRACE printf("-trace [
]\n"); printf("\tPrint instruction trace. Optionally, a trigger address\n"); @@ -547,6 +562,36 @@ usage_keymap() exit(1); } +void no_fluidsynth_warning(void) +{ + static bool already_warned; + + if (!already_warned) { + fprintf(stderr, "\nWarning: x16emu was built without FluidSynth support,\n"); + fprintf(stderr, "so the MIDI synth will be inoperative.\n\n"); +#if defined(__linux__) + fprintf(stderr, "To build x16emu with fluidsynth support, you distro may\n"); + fprintf(stderr, "have a libfluidsynth-dev or fluidsynth-devel package that\n"); + fprintf(stderr, "needs to be installed before building x16emu.\n\n"); +#elif defined(__APPLE__) + fprintf(stderr, "To build x16emu with fluidsynth support,\n"); + fprintf(stderr, "install the homebrew package fluid-synth before\n"); + fprintf(stderr, "building x16emu.\n\n"); +#elif defined(_WIN64) + fprintf(stderr, "To build x16emu with fluidsynth support under MSYS2,\n"); + fprintf(stderr, "install the mingw-w64-x86_64-fluidsynth package before\n"); + fprintf(stderr, "building x16emu.\n\n"); +#elif defined(_WIN32) + fprintf(stderr, "To build x16emu with fluidsynth support under MSYS2,\n"); + fprintf(stderr, "install the mingw-w64-i686-fluidsynth package before\n"); + fprintf(stderr, "building x16emu.\n\n"); +#endif + fprintf(stderr, "Then build x16emu with FLUIDSYNTH=1. For example:\n"); + fprintf(stderr, "FLUIDSYNTH=1 make\n"); + already_warned = true; + } +} + int main(int argc, char **argv) { @@ -556,6 +601,7 @@ main(int argc, char **argv) char *rom_path = rom_path_data; char *prg_path = NULL; char *bas_path = NULL; + char *sf2_path = NULL; char *sdcard_path = NULL; bool run_test = false; int test_number = 0; @@ -630,6 +676,40 @@ main(int argc, char **argv) prg_path = argv[0]; argc--; argv++; + } else if (!strcmp(argv[0], "-midicard")) { +#ifndef HAS_FLUIDSYNTH + no_fluidsynth_warning(); +#endif + argc--; + argv++; + has_midi_card = true; + if (argc && argv[0][0] != '-') { + midi_card_addr = 0x9f00 | ((uint16_t)strtol(argv[0], NULL, 16) & 0xff); + midi_card_addr &= 0xfff0; + argc--; + argv++; + } else { + midi_card_addr = 0x9f60; + } + } else if (!strcmp(argv[0], "-sf2")) { +#ifndef HAS_FLUIDSYNTH + no_fluidsynth_warning(); +#endif + argc--; + argv++; + if (!argc || argv[0][0] == '-') { + usage(); + } + sf2_path = argv[0]; + argc--; + argv++; + } else if (!strcmp(argv[0], "-midi-in")) { +#ifndef HAS_FLUIDSYNTH + no_fluidsynth_warning(); +#endif + argc--; + argv++; + fs_midi_in_connect = true; } else if (!strcmp(argv[0], "-run")) { argc--; argv++; @@ -1088,6 +1168,18 @@ main(int argc, char **argv) #endif } + if (sf2_path && has_midi_card) { + if (midi_card_addr < 0x9f60) { + fprintf(stderr, "Warning: Serial MIDI card address must be in the range of 9F60-9FF0\n"); + } else { + midi_init(); + midi_load_sf2((uint8_t *)sf2_path); + } + } else if (sf2_path || has_midi_card) { + fprintf(stderr, "Warning: -sf2 and -midicard must be specified together in order to enable the MIDI synth.\n"); + has_midi_card = false; + } + if (cartridge_path) { if (!cartridge_load(cartridge_path, !zeroram)) { printf("Cannot open %s!\n", cartridge_path); @@ -1587,6 +1679,8 @@ emulator_loop(void *param) audio_step(clocks); } + midi_serial_step(clocks); + if (!headless && new_frame) { if (nvram_dirty && nvram_path) { SDL_RWops *f = SDL_RWFromFile(nvram_path, "wb"); @@ -1615,7 +1709,7 @@ emulator_loop(void *param) audio_render(); } - if (video_get_irq_out() || via1_irq() || (has_via2 && via2_irq()) || (ym2151_irq_support && YM_irq())) { + if (video_get_irq_out() || via1_irq() || (has_via2 && via2_irq()) || (ym2151_irq_support && YM_irq()) || (has_midi_card && midi_serial_irq())) { // printf("IRQ!\n"); irq6502(); } diff --git a/src/memory.c b/src/memory.c index 5826d13..1e0cc65 100644 --- a/src/memory.c +++ b/src/memory.c @@ -18,6 +18,7 @@ #include "audio.h" #include "cartridge.h" #include "iso_8859_15.h" +#include "midi.h" uint8_t ram_bank; uint8_t rom_bank; @@ -200,6 +201,9 @@ real_read6502(uint16_t address, bool debugOn, int16_t bank) } else if (address >= 0x9fb0 && address < 0x9fc0) { // emulator state return emu_read(address & 0xf, debugOn); + } else if (has_midi_card && (address & 0xfff0) == midi_card_addr) { + // midi card + return midi_serial_read(address & 0xf, debugOn); } else { // future expansion return 0x9f; // open bus read @@ -277,6 +281,8 @@ write6502(uint16_t address, uint8_t value) } else if (address >= 0x9fb0 && address < 0x9fc0) { // emulator state emu_write(address & 0xf, value); + } else if (has_midi_card && (address & 0xfff0) == midi_card_addr) { + midi_serial_write(address & 0xf, value); } else { // future expansion } diff --git a/src/midi.c b/src/midi.c new file mode 100644 index 0000000..5e79bd7 --- /dev/null +++ b/src/midi.c @@ -0,0 +1,963 @@ +// Commander X16 Emulator +// Copyright (c) 2024 MooingLemur +// All rights reserved. License: 2-clause BSD + +#include +#include "glue.h" +#include "midi.h" +#include "audio.h" +#include "endian.h" + +#ifdef _WIN32 + #include + #define LIBRARY_TYPE HMODULE + #define LOAD_LIBRARY(name) LoadLibrary(name) + #define GET_FUNCTION(lib, name) (void *)GetProcAddress(lib, name) + #define CLOSE_LIBRARY(lib) FreeLibrary(lib) +#else + #include + #define LIBRARY_TYPE void* + #define LOAD_LIBRARY(name) dlopen(name, RTLD_LAZY) + #define GET_FUNCTION(lib, name) dlsym(lib, name) + #define CLOSE_LIBRARY(lib) dlclose(lib) +#endif + +#define ASSIGN_FUNCTION(lib, var, name) {\ + var = GET_FUNCTION(lib, name);\ + if (!var) { fprintf(stderr, "Unable to find symbol for '%s'\n", name); CLOSE_LIBRARY(handle); return; }\ +} + +enum MIDI_states { + MSTATE_Normal, + MSTATE_Param, + MSTATE_SysEx, +}; + +struct midi_serial_regs +{ + LOW_HIGH_UNION(dl, dll, dlm); + + bool ier_erbi; + bool ier_etbei; + bool ier_elsi; + bool ier_edssi; + + uint8_t iir; + + bool fcr_fifo_enable; + uint8_t fcr_ififo_trigger_level_bytes; + + uint8_t lcr_word_length_bits; + bool lcr_stb; + bool lcr_pen; + bool lcr_eps; + bool lcr_stick; + bool lcr_break; + bool lcr_dlab; + + bool mcr_dtr; + bool mcr_rts; + bool mcr_out1; + bool mcr_out2; + bool mcr_loop; + bool mcr_afe; + + bool lsr_oe; + bool lsr_pe; + bool lsr_fe; + bool lsr_bi; + bool lsr_eif; + + bool msr_dcts; + bool msr_ddsr; + bool msr_teri; + bool msr_ddcd; + bool msr_cts; + bool msr_dsr; + bool msr_ri; + bool msr_dcd; + + uint8_t obyte_bits_remain; + uint8_t ibyte_bits_remain; + uint8_t rx_timeout; + bool rx_timeout_enabled; + + bool thre_intr; + uint8_t thre_bits_remain; + + uint8_t scratch; + uint8_t ififo[16]; + uint8_t ifsz; + uint8_t ofifo[16]; + uint8_t ofsz; + + int64_t clock; // 40.24 fixed point + int32_t clockdec; // 8.24 fixed point + + time_t last_warning; + + uint8_t midi_event_fifo[256]; + int mfsz; + uint8_t in_midi_last_command; + + pthread_mutex_t fifo_mutex; + pthread_mutexattr_t fifo_mutex_attr; +}; + +struct midi_serial_regs mregs[2]; +static bool serial_midi_mutexes_initialized = false; + +void midi_serial_iir_check(uint8_t sel); +bool fs_midi_in_connect = false; + +#ifdef HAS_FLUIDSYNTH + +int handle_midi_event(void* data, fluid_midi_event_t* event); + +static uint8_t sysex_buffer[1024]; +static int sysex_bufptr; + +static enum MIDI_states midi_state[2] = {MSTATE_Normal, MSTATE_Normal}; +static uint8_t out_midi_last_command[2] = {0, 0}; +static uint8_t out_midi_first_param[2]; + +static bool midi_initialized = false; + +static fluid_settings_t* fl_settings; +static fluid_midi_driver_t* fl_mdriver; +static fluid_synth_t* fl_synth; +static int fl_sf2id; + +typedef fluid_settings_t* (*new_fluid_settings_f_t)(void); +typedef fluid_synth_t* (*new_fluid_synth_f_t)(fluid_settings_t*); +typedef fluid_audio_driver_t* (*new_fluid_audio_driver_f_t)(fluid_settings_t*, fluid_synth_t*); +typedef fluid_midi_driver_t* (*new_fluid_midi_driver_f_t)(fluid_settings_t*, handle_midi_event_func_t, void*); +typedef int (*fluid_settings_setnum_f_t)(fluid_settings_t*,const char*, double); +typedef int (*fluid_settings_setint_f_t)(fluid_settings_t*,const char*, int); +typedef int (*fluid_settings_setstr_f_t)(fluid_settings_t*,const char*, const char*); +typedef int (*fluid_synth_sfload_f_t)(fluid_synth_t*, const char *, int); +typedef int (*fluid_synth_program_change_f_t)(fluid_synth_t*, int, int); +typedef int (*fluid_synth_channel_pressure_f_t)(fluid_synth_t*, int, int); +typedef int (*fluid_synth_system_reset_f_t)(fluid_synth_t*); +typedef int (*fluid_synth_noteoff_f_t)(fluid_synth_t*, int, int); +typedef int (*fluid_synth_noteon_f_t)(fluid_synth_t*, int, int, int); +typedef int (*fluid_synth_key_pressure_f_t)(fluid_synth_t*, int, int, int); +typedef int (*fluid_synth_cc_f_t)(fluid_synth_t*, int, int, int); +typedef int (*fluid_synth_pitch_bend_f_t)(fluid_synth_t*, int, int); +typedef int (*fluid_synth_sysex_f_t)(fluid_synth_t*, const char*, int, char*, int*, int*, int); +typedef int (*fluid_synth_write_s16_f_t)(fluid_synth_t*, int, void*, int, int, void*, int, int); +typedef int (*fluid_midi_event_get_channel_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_get_control_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_get_key_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_get_lyrics_f_t)(const fluid_midi_event_t*, void**, int*); +typedef int (*fluid_midi_event_get_pitch_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_get_program_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_get_text_f_t)(const fluid_midi_event_t*, void**, int*); +typedef int (*fluid_midi_event_get_type_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_get_value_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_get_velocity_f_t)(const fluid_midi_event_t*); +typedef int (*fluid_midi_event_set_type_f_t)(fluid_midi_event_t*, int); + +static new_fluid_settings_f_t dl_new_fluid_settings; +static new_fluid_synth_f_t dl_new_fluid_synth; +static new_fluid_audio_driver_f_t dl_new_fluid_audio_driver; +static new_fluid_midi_driver_f_t dl_new_fluid_midi_driver; +static fluid_settings_setnum_f_t dl_fluid_settings_setnum; +static fluid_settings_setint_f_t dl_fluid_settings_setint; +static fluid_settings_setstr_f_t dl_fluid_settings_setstr; +static fluid_synth_sfload_f_t dl_fs_sfload; +static fluid_synth_program_change_f_t dl_fs_program_change; +static fluid_synth_channel_pressure_f_t dl_fs_channel_pressure; +static fluid_synth_system_reset_f_t dl_fs_system_reset; +static fluid_synth_noteoff_f_t dl_fs_noteoff; +static fluid_synth_noteon_f_t dl_fs_noteon; +static fluid_synth_key_pressure_f_t dl_fs_key_pressure; +static fluid_synth_cc_f_t dl_fs_cc; +static fluid_synth_pitch_bend_f_t dl_fs_pitch_bend; +static fluid_synth_sysex_f_t dl_fs_sysex; +static fluid_synth_write_s16_f_t dl_fs_write_s16; +static fluid_midi_event_get_channel_f_t dl_fluid_midi_event_get_channel; +static fluid_midi_event_get_control_f_t dl_fluid_midi_event_get_control; +static fluid_midi_event_get_key_f_t dl_fluid_midi_event_get_key; +static fluid_midi_event_get_lyrics_f_t dl_fluid_midi_event_get_lyrics; +static fluid_midi_event_get_pitch_f_t dl_fluid_midi_event_get_pitch; +static fluid_midi_event_get_program_f_t dl_fluid_midi_event_get_program; +static fluid_midi_event_get_text_f_t dl_fluid_midi_event_get_text; +static fluid_midi_event_get_type_f_t dl_fluid_midi_event_get_type; +static fluid_midi_event_get_value_f_t dl_fluid_midi_event_get_value; +static fluid_midi_event_get_velocity_f_t dl_fluid_midi_event_get_velocity; +static fluid_midi_event_set_type_f_t dl_fluid_midi_event_set_type; + + +void midi_init() +{ + if (midi_initialized) { + return; + } + +#ifdef _WIN32 + LIBRARY_TYPE handle = LOAD_LIBRARY("libfluidsynth-3.dll"); +#elif __APPLE__ + LIBRARY_TYPE handle = LOAD_LIBRARY("libfluidsynth.dylib"); +#else + LIBRARY_TYPE handle = LOAD_LIBRARY("libfluidsynth.so.3"); +#endif + + if (!handle) { + // Handle the error on both platforms +#ifdef _WIN32 + fprintf(stderr, "Could not load MIDI synth library: error code %lu\n", GetLastError()); +#else + fprintf(stderr, "Could not load MIDI synth library: %s\n", dlerror()); +#endif + return; + } + +#ifndef _WIN32 + dlerror(); +#endif + + ASSIGN_FUNCTION(handle, dl_new_fluid_settings, "new_fluid_settings"); + ASSIGN_FUNCTION(handle, dl_new_fluid_synth, "new_fluid_synth"); + ASSIGN_FUNCTION(handle, dl_new_fluid_audio_driver, "new_fluid_audio_driver"); + ASSIGN_FUNCTION(handle, dl_new_fluid_midi_driver, "new_fluid_midi_driver"); + ASSIGN_FUNCTION(handle, dl_fluid_settings_setnum, "fluid_settings_setnum"); + ASSIGN_FUNCTION(handle, dl_fluid_settings_setint, "fluid_settings_setint"); + ASSIGN_FUNCTION(handle, dl_fluid_settings_setstr, "fluid_settings_setstr"); + ASSIGN_FUNCTION(handle, dl_fs_sfload, "fluid_synth_sfload"); + ASSIGN_FUNCTION(handle, dl_fs_program_change, "fluid_synth_program_change"); + ASSIGN_FUNCTION(handle, dl_fs_channel_pressure, "fluid_synth_channel_pressure"); + ASSIGN_FUNCTION(handle, dl_fs_system_reset, "fluid_synth_system_reset"); + ASSIGN_FUNCTION(handle, dl_fs_noteoff, "fluid_synth_noteoff"); + ASSIGN_FUNCTION(handle, dl_fs_noteon, "fluid_synth_noteon"); + ASSIGN_FUNCTION(handle, dl_fs_key_pressure, "fluid_synth_key_pressure"); + ASSIGN_FUNCTION(handle, dl_fs_cc, "fluid_synth_cc"); + ASSIGN_FUNCTION(handle, dl_fs_pitch_bend, "fluid_synth_pitch_bend"); + ASSIGN_FUNCTION(handle, dl_fs_sysex, "fluid_synth_sysex"); + ASSIGN_FUNCTION(handle, dl_fs_write_s16, "fluid_synth_write_s16"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_channel, "fluid_midi_event_get_channel"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_control, "fluid_midi_event_get_control"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_key, "fluid_midi_event_get_key"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_lyrics, "fluid_midi_event_get_lyrics"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_pitch, "fluid_midi_event_get_pitch"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_program, "fluid_midi_event_get_program"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_text, "fluid_midi_event_get_text"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_type, "fluid_midi_event_get_type"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_value, "fluid_midi_event_get_value"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_get_velocity, "fluid_midi_event_get_velocity"); + ASSIGN_FUNCTION(handle, dl_fluid_midi_event_set_type, "fluid_midi_event_set_type"); + + fl_settings = dl_new_fluid_settings(); + dl_fluid_settings_setnum(fl_settings, "synth.sample-rate", + AUDIO_SAMPLERATE); + dl_fluid_settings_setnum(fl_settings, "synth.gain", FL_DEFAULT_GAIN); + dl_fluid_settings_setstr(fl_settings, "midi.portname", "Commander X16 Emulator"); + dl_fluid_settings_setint(fl_settings, "midi.autoconnect", fs_midi_in_connect); + fl_synth = dl_new_fluid_synth(fl_settings); + if (fs_midi_in_connect) { + fl_mdriver = dl_new_fluid_midi_driver(fl_settings, handle_midi_event, &mregs[0]); + } + + midi_initialized = true; + fprintf(stderr, "Initialized MIDI synth at $%04X.\n", midi_card_addr); +} + +void midi_load_sf2(uint8_t* filename) +{ + if (!midi_initialized) return; + fl_sf2id = dl_fs_sfload(fl_synth, (const char *)filename, true); + if (fl_sf2id == FLUID_FAILED) { + fprintf(stderr, "Unable to load soundfont.\n"); + } +} + +// Receive a byte from client +// Store state, or dispatch event +void midi_byte_out(uint8_t sel, uint8_t b) +{ + if (!midi_initialized) return; + switch (midi_state[sel]) { + case MSTATE_Normal: + if (b < 0x80) { + if ((out_midi_last_command[sel] & 0xf0) == 0xc0) { // patch change + dl_fs_program_change(fl_synth, out_midi_last_command[sel] & 0xf, b); + } else if ((out_midi_last_command[sel] & 0xf0) == 0xd0) { // channel pressure + dl_fs_channel_pressure(fl_synth, out_midi_last_command[sel] & 0xf, b); + } else if (out_midi_last_command[sel] >= 0x80) { // two-param command + out_midi_first_param[sel] = b; + midi_state[sel] = MSTATE_Param; + } + } else { + if (b < 0xf0) { + out_midi_last_command[sel] = b; + } else if (b == 0xf0) { + sysex_bufptr = 0; + midi_state[sel] = MSTATE_SysEx; + } else if (b == 0xff) { + dl_fs_system_reset(fl_synth); + out_midi_last_command[sel] = 0; + } + } + break; + case MSTATE_Param: + switch (out_midi_last_command[sel] & 0xf0) { + case 0x80: // note off + dl_fs_noteoff(fl_synth, out_midi_last_command[sel] & 0xf, out_midi_first_param[sel]); // no release velocity + break; + case 0x90: // note on + if (b == 0) { + dl_fs_noteoff(fl_synth, out_midi_last_command[sel] & 0xf, out_midi_first_param[sel]); + } else { + dl_fs_noteon(fl_synth, out_midi_last_command[sel] & 0xf, out_midi_first_param[sel], b); + } + break; + case 0xa0: // aftertouch + dl_fs_key_pressure(fl_synth, out_midi_last_command[sel] & 0xf, out_midi_first_param[sel], b); + break; + case 0xb0: // controller + dl_fs_cc(fl_synth, out_midi_last_command[sel] & 0xf, out_midi_first_param[sel], b); + break; + case 0xe0: // pitch bend + dl_fs_pitch_bend(fl_synth, out_midi_last_command[sel] & 0xf, ((uint16_t)out_midi_first_param[sel]) | (uint16_t)b << 7); + break; + } + midi_state[sel] = MSTATE_Normal; + break; + case MSTATE_SysEx: + if (b & 0x80) { // any command byte can terminate a SYSEX, not just 0xf7 + if (sysex_bufptr < (sizeof(sysex_buffer) / sizeof(sysex_buffer[0]))-1) { // only if buffer didn't fill + sysex_buffer[sysex_bufptr] = 0; + // Handle Master Volume SysEx + if (!strncmp((const char *)sysex_buffer, "\x7F\x7F\x04\x01\x00", sysex_bufptr-1)) { + dl_fluid_settings_setnum(fl_settings, "synth.gain", FL_DEFAULT_GAIN*((float)sysex_buffer[sysex_bufptr-1]/127)); + } + dl_fs_sysex(fl_synth, (const char *)sysex_buffer, sysex_bufptr, NULL, NULL, NULL, 0); + } + midi_state[sel] = MSTATE_Normal; + return midi_byte_out(sel, b); + } else { + sysex_buffer[sysex_bufptr] = b; + // we can't do much about a runaway sysex other than continue to absorb it + // but we throw it all away later if the buffer filled + if (sysex_bufptr < (sizeof(sysex_buffer) / sizeof(sysex_buffer[0]))-1) { + sysex_bufptr++; + } + } + break; + } +} + +void midi_synth_render(int16_t* buf, int len) +{ + if (midi_initialized) { + dl_fs_write_s16(fl_synth, len, buf, 0, 2, buf, 1, 2); + } else { + memset(buf, 0, len * 2 * sizeof(int16_t)); + } +} + +void midi_event_enqueue_byte(struct midi_serial_regs* mrp, uint8_t val) +{ + if (mrp->mfsz >= 256) return; + mrp->midi_event_fifo[mrp->mfsz++] = val; +} + +void midi_event_enqueue_short(struct midi_serial_regs* mrp, uint8_t cmd, uint8_t val) +{ + if (mrp->mfsz >= 255) { + mrp->in_midi_last_command = 0; + return; + } + if (mrp->in_midi_last_command != cmd) { + mrp->midi_event_fifo[mrp->mfsz++] = cmd; + mrp->in_midi_last_command = cmd; + } + mrp->midi_event_fifo[mrp->mfsz++] = val; +} + +void midi_event_enqueue_normal(struct midi_serial_regs* mrp, uint8_t cmd, uint8_t key, uint8_t val) +{ + if (mrp->mfsz >= 254) { + mrp->in_midi_last_command = 0; + return; + } + if (mrp->in_midi_last_command != cmd) { + mrp->midi_event_fifo[mrp->mfsz++] = cmd; + mrp->in_midi_last_command = cmd; + } + mrp->midi_event_fifo[mrp->mfsz++] = key; + mrp->midi_event_fifo[mrp->mfsz++] = val; +} + +void midi_event_enqueue_sysex(struct midi_serial_regs* mrp, uint8_t *bufptr, int buflen) +{ + int i; + + mrp->in_midi_last_command = 0; + if (mrp->mfsz >= 255L - (buflen + 2)) { // too full + return; + } + + mrp->midi_event_fifo[mrp->mfsz++] = 0xf0; + for (i = 0; i < buflen; i++) { + mrp->midi_event_fifo[mrp->mfsz++] = bufptr[i]; + } + mrp->midi_event_fifo[mrp->mfsz++] = 0xf7; +} + +int handle_midi_event(void* data, fluid_midi_event_t* event) +{ + struct midi_serial_regs* mrp = (struct midi_serial_regs*)data; + pthread_mutex_lock(&mrp->fifo_mutex); + + uint8_t type = dl_fluid_midi_event_get_type(event); + uint8_t chan = dl_fluid_midi_event_get_channel(event); + uint8_t cmd = (type < 0x80 || type >= 0xf0) ? type : (type | (chan & 0xf)); + uint8_t key, val; + uint8_t *bufptr; + int buflen; + + switch (type) { + case FS_NOTE_OFF: + case FS_NOTE_ON: + key = dl_fluid_midi_event_get_key(event); + val = dl_fluid_midi_event_get_velocity(event); + midi_event_enqueue_normal(mrp, cmd, key, val); + break; + case FS_KEY_PRESSURE: + key = dl_fluid_midi_event_get_key(event); + val = dl_fluid_midi_event_get_value(event); + midi_event_enqueue_normal(mrp, cmd, key, val); + break; + case FS_CONTROL_CHANGE: + key = dl_fluid_midi_event_get_control(event); + val = dl_fluid_midi_event_get_value(event); + midi_event_enqueue_normal(mrp, cmd, key, val); + break; + case FS_PITCH_BEND: + key = dl_fluid_midi_event_get_pitch(event) & 0x7f; + val = (dl_fluid_midi_event_get_pitch(event) >> 7) & 0x7f; + midi_event_enqueue_normal(mrp, cmd, key, val); + break; + case FS_PROGRAM_CHANGE: + case FS_CHANNEL_PRESSURE: + val = dl_fluid_midi_event_get_program(event); + midi_event_enqueue_short(mrp, cmd, val); + break; + case FS_MIDI_TIME_CODE: + val = dl_fluid_midi_event_get_value(event); + midi_event_enqueue_short(mrp, type, val); + mrp->in_midi_last_command = 0; + break; + case FS_MIDI_TUNE_REQUEST: + case 0xF4: + case 0xF5: + midi_event_enqueue_byte(mrp, type); + mrp->in_midi_last_command = 0; + break; + case FS_MIDI_SYNC: + case FS_MIDI_TICK: + case FS_MIDI_START: + case FS_MIDI_CONTINUE: + case FS_MIDI_STOP: + case FS_MIDI_ACTIVE_SENSING: + case FS_MIDI_SYSTEM_RESET: + midi_event_enqueue_byte(mrp, type); + break; + case FS_MIDI_SYSEX: + // FluidSynth doesn't offer a get_sysex function, but internally + // a text event is equivalent to a sysex event, in terms of what + // parts of the event structure are populated + // + // Unfortunately that means we have to fool it in order to + // access the data. + dl_fluid_midi_event_set_type(event, FS_MIDI_TEXT); + if (dl_fluid_midi_event_get_text(event, (void **)&bufptr, &buflen) == FLUID_OK && bufptr != NULL) { + midi_event_enqueue_sysex(mrp, bufptr, buflen); + } + break; + } + + //fprintf(stderr, "Debug: MIDI IN: Type: %02X Chan: %02X\n", type, chan); + + pthread_mutex_unlock(&mrp->fifo_mutex); + return FLUID_OK; +} + + +#else +void midi_load_sf2(uint8_t* filename) +{ + // no-op +} + +void midi_init() +{ + fprintf(stderr, "No FluidSynth support.\n"); +} + +void midi_byte_out(uint8_t sel, uint8_t b) +{ + // no-op +} + +void midi_synth_render(int16_t* buf, int len) +{ + // no synth, return zeroed buffer + memset(buf, 0, len * 2 * sizeof(int16_t)); +} + +#endif + +void midi_serial_init() +{ + uint8_t sel; + + for (sel=0; sel<2; sel++) { + mregs[sel].ier_erbi = false; + mregs[sel].ier_etbei = false; + mregs[sel].ier_elsi = false; + mregs[sel].ier_edssi = false; + + mregs[sel].iir = 0x01; + + mregs[sel].fcr_fifo_enable = false; + mregs[sel].fcr_ififo_trigger_level_bytes = 1; + + mregs[sel].lcr_word_length_bits = 5; + mregs[sel].lcr_stb = false; + mregs[sel].lcr_pen = false; + mregs[sel].lcr_eps = false; + mregs[sel].lcr_stick = false; + mregs[sel].lcr_break = false; + mregs[sel].lcr_dlab = false; + + mregs[sel].mcr_dtr = false; + mregs[sel].mcr_rts = false; + mregs[sel].mcr_out1 = false; + mregs[sel].mcr_out2 = false; + mregs[sel].mcr_loop = false; + mregs[sel].mcr_afe = false; + + mregs[sel].lsr_oe = false; + mregs[sel].lsr_pe = false; + mregs[sel].lsr_fe = false; + mregs[sel].lsr_bi = false; + mregs[sel].lsr_eif = false; + + mregs[sel].msr_dcts = false; + mregs[sel].msr_ddsr = false; + mregs[sel].msr_teri = false; + mregs[sel].msr_ddcd = false; + + mregs[sel].msr_cts = false; + mregs[sel].msr_dsr = false; + mregs[sel].msr_ri = false; + mregs[sel].msr_dcd = false; + + mregs[sel].obyte_bits_remain = 0; + mregs[sel].rx_timeout = 0; + mregs[sel].rx_timeout_enabled = false; + mregs[sel].thre_intr = false; + mregs[sel].thre_bits_remain = 0; + + mregs[sel].ibyte_bits_remain = 7; + + mregs[sel].ifsz = 0; + mregs[sel].ififo[0] = 0; + mregs[sel].ofsz = 0; + mregs[sel].clock = 0; + mregs[sel].clockdec = 0; + mregs[sel].last_warning = 0; + + mregs[sel].mfsz = 0; + mregs[sel].in_midi_last_command = 0; + } + + if (!serial_midi_mutexes_initialized) { + for (sel=0; sel<2; sel++) { + pthread_mutexattr_init(&mregs[sel].fifo_mutex_attr); + pthread_mutex_init(&mregs[sel].fifo_mutex, &mregs[sel].fifo_mutex_attr); + } + serial_midi_mutexes_initialized = true; + } +} + +void midi_serial_iir_check(uint8_t sel) +{ + uint8_t fifoen = (uint8_t)mregs[sel].fcr_fifo_enable << 6 | (uint8_t)mregs[sel].fcr_fifo_enable << 7; + if (mregs[sel].ier_elsi && (mregs[sel].lsr_oe || mregs[sel].lsr_pe || mregs[sel].lsr_fe || mregs[sel].lsr_bi)) { + mregs[sel].iir = (0x06 | fifoen); // Receiver line status interrupt + } else if (mregs[sel].ier_erbi && !mregs[sel].fcr_fifo_enable && mregs[sel].ifsz > 0) { + mregs[sel].iir = (0x04 | fifoen); // Received data available interrupt (16450 mode) + } else if (mregs[sel].ier_erbi && mregs[sel].fcr_fifo_enable && mregs[sel].ifsz >= mregs[sel].fcr_ififo_trigger_level_bytes) { + mregs[sel].iir = (0x04 | fifoen); // Received data available interrupt (16550 mode) + } else if (mregs[sel].ier_erbi && mregs[sel].fcr_fifo_enable && mregs[sel].ifsz > 0 && mregs[sel].rx_timeout_enabled && mregs[sel].rx_timeout == 0) { + mregs[sel].iir = (0x0c | fifoen); // Received data available interrupt (timeout, 16550 mode) + } else if (mregs[sel].ier_etbei && mregs[sel].thre_intr) { + mregs[sel].iir = (0x02 | fifoen); // Transmitter holding register empty interrupt + } else if (mregs[sel].ier_edssi && ((!mregs[sel].mcr_afe && mregs[sel].msr_dcts) || mregs[sel].msr_ddcd || mregs[sel].msr_ddsr || mregs[sel].msr_teri)) { + mregs[sel].iir = (0x00 | fifoen); // Modem status register interrupt + } else { + mregs[sel].iir = (0x01 | fifoen); // no interrupt waiting + } +} + +void midi_serial_step(int clocks) +{ + uint8_t sel, i; + for (sel=0; sel<2; sel++) { + mregs[sel].clock -= (int64_t)mregs[sel].clockdec * clocks; + while (mregs[sel].clock < 0) { + // process uart + pthread_mutex_lock(&mregs[sel].fifo_mutex); + + if (mregs[sel].obyte_bits_remain > 0) { + mregs[sel].obyte_bits_remain--; + } + if (mregs[sel].ofsz > 0) { + if (mregs[sel].obyte_bits_remain == 0) { + midi_byte_out(sel, mregs[sel].ofifo[0]); + mregs[sel].ofsz--; + if (mregs[sel].ofsz > 0) { + for (i=0; i 0) { + mregs[sel].thre_bits_remain--; + if (mregs[sel].thre_bits_remain == 0) { + mregs[sel].thre_intr = true; + } + } + } + + if (mregs[sel].rx_timeout > 0) { + mregs[sel].rx_timeout--; + } + + if (mregs[sel].ibyte_bits_remain > 0 && mregs[sel].mfsz > 0) { + mregs[sel].ibyte_bits_remain--; + } + if (mregs[sel].ibyte_bits_remain == 0 && mregs[sel].mfsz > 0) { + if (mregs[sel].ifsz < (mregs[sel].fcr_fifo_enable ? 16 : 1)) { + mregs[sel].ififo[mregs[sel].ifsz++] = mregs[sel].midi_event_fifo[0]; + mregs[sel].rx_timeout_enabled = true; + mregs[sel].rx_timeout = 4 * (2 + mregs[sel].lcr_word_length_bits + mregs[sel].lcr_stb + mregs[sel].lcr_pen); + } else { + mregs[sel].lsr_oe = true; // inbound FIFO overflow + if (!mregs[sel].fcr_fifo_enable) { + // RBR is overwritten by RSR in this mode + // whenever overflow happens + mregs[sel].ififo[mregs[sel].ifsz-1] = mregs[sel].midi_event_fifo[0]; + } + } + mregs[sel].mfsz--; + if (mregs[sel].mfsz > 0) { + for (i=0; i 0) { + mregs[sel].ifsz--; + if (mregs[sel].ifsz == 0) { + mregs[sel].rx_timeout_enabled = false; + } else { + for (i=0; i> 3; + switch (reg & 7) { + case 0x0: + if (mregs[sel].lcr_dlab) { + return mregs[sel].dll; + } else { + if (debugOn) { + return mregs[sel].ififo[0]; + } else { + return midi_serial_dequeue_ibyte(sel); + } + } + break; + case 0x1: + if (mregs[sel].lcr_dlab) { + return mregs[sel].dlm; + } else { + return (((uint8_t)mregs[sel].ier_edssi << 3) | + ((uint8_t)mregs[sel].ier_elsi << 2) | + ((uint8_t)mregs[sel].ier_etbei << 1) | + ((uint8_t)mregs[sel].ier_erbi)); + } + break; + case 0x2: { + uint8_t ret = mregs[sel].iir; + if (!debugOn) { + mregs[sel].thre_intr = false; + midi_serial_iir_check(sel); + } + return ret; + break; + } + case 0x3: + return (((uint8_t)mregs[sel].lcr_dlab << 7) | + ((uint8_t)mregs[sel].lcr_break << 6) | + ((uint8_t)mregs[sel].lcr_stick << 5) | + ((uint8_t)mregs[sel].lcr_eps << 4) | + ((uint8_t)mregs[sel].lcr_pen << 3) | + ((uint8_t)mregs[sel].lcr_stb << 2) | + ((mregs[sel].lcr_word_length_bits - 5) & 0x3)); + break; + case 0x4: + return (((uint8_t)mregs[sel].mcr_afe << 5) | + ((uint8_t)mregs[sel].mcr_loop << 4) | + ((uint8_t)mregs[sel].mcr_out2 << 3) | + ((uint8_t)mregs[sel].mcr_out1 << 2) | + ((uint8_t)mregs[sel].mcr_rts << 1) | + ((uint8_t)mregs[sel].mcr_dtr)); + break; + case 0x5: { + uint8_t ret = (((uint8_t)mregs[sel].lsr_eif << 7) | + ((uint8_t)(mregs[sel].obyte_bits_remain == 0 && mregs[sel].ofsz == 0) << 6) | + ((uint8_t)(mregs[sel].ofsz == 0) << 5) | + ((uint8_t)mregs[sel].lsr_bi << 4) | + ((uint8_t)mregs[sel].lsr_fe << 3) | + ((uint8_t)mregs[sel].lsr_pe << 2) | + ((uint8_t)mregs[sel].lsr_oe << 1) | + ((uint8_t)(mregs[sel].ifsz > 0))); + if (!debugOn) { + mregs[sel].lsr_oe = false; + mregs[sel].lsr_pe = false; + mregs[sel].lsr_fe = false; + mregs[sel].lsr_bi = false; + } + return ret; + break; + } + case 0x6: + return (((uint8_t)mregs[sel].msr_dcd << 7) | + ((uint8_t)mregs[sel].msr_ri << 6) | + ((uint8_t)mregs[sel].msr_dsr << 5) | + ((uint8_t)mregs[sel].msr_cts << 4) | + ((uint8_t)mregs[sel].msr_ddcd << 3) | + ((uint8_t)mregs[sel].msr_teri << 2) | + ((uint8_t)mregs[sel].msr_ddsr << 1) | + ((uint8_t)mregs[sel].msr_dcts)); + break; + case 0x7: + return mregs[sel].scratch; + break; + } + return 0x00; +} + +void midi_serial_calculate_clk(uint8_t sel) +{ + double uart_clks_per_cpu = (MIDI_UART_OSC_RATE_MHZ / MIDI_UART_PRIMARY_DIVIDER) / MHZ; + if (mregs[sel].dl > 0) { + uart_clks_per_cpu /= mregs[sel].dl; + mregs[sel].clockdec = uart_clks_per_cpu * 0x1000000L; // convert to 8.24 fixed point + } else { + mregs[sel].clockdec = 0; + } +} + +void midi_serial_write(uint8_t reg, uint8_t val) +{ + time_t now = time(NULL); + + //printf("midi_serial_write %d %d\n", reg, val); + uint8_t sel = (reg & 8) >> 3; + switch (reg & 7) { + case 0x0: + if (mregs[sel].lcr_dlab) { + mregs[sel].dll = val; + midi_serial_calculate_clk(sel); + } else { + if (mregs[sel].lcr_word_length_bits != 8 || mregs[sel].lcr_stb || mregs[sel].lcr_pen) { + if (mregs[sel].last_warning + 60 < now) { + unsigned char par = 'N'; + if (mregs[sel].lcr_pen) { + switch ((uint8_t)(mregs[sel].lcr_eps << 1) | (uint8_t)mregs[sel].lcr_stick) { + case 0: + par = 'O'; + break; + case 1: + par = 'M'; + break; + case 2: + par = 'E'; + break; + case 3: + par = 'S'; + break; + } + } + fprintf(stderr, "Serial MIDI: Warning: improper LCR %d%c%d for UART %d, must be set to 8N1.\n", mregs[sel].lcr_word_length_bits, par, 1+(uint8_t)mregs[sel].lcr_stb, sel); + mregs[sel].last_warning = now; + } + } else if (mregs[sel].lcr_break && mregs[sel].last_warning + 60 < now) { + fprintf(stderr, "Serial MIDI: Warning: break improperly set for UART %d.\n", sel); + mregs[sel].last_warning = now; + } else if (mregs[sel].dl != 32 && mregs[sel].last_warning + 60 < now) { + fprintf(stderr, "Serial MIDI: Warning: improper divisor %d for UART %d, must be set to 32 for standard MIDI bitrate.\n", mregs[sel].dl, sel); + mregs[sel].last_warning = now; + } else { + midi_serial_enqueue_obyte(sel, val); + } + } + break; + case 0x1: + if (mregs[sel].lcr_dlab) { + mregs[sel].dlm = val; + midi_serial_calculate_clk(sel); + } else { + mregs[sel].ier_erbi = !!(val & 1); + mregs[sel].ier_etbei = !!(val & 2); + mregs[sel].ier_elsi = !!(val & 4); + mregs[sel].ier_edssi = !!(val & 8); + } + break; + case 0x2: + if (val & 1) { + mregs[sel].fcr_fifo_enable = true; + switch ((val & 0xc0) >> 6) { + case 0: + mregs[sel].fcr_ififo_trigger_level_bytes = 1; + break; + case 1: + mregs[sel].fcr_ififo_trigger_level_bytes = 4; + break; + case 2: + mregs[sel].fcr_ififo_trigger_level_bytes = 8; + break; + case 3: + mregs[sel].fcr_ififo_trigger_level_bytes = 14; + break; + } + } else { + mregs[sel].fcr_fifo_enable = false; + mregs[sel].fcr_ififo_trigger_level_bytes = 1; + } + if (val & 2) { + mregs[sel].ifsz = 0; + } + if (val & 4) { + mregs[sel].ofsz = 0; + } + if (mregs[sel].thre_bits_remain == 0) { + mregs[sel].thre_intr = true; + } + midi_serial_iir_check(sel); + break; + case 0x3: + mregs[sel].lcr_word_length_bits = (val & 0x03)+5; + mregs[sel].lcr_stb = !!(val & 0x04); + mregs[sel].lcr_pen = !!(val & 0x08); + mregs[sel].lcr_eps = !!(val & 0x10); + mregs[sel].lcr_stick = !!(val & 0x20); + mregs[sel].lcr_break = !!(val & 0x40); + mregs[sel].lcr_dlab = !!(val & 0x80); + if (mregs[sel].mfsz == 0) { + mregs[sel].ibyte_bits_remain = 2 + mregs[sel].lcr_word_length_bits + mregs[sel].lcr_stb + mregs[sel].lcr_pen; + } + break; + case 0x4: + mregs[sel].mcr_dtr = !!(val & 0x01); + mregs[sel].mcr_rts = !!(val & 0x02); + mregs[sel].mcr_out1 = !!(val & 0x04); + mregs[sel].mcr_out2 = !!(val & 0x08); + mregs[sel].mcr_loop = !!(val & 0x10); + mregs[sel].mcr_afe = !!(val & 0x20); + + if (mregs[sel].mcr_loop) { + mregs[sel].msr_dcts |= mregs[sel].msr_cts ^ mregs[sel].mcr_dtr; + mregs[sel].msr_cts = mregs[sel].mcr_dtr; + + mregs[sel].msr_ddsr |= mregs[sel].msr_dsr ^ mregs[sel].mcr_rts; + mregs[sel].msr_dsr = mregs[sel].mcr_rts; + + if (mregs[sel].msr_ri) { + mregs[sel].msr_teri |= mregs[sel].msr_ri ^ mregs[sel].mcr_out1; + } + mregs[sel].msr_ri = mregs[sel].mcr_out1; + + mregs[sel].msr_ddcd |= mregs[sel].msr_dcd ^ mregs[sel].mcr_out2; + mregs[sel].msr_dcd = mregs[sel].mcr_out2; + + } else { + // DTR tied to RI on opposite UART, wired this way on the official X16 MIDI card + if (mregs[sel ^ 1].msr_ri && !mregs[sel].mcr_dtr) { + mregs[sel ^ 1].msr_teri = true; + } + mregs[sel ^ 1].msr_ri = mregs[sel].mcr_dtr; + } + + break; + case 0x7: + mregs[sel].scratch = val; + break; + } +} + +bool midi_serial_irq(void) +{ + bool uart0int = (mregs[0].iir & 1) == 0 && mregs[0].mcr_out2; + bool uart1int = (mregs[1].iir & 1) == 0 && mregs[1].mcr_out2; + return (uart0int | uart1int); +} diff --git a/src/midi.h b/src/midi.h new file mode 100644 index 0000000..236feb0 --- /dev/null +++ b/src/midi.h @@ -0,0 +1,47 @@ +// Commander X16 Emulator +// Copyright (c) 2024 MooingLemur +// All rights reserved. License: 2-clause BSD + +#pragma once + +#ifdef HAS_FLUIDSYNTH +#include +#endif + +#define MIDI_UART_OSC_RATE_MHZ 16.0f +#define MIDI_UART_PRIMARY_DIVIDER 16 + +#define FS_MIDI_TEXT 0x01 +#define FS_NOTE_OFF 0x80 +#define FS_NOTE_ON 0x90 +#define FS_KEY_PRESSURE 0xa0 +#define FS_CONTROL_CHANGE 0xb0 +#define FS_PROGRAM_CHANGE 0xc0 +#define FS_CHANNEL_PRESSURE 0xd0 +#define FS_PITCH_BEND 0xe0 +#define FS_MIDI_SYSEX 0xf0 +#define FS_MIDI_TIME_CODE 0xf1 +#define FS_MIDI_SONG_POSITION 0xf2 +#define FS_MIDI_SONG_SELECT 0xf3 +#define FS_MIDI_TUNE_REQUEST 0xf6 +#define FS_MIDI_EOX 0xf7 +#define FS_MIDI_SYNC 0xf8 +#define FS_MIDI_TICK 0xf9 +#define FS_MIDI_START 0xfa +#define FS_MIDI_CONTINUE 0xfb +#define FS_MIDI_STOP 0xfc +#define FS_MIDI_ACTIVE_SENSING 0xfe +#define FS_MIDI_SYSTEM_RESET 0xff + +#define FL_DEFAULT_GAIN 0.2f + +void midi_init(void); +void midi_serial_init(void); +void midi_serial_step(int clocks); +uint8_t midi_serial_read(uint8_t reg, bool debugOn); +void midi_serial_write(uint8_t reg, uint8_t val); +void midi_load_sf2(uint8_t* filename); +void midi_synth_render(int16_t* buf, int len); +bool midi_serial_irq(void); + +extern bool fs_midi_in_connect;