Roland SC-55 MIDI emulation.

This commit is contained in:
Tatsuya79 committed 2026-10-26 15:13:37 +01:00
1 parent a935187697
commit 392ffba9e7
9 files changed
+16936

No files matched your search

+2
View File
@@ -71,6 +71,8 @@ SOURCES_C += \
$(CORE_DIR)/libretro/peace.c \
$(CORE_DIR)/libretro/zipdisk.c \
$(CORE_DIR)/libretro/state.c \
$(CORE_DIR)/sc55.c \
$(CORE_DIR)/sc55_libretro.c \
$(CORE_DIR)/libretro.c
SOURCES_CXX += \
+46
View File
@@ -18,6 +18,7 @@
#include "libretro/winx68k.h"
#include "libretro/dosio.h"
#include "libretro/zipdisk.h"
#include "sc55_libretro.h"
#include "libretro/dswin.h"
#include "libretro/windraw.h"
#include "libretro/joystick.h"
@@ -302,6 +303,11 @@ static bool libretro_supports_option_categories = 0;
void midi_out_short_msg(size_t msg)
{
if (px68k_sc55_enabled())
{
px68k_sc55_midi_short(msg);
return;
}
if (libretro_supports_midi_output && midi_cb.output_enabled())
{
midi_cb.write(msg & 0xFF, 0); /* status byte */
@@ -312,6 +318,11 @@ void midi_out_short_msg(size_t msg)
void midi_out_long_msg(uint8_t *s, size_t len)
{
if (px68k_sc55_enabled())
{
px68k_sc55_midi_long(s, len);
return;
}
if (libretro_supports_midi_output && midi_cb.output_enabled())
{
int i;
@@ -320,8 +331,18 @@ void midi_out_long_msg(uint8_t *s, size_t len)
}
}
/* A non-NULL handle is required by x68k/midi.c before it will dispatch
* MIDI bytes. Use a private sentinel for the internal synthesizer: no
* external MIDI handle is involved in this case. */
static int px68k_sc55_midi_handle;
int midi_out_open(void **phmo)
{
if (px68k_sc55_enabled())
{
if (phmo) *phmo = &px68k_sc55_midi_handle;
return 0;
}
if (libretro_supports_midi_output && midi_cb.output_enabled())
{
*phmo = &midi_cb;
@@ -1399,6 +1420,29 @@ static void update_variables(int running)
update_variable_midi_interface(running);
/* The internal SC-55 is an alternative MIDI destination, independent of
* RetroArch's external MIDI interface. Search the same roots as PrBoom and
* apply the SC-55-only gain independently of the PX68K audio volume. */
{
struct retro_variable sc55_var;
struct retro_variable sc55_volume_var;
int sc55_enabled = 0;
int sc55_volume = 100;
sc55_var.key = "px68k_sc55_emulation";
sc55_var.value = NULL;
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &sc55_var) && sc55_var.value &&
!strcmp(sc55_var.value, "enabled"))
sc55_enabled = 1;
sc55_volume_var.key = "px68k_sc55_volume";
sc55_volume_var.value = NULL;
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &sc55_volume_var) && sc55_volume_var.value)
sc55_volume = atoi(sc55_volume_var.value);
px68k_sc55_set_volume(sc55_volume);
px68k_sc55_configure(sc55_enabled, retro_system_directory, retro_content_directory);
if (sc55_enabled && px68k_sc55_enabled())
Config.MIDI_SW = 1;
}
strcpy(key, "px68k_joytype");
var.key = key;
for (i = 0; i < 2; i++)
@@ -2173,6 +2217,7 @@ void retro_deinit(void)
CDROM_Cleanup();
#endif
MIDI_Cleanup();
px68k_sc55_shutdown();
WinX68k_Cleanup();
WinDraw_Cleanup();
@@ -2805,6 +2850,7 @@ void retro_run(void)
audio_samples_discard(nsamples - soundbuf_size);
}
raudio_callback(soundbuf, NULL, soundbuf_size << 2);
px68k_sc55_mix(soundbuf, (size_t)soundbuf_size);
if (libretro_supports_midi_output && midi_cb.output_enabled())
midi_cb.flush();
+67
View File
@@ -171,6 +171,73 @@ struct retro_core_option_v2_definition option_defs_us[] = {
},
"Default"
},
{
"px68k_sc55_emulation",
"Roland SC-55 Emulation",
NULL,
"Use the internal Roland SC-55 synthesizer instead of RetroArch MIDI output. Requires compatible SC-55 ROM files in the System Directory or PrBoom-compatible subfolders.",
NULL,
"audio",
{
{ "disabled", NULL },
{ "enabled", NULL },
{ NULL, NULL },
},
"disabled"
},
{
"px68k_sc55_volume",
"SC-55 Emulation Volume",
NULL,
"Adjust the volume of the internal Roland SC-55 synthesizer relative to the PX68K audio. 100% is the default level.",
NULL,
"audio",
{
{ "0", "0%" },
{ "5", "5%" },
{ "10", "10%" },
{ "15", "15%" },
{ "20", "20%" },
{ "25", "25%" },
{ "30", "30%" },
{ "35", "35%" },
{ "40", "40%" },
{ "45", "45%" },
{ "50", "50%" },
{ "55", "55%" },
{ "60", "60%" },
{ "65", "65%" },
{ "70", "70%" },
{ "75", "75%" },
{ "80", "80%" },
{ "85", "85%" },
{ "90", "90%" },
{ "95", "95%" },
{ "100", "100%" },
{ "105", "105%" },
{ "110", "110%" },
{ "115", "115%" },
{ "120", "120%" },
{ "125", "125%" },
{ "130", "130%" },
{ "135", "135%" },
{ "140", "140%" },
{ "145", "145%" },
{ "150", "150%" },
{ "155", "155%" },
{ "160", "160%" },
{ "165", "165%" },
{ "170", "170%" },
{ "175", "175%" },
{ "180", "180%" },
{ "185", "185%" },
{ "190", "190%" },
{ "195", "195%" },
{ "200", "200%" },
{ NULL, NULL },
},
"100"
},
{
"px68k_midi_output",
"MIDI Output (Restart)",
+67
View File
@@ -198,6 +198,73 @@ struct retro_core_option_v2_definition option_defs_fr[] = {
},
"Default"
},
{
"px68k_sc55_emulation",
"Roland SC-55 Emulation",
NULL,
"Use the internal Roland SC-55 synthesizer instead of RetroArch MIDI output. Requires compatible SC-55 ROM files in the System Directory or PrBoom-compatible subfolders.",
NULL,
"audio",
{
{ "disabled", NULL },
{ "enabled", NULL },
{ NULL, NULL },
},
"disabled"
},
{
"px68k_sc55_volume",
"SC-55 Emulation Volume",
NULL,
"Adjust the volume of the internal Roland SC-55 synthesizer relative to the PX68K audio. 100% is the default level.",
NULL,
"audio",
{
{ "0", "0%" },
{ "5", "5%" },
{ "10", "10%" },
{ "15", "15%" },
{ "20", "20%" },
{ "25", "25%" },
{ "30", "30%" },
{ "35", "35%" },
{ "40", "40%" },
{ "45", "45%" },
{ "50", "50%" },
{ "55", "55%" },
{ "60", "60%" },
{ "65", "65%" },
{ "70", "70%" },
{ "75", "75%" },
{ "80", "80%" },
{ "85", "85%" },
{ "90", "90%" },
{ "95", "95%" },
{ "100", "100%" },
{ "105", "105%" },
{ "110", "110%" },
{ "115", "115%" },
{ "120", "120%" },
{ "125", "125%" },
{ "130", "130%" },
{ "135", "135%" },
{ "140", "140%" },
{ "145", "145%" },
{ "150", "150%" },
{ "155", "155%" },
{ "160", "160%" },
{ "165", "165%" },
{ "170", "170%" },
{ "175", "175%" },
{ "180", "180%" },
{ "185", "185%" },
{ "190", "190%" },
{ "195", "195%" },
{ "200", "200%" },
{ NULL, NULL },
},
"100"
},
{
"px68k_midi_output",
"MIDI Output (Restart)",
File diff suppressed because it is too large. Load diff
+7910
View File
File diff suppressed because it is too large. Load diff
+68
View File
@@ -0,0 +1,68 @@
/*
* Roland SC-55 emulation: a C89 single-file port of the Nuked-SC55
* backend. See sc55.c for the copyright notices.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*/
#ifndef SC55_H
#define SC55_H
#include <stddef.h>
#include <stdint.h>
typedef struct sc55 sc55_t;
enum
{
SC55_MODEL_MK2, /* SC-55mkII */
SC55_MODEL_MK1, /* SC-55 */
SC55_MODEL_ST, /* SC-55ST */
SC55_MODEL_SC155, /* SC-155 */
SC55_MODEL_CM300, /* CM-300, SCC-1 */
SC55_MODEL_SCB55, /* SCB-55 */
SC55_MODEL_RLP3237, /* RLP-3237 */
SC55_MODEL_COUNT
};
enum
{
SC55_ROM_ROM1,
SC55_ROM_ROM2,
SC55_ROM_SMROM, /* sub-MCU: mkII and ST */
SC55_ROM_WAVEROM1,
SC55_ROM_WAVEROM2,
SC55_ROM_WAVEROM3,
SC55_ROM_COUNT
};
/* NULL if the allocation fails. */
sc55_t *sc55_new(int model);
void sc55_free(sc55_t *s);
/* `data` is the dump as it is on disk; wave ROMs are unscrambled here.
* Returns 0 if the image does not fit the slot. */
int sc55_load_rom(sc55_t *s, int slot, const unsigned char *data, size_t len);
/* Call once every ROM the model needs is loaded. */
void sc55_reset(sc55_t *s);
/* Queue MIDI bytes for the unit's serial input. The queue holds
* sc55_midi_room() more bytes; anything past that overwrites what has
* not been read yet. */
void sc55_midi(sc55_t *s, const unsigned char *data, size_t len);
size_t sc55_midi_room(const sc55_t *s);
/* One bit per voice the firmware has keyed on in the PCM chip. */
uint32_t sc55_voices(const sc55_t *s);
/* Native output rate in Hz: 66207 for the mkII, 64000 for the mk1. */
unsigned sc55_rate(const sc55_t *s);
/* Run the unit until it has produced `count` stereo frames and store
* them in `frames` as left/right pairs. Full scale is +-2^29. */
size_t sc55_run(sc55_t *s, int32_t *frames, size_t count);
#endif
+240
View File
@@ -0,0 +1,240 @@
/* Libretro integration glue for the Nuked-SC55 emulator. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <math.h>
#include "sc55.h"
#include "sc55_libretro.h"
#ifndef PATH_MAX
#define PATH_MAX 1024
#endif
#ifdef _WIN32
#define PXSLASH '\\'
#else
#define PXSLASH '/'
#endif
static sc55_t *unit;
static int active_enabled;
static char saved_system[PATH_MAX];
static char saved_content[PATH_MAX];
static int32_t native_audio[4096 * 2];
static size_t native_count;
static double native_phase;
static unsigned native_rate = 66207;
static int volume_percent = 100;
static void copy_path(char *out, size_t cap, const char *in)
{
if (!in) in = "";
snprintf(out, cap, "%s", in);
}
static int load_file_slot(sc55_t *dev, int slot, const char *path)
{
FILE *f = fopen(path, "rb");
long size;
unsigned char *buf;
int ok;
if (!f) return 0;
if (fseek(f, 0, SEEK_END) != 0 || (size = ftell(f)) <= 0 || fseek(f, 0, SEEK_SET) != 0)
{ fclose(f); return 0; }
buf = (unsigned char*)malloc((size_t)size);
if (!buf) { fclose(f); return 0; }
if (fread(buf, 1, (size_t)size, f) != (size_t)size)
{ free(buf); fclose(f); return 0; }
fclose(f);
ok = sc55_load_rom(dev, slot, buf, (size_t)size);
free(buf);
return ok;
}
static int try_named_set(int model, const char *const names[SC55_ROM_COUNT],
const char *const *dirs, size_t ndirs)
{
sc55_t *dev = sc55_new(model);
int slot;
size_t d;
char path[PATH_MAX * 2];
if (!dev) return 0;
for (slot = 0; slot < SC55_ROM_COUNT; slot++)
{
int loaded = 0;
if (!names[slot]) continue;
for (d = 0; d < ndirs && !loaded; d++)
{
if (!dirs[d] || !dirs[d][0]) continue;
snprintf(path, sizeof(path), "%s%c%s", dirs[d], PXSLASH, names[slot]);
loaded = load_file_slot(dev, slot, path);
}
if (!loaded) { sc55_free(dev); return 0; }
}
sc55_reset(dev);
unit = dev;
native_rate = sc55_rate(unit);
native_count = 0;
native_phase = 0.0;
return 1;
}
static int load_unit(void)
{
char sys_prboom[PATH_MAX], content_sc55[PATH_MAX], sys_sc55[PATH_MAX];
char sys_sc_55[PATH_MAX], sys_roland[PATH_MAX], sys_nuked[PATH_MAX];
char pr_sc55[PATH_MAX], pr_sc_55[PATH_MAX], pr_roland[PATH_MAX], pr_nuked[PATH_MAX];
const char *dirs[16];
size_t n = 0;
static const char *const mk2[SC55_ROM_COUNT] =
{ "rom1.bin", "rom2.bin", "rom_sm.bin", "waverom1.bin", "waverom2.bin", NULL };
static const char *const mk1[SC55_ROM_COUNT] =
{ "sc55_rom1.bin", "sc55_rom2.bin", NULL, "sc55_waverom1.bin", "sc55_waverom2.bin", "sc55_waverom3.bin" };
snprintf(sys_prboom, sizeof(sys_prboom), "%s%cprboom", saved_system, PXSLASH);
snprintf(content_sc55, sizeof(content_sc55), "%s%csc55", saved_content, PXSLASH);
snprintf(sys_sc55, sizeof(sys_sc55), "%s%csc55", saved_system, PXSLASH);
snprintf(sys_sc_55, sizeof(sys_sc_55), "%s%csc-55", saved_system, PXSLASH);
snprintf(sys_roland, sizeof(sys_roland), "%s%croland", saved_system, PXSLASH);
snprintf(sys_nuked, sizeof(sys_nuked), "%s%cnuked", saved_system, PXSLASH);
snprintf(pr_sc55, sizeof(pr_sc55), "%s%csc55", sys_prboom, PXSLASH);
snprintf(pr_sc_55, sizeof(pr_sc_55), "%s%csc-55", sys_prboom, PXSLASH);
snprintf(pr_roland, sizeof(pr_roland), "%s%croland", sys_prboom, PXSLASH);
snprintf(pr_nuked, sizeof(pr_nuked), "%s%cnuked", sys_prboom, PXSLASH);
if (saved_content[0]) dirs[n++] = saved_content;
if (saved_system[0]) dirs[n++] = sys_prboom;
if (saved_system[0]) dirs[n++] = saved_system;
if (saved_content[0]) dirs[n++] = content_sc55;
if (saved_system[0]) dirs[n++] = sys_sc55;
if (saved_system[0]) dirs[n++] = sys_sc_55;
if (saved_system[0]) dirs[n++] = sys_roland;
if (saved_system[0]) dirs[n++] = sys_nuked;
if (saved_system[0]) dirs[n++] = pr_sc55;
if (saved_system[0]) dirs[n++] = pr_sc_55;
if (saved_system[0]) dirs[n++] = pr_roland;
if (saved_system[0]) dirs[n++] = pr_nuked;
if (try_named_set(SC55_MODEL_MK2, mk2, dirs, n)) return 1;
if (try_named_set(SC55_MODEL_MK1, mk1, dirs, n)) return 1;
return 0;
}
void px68k_sc55_shutdown(void)
{
if (unit) sc55_free(unit);
unit = NULL;
active_enabled = 0;
native_count = 0;
native_phase = 0.0;
}
void px68k_sc55_configure(int enabled, const char *system_dir, const char *content_dir)
{
char sys[PATH_MAX], content[PATH_MAX];
copy_path(sys, sizeof(sys), system_dir);
copy_path(content, sizeof(content), content_dir);
enabled = enabled != 0;
if (!enabled)
{
if (unit || active_enabled) px68k_sc55_shutdown();
saved_system[0] = saved_content[0] = '\0';
return;
}
if (unit && active_enabled && !strcmp(saved_system, sys) && !strcmp(saved_content, content))
return;
px68k_sc55_shutdown();
copy_path(saved_system, sizeof(saved_system), sys);
copy_path(saved_content, sizeof(saved_content), content);
if (load_unit()) active_enabled = 1;
else fprintf(stderr, "PX68K SC-55: ROM set not found. Expected PrBoom-compatible filenames in the System Directory, System/prboom, or SC-55 subfolders.\n");
}
int px68k_sc55_enabled(void) { return active_enabled && unit != NULL; }
void px68k_sc55_set_volume(int value)
{
if (value < 0) value = 0;
if (value > 200) value = 200;
volume_percent = value;
}
void px68k_sc55_midi_short(size_t msg)
{
uint8_t bytes[3];
size_t len = 3;
if (!px68k_sc55_enabled()) return;
bytes[0] = (uint8_t)(msg & 0xff);
bytes[1] = (uint8_t)((msg >> 8) & 0xff);
bytes[2] = (uint8_t)((msg >> 16) & 0xff);
if ((bytes[0] & 0xf0) == 0xc0 || (bytes[0] & 0xf0) == 0xd0) len = 2;
sc55_midi(unit, bytes, len);
}
void px68k_sc55_midi_long(const uint8_t *data, size_t len)
{
if (px68k_sc55_enabled() && data && len) sc55_midi(unit, data, len);
}
static int16_t clamp16(int32_t x)
{
if (x > 32767) return 32767;
if (x < -32768) return -32768;
return (int16_t)x;
}
void px68k_sc55_mix(int16_t *stereo, size_t frames)
{
size_t i;
double ratio;
if (!px68k_sc55_enabled() || !stereo || !frames) return;
ratio = (double)native_rate / 44100.0;
if (ratio <= 0.0) return;
/* Keep one native sample on the left of the interpolation point. */
if (native_phase >= (double)(native_count > 1 ? native_count - 1 : 0))
{
size_t drop = (size_t)native_phase;
if (drop >= native_count) drop = native_count ? native_count - 1 : 0;
if (drop)
{
memmove(native_audio, native_audio + drop * 2, (native_count - drop) * 2 * sizeof(int32_t));
native_count -= drop;
native_phase -= (double)drop;
}
}
{
double last_needed = native_phase + (double)(frames - 1) * ratio + 1.0;
size_t need = last_needed >= 0.0 ? (size_t)last_needed + 1 : 2;
if (need > 4095) need = 4095;
if (need > native_count)
{
size_t add = need - native_count;
if (native_count + add > 4096) add = 4096 - native_count;
if (add && sc55_run(unit, native_audio + native_count * 2, add) == add)
native_count += add;
}
}
for (i = 0; i < frames; i++)
{
size_t idx = (size_t)native_phase;
double frac = native_phase - (double)idx;
int32_t l, r;
if (idx + 1 >= native_count) break;
l = (int32_t)((double)native_audio[idx * 2] * (1.0 - frac) + (double)native_audio[(idx + 1) * 2] * frac);
r = (int32_t)((double)native_audio[idx * 2 + 1] * (1.0 - frac) + (double)native_audio[(idx + 1) * 2 + 1] * frac);
/* Scale after converting to the 16-bit mixer domain to avoid overflow. */
l = (int32_t)(((int64_t)(l >> 14) * volume_percent) / 100);
r = (int32_t)(((int64_t)(r >> 14) * volume_percent) / 100);
stereo[i * 2] = clamp16((int32_t)stereo[i * 2] + l);
stereo[i * 2 + 1] = clamp16((int32_t)stereo[i * 2 + 1] + r);
native_phase += ratio;
}
if (native_phase >= 1.0)
{
size_t drop = (size_t)native_phase;
if (drop >= native_count) drop = native_count ? native_count - 1 : 0;
if (drop)
{
memmove(native_audio, native_audio + drop * 2, (native_count - drop) * 2 * sizeof(int32_t));
native_count -= drop;
native_phase -= (double)drop;
}
}
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef PX68K_SC55_LIBRETRO_H
#define PX68K_SC55_LIBRETRO_H
#include <stddef.h>
#include <stdint.h>
void px68k_sc55_configure(int enabled, const char *system_dir, const char *content_dir);
int px68k_sc55_enabled(void);
void px68k_sc55_midi_short(size_t packed_msg);
void px68k_sc55_midi_long(const uint8_t *data, size_t len);
void px68k_sc55_mix(int16_t *stereo, size_t frames);
void px68k_sc55_set_volume(int volume_percent);
void px68k_sc55_shutdown(void);
#endif