Spingle motor spin-up, spin-down implemented with smooth transitions to motor-on loop.

This commit is contained in:
Toni Riikonen
2025-09-06 21:28:09 +03:00
parent e07043bab5
commit 47ea0e594d

View File

@@ -41,17 +41,38 @@
#include <86box/fdd_mfm.h>
#include <86box/fdd_td0.h>
#include <86box/fdc.h>
#include <86box/sound.h>
#include <86box/sound.h>
static int16_t *load_wav(const char *filename, int *sample_count);
// TODO:
// 1. Implement spindle motor spin-up and spin-down
// 2. Implement sound support for all drives (not only for drive 0)
// OK 1. Implement spindle motor spin-up and spin-down
// 2. Move audio emulation to separate code file
// 3. Implement sound support for all drives (not only for drive 0)
// 4. Single sector read/write sound emulation
// 5. Multi-track seek sound emulation
// 6. Limit sound emulation only for 3,5" 300 rpm drives, until we have sound samples for other rpm drives
// Motor sound states
typedef enum {
MOTOR_STATE_STOPPED = 0,
MOTOR_STATE_STARTING,
MOTOR_STATE_RUNNING,
MOTOR_STATE_STOPPING
} motor_state_t;
static int16_t *spindlemotor_start_wav = NULL;
static int spindlemotor_start_wav_samples = 0;
static char *spindlemotor_start_wav_filename = "mitsumi_spindle_motor_start_48000_16_1_PCM.wav";
static int16_t *spindlemotor_loop_wav = NULL;
static int spindlemotor_loop_wav_samples = 0;
static char *spindlemotor_loop_wav_filename = "mitsumi_spindle_motor_loop_48000_16_1_PCM.wav";
static int16_t *spindlemotor_stop_wav = NULL;
static int spindlemotor_stop_wav_samples = 0;
static char *spindlemotor_stop_wav_filename = "mitsumi_spindle_motor_stop_48000_16_1_PCM.wav";
static int16_t *spindlemotor_wav = NULL;
static int spindlemotor_wav_samples = 0;
static char *spindlemotor_wav_filename = "mitsumi_spindle_motor_loop_48000_16_1_PCM.wav";
static int16_t *steptrackup_wav[80];
static int steptrackup_wav_samples[80];
static int16_t *steptrackdown_wav[80];
@@ -59,8 +80,14 @@ static int steptrackdown_wav_samples[80];
static int16_t *seekmultipletracks_wav[79]; // Seek 2, 3, 4 ... 80 tracks = 79 sounds
static int seekmultipletracks_wav_samples[79];
static int spindlemotor_pos[FDD_NUM] = {};
static int spindlemotor_playing[FDD_NUM] = {};
static int spindlemotor_pos[FDD_NUM] = {};
static motor_state_t spindlemotor_state[FDD_NUM] = {};
static float spindlemotor_fade_volume[FDD_NUM] = {};
static int spindlemotor_fade_samples_remaining[FDD_NUM] = {};
// Fade duration: 75ms at 48kHz = 3600 samples
#define FADE_DURATION_MS 75
#define FADE_SAMPLES (48000 * FADE_DURATION_MS / 1000)
// WAV-header
typedef struct {
@@ -127,7 +154,7 @@ int writeprot[FDD_NUM];
int fwriteprot[FDD_NUM];
int fdd_changed[FDD_NUM];
int ui_writeprot[FDD_NUM] = { 0, 0, 0, 0 };
int drive_empty[FDD_NUM] = { 1, 1, 1, 1 };
int drive_empty[FDD_NUM] = { 1, 1, 1, 1 };
DRIVE drives[FDD_NUM];
@@ -140,43 +167,43 @@ d86f_handler_t d86f_handler[FDD_NUM];
static const struct
{
const char *ext;
void (*load)(int drive, char *fn);
void (*close)(int drive);
int size;
void (*load)(int drive, char *fn);
void (*close)(int drive);
int size;
} loaders[] = {
{ "001", img_load, img_close, -1},
{ "002", img_load, img_close, -1},
{ "003", img_load, img_close, -1},
{ "004", img_load, img_close, -1},
{ "005", img_load, img_close, -1},
{ "006", img_load, img_close, -1},
{ "007", img_load, img_close, -1},
{ "008", img_load, img_close, -1},
{ "009", img_load, img_close, -1},
{ "010", img_load, img_close, -1},
{ "12", img_load, img_close, -1},
{ "144", img_load, img_close, -1},
{ "360", img_load, img_close, -1},
{ "720", img_load, img_close, -1},
{ "86F", d86f_load, d86f_close, -1},
{ "BIN", img_load, img_close, -1},
{ "CQ", img_load, img_close, -1},
{ "CQM", img_load, img_close, -1},
{ "DDI", img_load, img_close, -1},
{ "DSK", img_load, img_close, -1},
{ "FDI", fdi_load, fdi_close, -1},
{ "FDF", img_load, img_close, -1},
{ "FLP", img_load, img_close, -1},
{ "HDM", img_load, img_close, -1},
{ "IMA", img_load, img_close, -1},
{ "IMD", imd_load, imd_close, -1},
{ "IMG", img_load, img_close, -1},
{ "JSON", pcjs_load, pcjs_close, -1},
{ "MFM", mfm_load, mfm_close, -1},
{ "TD0", td0_load, td0_close, -1},
{ "VFD", img_load, img_close, -1},
{ "XDF", img_load, img_close, -1},
{ 0, 0, 0, 0 }
{ "001", img_load, img_close, -1 },
{ "002", img_load, img_close, -1 },
{ "003", img_load, img_close, -1 },
{ "004", img_load, img_close, -1 },
{ "005", img_load, img_close, -1 },
{ "006", img_load, img_close, -1 },
{ "007", img_load, img_close, -1 },
{ "008", img_load, img_close, -1 },
{ "009", img_load, img_close, -1 },
{ "010", img_load, img_close, -1 },
{ "12", img_load, img_close, -1 },
{ "144", img_load, img_close, -1 },
{ "360", img_load, img_close, -1 },
{ "720", img_load, img_close, -1 },
{ "86F", d86f_load, d86f_close, -1 },
{ "BIN", img_load, img_close, -1 },
{ "CQ", img_load, img_close, -1 },
{ "CQM", img_load, img_close, -1 },
{ "DDI", img_load, img_close, -1 },
{ "DSK", img_load, img_close, -1 },
{ "FDI", fdi_load, fdi_close, -1 },
{ "FDF", img_load, img_close, -1 },
{ "FLP", img_load, img_close, -1 },
{ "HDM", img_load, img_close, -1 },
{ "IMA", img_load, img_close, -1 },
{ "IMD", imd_load, imd_close, -1 },
{ "IMG", img_load, img_close, -1 },
{ "JSON", pcjs_load, pcjs_close, -1 },
{ "MFM", mfm_load, mfm_close, -1 },
{ "TD0", td0_load, td0_close, -1 },
{ "VFD", img_load, img_close, -1 },
{ "XDF", img_load, img_close, -1 },
{ 0, 0, 0, 0 }
};
static const struct {
@@ -186,35 +213,35 @@ static const struct {
const char *internal_name;
} drive_types[] = {
/* None */
{ 0, 0, "None", "none" },
{ 0, 0, "None", "none" },
/* 5.25" 1DD */
{ 43, FLAG_RPM_300 | FLAG_525 | FLAG_HOLE0, "5.25\" 180k", "525_1dd" },
{ 43, FLAG_RPM_300 | FLAG_525 | FLAG_HOLE0, "5.25\" 180k", "525_1dd" },
/* 5.25" DD */
{ 43, FLAG_RPM_300 | FLAG_525 | FLAG_DS | FLAG_HOLE0, "5.25\" 360k", "525_2dd" },
{ 43, FLAG_RPM_300 | FLAG_525 | FLAG_DS | FLAG_HOLE0, "5.25\" 360k", "525_2dd" },
/* 5.25" QD */
{ 86, FLAG_RPM_300 | FLAG_525 | FLAG_DS | FLAG_HOLE0 | FLAG_DOUBLE_STEP, "5.25\" 720k", "525_2qd" },
{ 86, FLAG_RPM_300 | FLAG_525 | FLAG_DS | FLAG_HOLE0 | FLAG_DOUBLE_STEP, "5.25\" 720k", "525_2qd" },
/* 5.25" HD */
{ 86, FLAG_RPM_360 | FLAG_525 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP | FLAG_PS2, "5.25\" 1.2M", "525_2hd" },
{ 86, FLAG_RPM_360 | FLAG_525 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP | FLAG_PS2, "5.25\" 1.2M", "525_2hd" },
/* 5.25" HD Dual RPM */
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_525 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP, "5.25\" 1.2M 300/360 RPM", "525_2hd_dualrpm" },
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_525 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP, "5.25\" 1.2M 300/360 RPM", "525_2hd_dualrpm" },
/* 3.5" 1DD */
{ 86, FLAG_RPM_300 | FLAG_HOLE0 | FLAG_DOUBLE_STEP, "3.5\" 360k", "35_1dd" },
{ 86, FLAG_RPM_300 | FLAG_HOLE0 | FLAG_DOUBLE_STEP, "3.5\" 360k", "35_1dd" },
/* 3.5" DD, Equivalent to TEAC FD-235F */
{ 86, FLAG_RPM_300 | FLAG_DS | FLAG_HOLE0 | FLAG_DOUBLE_STEP, "3.5\" 720k", "35_2dd" },
{ 86, FLAG_RPM_300 | FLAG_DS | FLAG_HOLE0 | FLAG_DOUBLE_STEP, "3.5\" 720k", "35_2dd" },
/* 3.5" HD, Equivalent to TEAC FD-235HF */
{ 86, FLAG_RPM_300 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP | FLAG_PS2, "3.5\" 1.44M", "35_2hd" },
{ 86, FLAG_RPM_300 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP | FLAG_PS2, "3.5\" 1.44M", "35_2hd" },
/* TODO: 3.5" DD, Equivalent to TEAC FD-235GF */
// { 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP, "3.5\" 1.25M", "35_2hd_2mode" },
// { 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP, "3.5\" 1.25M", "35_2hd_2mode" },
/* 3.5" HD PC-98 */
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP | FLAG_INVERT_DENSEL, "3.5\" 1.25M PC-98", "35_2hd_nec" },
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP | FLAG_INVERT_DENSEL, "3.5\" 1.25M PC-98", "35_2hd_nec" },
/* 3.5" HD 3-Mode, Equivalent to TEAC FD-235HG */
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP, "3.5\" 1.44M 300/360 RPM", "35_2hd_3mode" },
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_DOUBLE_STEP, "3.5\" 1.44M 300/360 RPM", "35_2hd_3mode" },
/* 3.5" ED, Equivalent to TEAC FD-235J */
{ 86, FLAG_RPM_300 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_HOLE2 | FLAG_DOUBLE_STEP, "3.5\" 2.88M", "35_2ed" },
{ 86, FLAG_RPM_300 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_HOLE2 | FLAG_DOUBLE_STEP, "3.5\" 2.88M", "35_2ed" },
/* 3.5" ED Dual RPM, Equivalent to TEAC FD-335J */
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_HOLE2 | FLAG_DOUBLE_STEP, "3.5\" 2.88M 300/360 RPM", "35_2ed_dualrpm" },
{ 86, FLAG_RPM_300 | FLAG_RPM_360 | FLAG_DS | FLAG_HOLE0 | FLAG_HOLE1 | FLAG_HOLE2 | FLAG_DOUBLE_STEP, "3.5\" 2.88M 300/360 RPM", "35_2ed_dualrpm" },
/* End of list */
{ -1, -1, "", "" }
{ -1, -1, "", "" }
};
#ifdef ENABLE_FDD_LOG
@@ -235,6 +262,23 @@ fdd_log(const char *fmt, ...)
# define fdd_log(fmt, ...)
#endif
static const char *
motor_state_name(motor_state_t state)
{
switch (state) {
case MOTOR_STATE_STOPPED:
return "STOPPED";
case MOTOR_STATE_STARTING:
return "STARTING";
case MOTOR_STATE_RUNNING:
return "RUNNING";
case MOTOR_STATE_STOPPING:
return "STOPPING";
default:
return "UNKNOWN";
}
}
char *
fdd_getname(int type)
{
@@ -250,7 +294,7 @@ fdd_get_internal_name(int type)
int
fdd_get_from_internal_name(char *s)
{
int c = 0;
int c = 0;
while (strlen(drive_types[c].internal_name)) {
if (!strcmp((char *) drive_types[c].internal_name, s))
@@ -503,7 +547,7 @@ fdd_load(int drive, char *fn)
if (!fn)
return;
if (strstr(fn, "wp://") == fn) {
offs = 5;
offs = 5;
ui_writeprot[drive] = 1;
}
fn += offs;
@@ -584,13 +628,27 @@ fdd_set_motor_enable(int drive, int motor_enable)
{
if (motor_enable && !motoron[drive]) {
// Motor starting up
spindlemotor_pos[drive] = 0;
spindlemotor_playing[drive] = 1;
if (spindlemotor_state[drive] == MOTOR_STATE_STOPPING) {
// Interrupt stop sequence and transition back to loop
spindlemotor_state[drive] = MOTOR_STATE_RUNNING;
spindlemotor_pos[drive] = 0;
spindlemotor_fade_volume[drive] = 1.0f;
spindlemotor_fade_samples_remaining[drive] = 0;
} else {
// Normal startup
spindlemotor_state[drive] = MOTOR_STATE_STARTING;
spindlemotor_pos[drive] = 0;
spindlemotor_fade_volume[drive] = 1.0f;
spindlemotor_fade_samples_remaining[drive] = 0;
}
timer_set_delay_u64(&fdd_poll_time[drive], fdd_byteperiod(drive));
} else if (!motor_enable) {
} else if (!motor_enable && motoron[drive]) {
// Motor stopping
spindlemotor_playing[drive] = 0;
timer_disable(&fdd_poll_time[drive]);
spindlemotor_state[drive] = MOTOR_STATE_STOPPING;
spindlemotor_pos[drive] = 0;
spindlemotor_fade_volume[drive] = 1.0f;
spindlemotor_fade_samples_remaining[drive] = FADE_SAMPLES;
// Don't disable timer yet - let the stop sound finish
}
motoron[drive] = motor_enable;
}
@@ -714,14 +772,18 @@ fdd_init(void)
{
int i;
spindlemotor_wav = load_wav(spindlemotor_wav_filename, &spindlemotor_wav_samples);
spindlemotor_start_wav = load_wav(spindlemotor_start_wav_filename, &spindlemotor_start_wav_samples);
spindlemotor_loop_wav = load_wav(spindlemotor_loop_wav_filename, &spindlemotor_loop_wav_samples);
spindlemotor_stop_wav = load_wav(spindlemotor_stop_wav_filename, &spindlemotor_stop_wav_samples);
for (i = 0; i < FDD_NUM; i++) {
drives[i].poll = 0;
drives[i].seek = 0;
drives[i].readsector = 0;
spindlemotor_pos[i] = 0;
spindlemotor_playing[i] = 0;
drives[i].poll = 0;
drives[i].seek = 0;
drives[i].readsector = 0;
spindlemotor_pos[i] = 0;
spindlemotor_state[i] = MOTOR_STATE_STOPPED;
spindlemotor_fade_volume[i] = 1.0f;
spindlemotor_fade_samples_remaining[i] = 0;
}
img_init();
@@ -747,10 +809,8 @@ static int16_t *
load_wav(const char *filename, int *sample_count)
{
FILE *f = fopen(filename, "rb");
if (!f) {
if (!f)
return NULL;
}
wav_header_t hdr;
if (fread(&hdr, sizeof(hdr), 1, f) != 1) {
@@ -820,35 +880,109 @@ fdd_audio_callback(int16_t *buffer, int length)
// Clear buffer
memset(buffer, 0, length * sizeof(int16_t));
// Check if any motor is running
int any_motor_running = 0;
// Check if any motor is running or transitioning
int any_motor_active = 0;
for (int drive = 0; drive < FDD_NUM; drive++) {
if (motoron[drive]) {
any_motor_running = 1;
if (spindlemotor_state[drive] != MOTOR_STATE_STOPPED) {
any_motor_active = 1;
break;
}
}
float *float_buffer = (float*)buffer;
int samples_in_buffer = length / 2;
if (!any_motor_active)
return;
if (any_motor_running && spindlemotor_wav && spindlemotor_wav_samples > 0) {
for (int i = 0; i < samples_in_buffer; i++) {
// Only for fdd0
int wav_pos = spindlemotor_pos[0];
// int16 -> float conversion
float left_sample = (float)spindlemotor_wav[wav_pos * 2] / 32768.0f;
float right_sample = (float)spindlemotor_wav[wav_pos * 2 + 1] / 32768.0f;
float_buffer[i * 2] = left_sample;
float_buffer[i * 2 + 1] = right_sample;
spindlemotor_pos[0]++;
// Loop back to beginning for sample, when end is reached
if (spindlemotor_pos[0] >= spindlemotor_wav_samples) {
spindlemotor_pos[0] = 0;
}
float *float_buffer = (float *) buffer;
int samples_in_buffer = length / 2;
// Process audio for drive 0 only for now
int drive = 0;
if (spindlemotor_state[drive] == MOTOR_STATE_STOPPED)
return;
for (int i = 0; i < samples_in_buffer; i++) {
float left_sample = 0.0f;
float right_sample = 0.0f;
switch (spindlemotor_state[drive]) {
case MOTOR_STATE_STARTING:
if (spindlemotor_start_wav && spindlemotor_pos[drive] < spindlemotor_start_wav_samples) {
// Play start sound
left_sample = (float) spindlemotor_start_wav[spindlemotor_pos[drive] * 2] / 32768.0f;
right_sample = (float) spindlemotor_start_wav[spindlemotor_pos[drive] * 2 + 1] / 32768.0f;
spindlemotor_pos[drive]++;
} else {
// Start sound finished, transition to loop
spindlemotor_state[drive] = MOTOR_STATE_RUNNING;
spindlemotor_pos[drive] = 0;
}
break;
case MOTOR_STATE_RUNNING:
if (spindlemotor_loop_wav && spindlemotor_loop_wav_samples > 0) {
// Play loop sound
left_sample = (float) spindlemotor_loop_wav[spindlemotor_pos[drive] * 2] / 32768.0f;
right_sample = (float) spindlemotor_loop_wav[spindlemotor_pos[drive] * 2 + 1] / 32768.0f;
spindlemotor_pos[drive]++;
// Loop back to beginning
if (spindlemotor_pos[drive] >= spindlemotor_loop_wav_samples) {
spindlemotor_pos[drive] = 0;
}
}
break;
case MOTOR_STATE_STOPPING:
if (spindlemotor_fade_samples_remaining[drive] > 0) {
// Mix fading loop sound with rising stop sound
float loop_volume = spindlemotor_fade_volume[drive];
float stop_volume = 1.0f - loop_volume;
float loop_left = 0.0f, loop_right = 0.0f;
float stop_left = 0.0f, stop_right = 0.0f;
// Get loop sample (continue from current position)
if (spindlemotor_loop_wav && spindlemotor_loop_wav_samples > 0) {
int loop_pos = spindlemotor_pos[drive] % spindlemotor_loop_wav_samples;
loop_left = (float) spindlemotor_loop_wav[loop_pos * 2] / 32768.0f;
loop_right = (float) spindlemotor_loop_wav[loop_pos * 2 + 1] / 32768.0f;
}
// Get stop sample
if (spindlemotor_stop_wav && spindlemotor_pos[drive] < spindlemotor_stop_wav_samples) {
stop_left = (float) spindlemotor_stop_wav[spindlemotor_pos[drive] * 2] / 32768.0f;
stop_right = (float) spindlemotor_stop_wav[spindlemotor_pos[drive] * 2 + 1] / 32768.0f;
}
// Mix the sounds
left_sample = loop_left * loop_volume + stop_left * stop_volume;
right_sample = loop_right * loop_volume + stop_right * stop_volume;
spindlemotor_pos[drive]++;
spindlemotor_fade_samples_remaining[drive]--;
// Update fade volume
spindlemotor_fade_volume[drive] = (float) spindlemotor_fade_samples_remaining[drive] / FADE_SAMPLES;
} else {
// Fade completed, play remaining stop sound
if (spindlemotor_stop_wav && spindlemotor_pos[drive] < spindlemotor_stop_wav_samples) {
left_sample = (float) spindlemotor_stop_wav[spindlemotor_pos[drive] * 2] / 32768.0f;
right_sample = (float) spindlemotor_stop_wav[spindlemotor_pos[drive] * 2 + 1] / 32768.0f;
spindlemotor_pos[drive]++;
} else {
// Stop sound finished
spindlemotor_state[drive] = MOTOR_STATE_STOPPED;
timer_disable(&fdd_poll_time[drive]);
}
}
break;
default:
break;
}
float_buffer[i * 2] = left_sample;
float_buffer[i * 2 + 1] = right_sample;
}
}