Files
86Box/src/floppy/fdd_audio.c
Domppari 42e0fb6726 Enhance FDD audio support for BIOS POST mode:
- Implement detection of BIOS vendor and corresponding audio samples for seek operations.
- Add support for loading and playing POST mode seek samples based on BIOS vendor.
- Reset seek state on FDD close and reset functions.
2025-12-21 22:45:16 +02:00

1351 lines
59 KiB
C

/*
* 86Box A hypervisor and IBM PC system emulator that specializes in
* running old operating systems and software designed for IBM
* PC systems and compatibles from 1981 through fairly recent
* system designs based on the PCI bus.
*
* This file is part of the 86Box distribution.
*
* Implementation of the floppy drive audio emulation.
*
* Authors: Toni Riikonen, <riikonen.toni@gmail.com>
*
* Copyright 2025 Toni Riikonen.
*/
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include <86box/timer.h>
#include <86box/fdd.h>
#include <86box/fdd_audio.h>
#include <86box/fdc.h>
#include <86box/mem.h>
#include <86box/rom.h>
#include <86box/sound.h>
#include <86box/plat.h>
#include <86box/path.h>
#include <86box/ini.h>
#ifndef DISABLE_FDD_AUDIO
/* Global audio profile configurations */
static fdd_audio_profile_config_t audio_profiles[FDD_AUDIO_PROFILE_MAX];
static int audio_profile_count = 0;
/* Dynamic sample storage for each profile */
static drive_audio_samples_t profile_samples[FDD_AUDIO_PROFILE_MAX];
/* Audio state for each drive */
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] = {};
/* Multi-track seek audio state for each drive */
static multi_seek_state_t seek_state[FDD_NUM][MAX_CONCURRENT_SEEKS] = {};
extern uint64_t motoron[FDD_NUM];
extern char exe_path[2048];
/* Forward declaration */
static int16_t *load_wav(const char *filename, int *sample_count);
extern uint8_t *rom;
extern uint32_t biosmask;
extern uint32_t biosaddr;
typedef enum {
BIOS_VENDOR_UNKNOWN = 0,
BIOS_VENDOR_AMI,
BIOS_VENDOR_AWARD,
BIOS_VENDOR_PHOENIX,
BIOS_VENDOR_IBM,
BIOS_VENDOR_COMPAQ,
BIOS_VENDOR_OTHER
} bios_vendor_t;
#ifdef ENABLE_FDD_LOG
int fdd_audio_do_log = ENABLE_FDD_LOG;
static void
fdd_log(const char *fmt, ...)
{
va_list ap;
if (fdd_audio_do_log) {
va_start(ap, fmt);
pclog_ex(fmt, ap);
va_end(ap);
}
}
#else
# define fdd_log(fmt, ...)
#endif
/* Detect BIOS vendor by scanning ROM for signature strings */
static bios_vendor_t
fdd_audio_detect_bios_vendor(void)
{
if (!rom || biosmask == 0)
return BIOS_VENDOR_UNKNOWN;
/* Search for BIOS vendor strings in ROM */
for (uint32_t i = 0; i < (biosmask + 1); i++) {
/* AMI BIOS signatures */
if ((i + 7) < (biosmask + 1)) {
if (memcmp(&rom[i], "AMIBIOS", 7) == 0) {
fdd_log("FDD Audio: Detected AMI BIOS\n");
return BIOS_VENDOR_AMI;
}
if (memcmp(&rom[i], "American Megatrends", 19) == 0) {
fdd_log("FDD Audio: Detected AMI BIOS (American Megatrends)\n");
return BIOS_VENDOR_AMI;
}
}
/* Award BIOS signatures */
if ((i + 5) < (biosmask + 1)) {
if (memcmp(&rom[i], "Award", 5) == 0) {
fdd_log("FDD Audio: Detected Award BIOS\n");
return BIOS_VENDOR_AWARD;
}
}
/* Phoenix BIOS signatures */
if ((i + 7) < (biosmask + 1)) {
if (memcmp(&rom[i], "Phoenix", 7) == 0) {
fdd_log("FDD Audio: Detected Phoenix BIOS\n");
return BIOS_VENDOR_PHOENIX;
}
}
/* IBM BIOS signatures */
if ((i + 3) < (biosmask + 1)) {
if (memcmp(&rom[i], "IBM", 3) == 0 && (i + 10) < (biosmask + 1)) {
if (memcmp(&rom[i], "IBM CORP", 8) == 0) {
fdd_log("FDD Audio: Detected IBM BIOS\n");
return BIOS_VENDOR_IBM;
}
}
}
/* Compaq BIOS signatures */
if ((i + 6) < (biosmask + 1)) {
if (memcmp(&rom[i], "COMPAQ", 6) == 0) {
fdd_log("FDD Audio: Detected Compaq BIOS\n");
return BIOS_VENDOR_COMPAQ;
}
}
}
fdd_log("FDD Audio: BIOS vendor unknown\n");
return BIOS_VENDOR_UNKNOWN;
}
/* Determine if this BIOS uses POST-mode FDC seeks */
static int
fdd_audio_get_bios_vendor(void)
{
static bios_vendor_t detected_vendor = BIOS_VENDOR_UNKNOWN;
static int detection_done = 0;
/* Only detect once */
if (!detection_done) {
detected_vendor = fdd_audio_detect_bios_vendor();
detection_done = 1;
}
return detected_vendor;
}
/* Logging function for audio profile parameters */
static void
fdd_audio_log_profile_params(int drive, const fdd_audio_profile_config_t *profile)
{
if (!profile) {
fdd_log("FDD Audio Drive %d: No profile assigned\n", drive);
return;
}
fdd_log("FDD Audio Drive %d Profile Parameters:\n", drive);
fdd_log(" Profile ID: %d\n", profile->id);
fdd_log(" Profile Name: %s\n", profile->name);
fdd_log(" Internal Name: %s\n", profile->internal_name);
fdd_log(" Sample Files:\n");
fdd_log(" Spindle Start: %s (volume: %.2f)\n",
profile->spindlemotor_start.filename, profile->spindlemotor_start.volume);
fdd_log(" Spindle Loop: %s (volume: %.2f)\n",
profile->spindlemotor_loop.filename, profile->spindlemotor_loop.volume);
fdd_log(" Spindle Stop: %s (volume: %.2f)\n",
profile->spindlemotor_stop.filename, profile->spindlemotor_stop.volume);
/* Log a few sample seek files as examples */
int max_tracks = (profile->total_tracks == 40) ? 39 : 79;
fdd_log(" Individual seek samples (up to %d tracks):\n", max_tracks);
for (int i = 0; i < max_tracks && i < 5; i++) {
if (profile->seek_up[i].filename[0]) {
fdd_log(" Seek up %d track(s): %s (volume: %.2f)\n",
i + 1, profile->seek_up[i].filename, profile->seek_up[i].volume);
}
if (profile->seek_down[i].filename[0]) {
fdd_log(" Seek down %d track(s): %s (volume: %.2f)\n",
i + 1, profile->seek_down[i].filename, profile->seek_down[i].volume);
}
}
if (max_tracks > 5)
fdd_log(" ... and %d more seek samples\n", (max_tracks - 5) * 2);
}
/* Log audio profile parameters for a specific drive */
void
fdd_audio_log_drive_profile(int drive)
{
if (drive < 0 || drive >= FDD_NUM) {
fdd_log("FDD Audio: Invalid drive number %d\n", drive);
return;
}
int profile_id = fdd_get_audio_profile(drive);
const fdd_audio_profile_config_t *profile = fdd_audio_get_profile(profile_id);
fdd_log("FDD Audio Drive %d: Using profile %d\n", drive, profile_id);
fdd_audio_log_profile_params(drive, profile);
}
/* Log only the audio profiles that are actually used by configured drives */
static void
fdd_audio_log_active_profiles(void)
{
fdd_log("FDD Audio: Checking active drive configurations...\n");
int active_drive_count = 0;
for (int drive = 0; drive < FDD_NUM; drive++) {
if (fdd_get_type(drive) == 0)
continue;
active_drive_count++;
int profile_id = fdd_get_audio_profile(drive);
if (profile_id >= 0 && profile_id < audio_profile_count) {
fdd_log("FDD Audio: Drive %d (configured) uses profile %d\n", drive, profile_id);
fdd_audio_log_profile_params(drive, &audio_profiles[profile_id]);
}
}
if (active_drive_count == 0) {
fdd_log("FDD Audio: No drives configured - no audio profiles to log\n");
return;
}
fdd_log("FDD Audio: Active audio profiles for %d configured drive(s):\n", active_drive_count);
}
void
fdd_audio_load_profiles(void)
{
ini_t profiles_ini;
char cfg_fn[1024] = { 0 };
int ret = asset_getfile("assets/sounds/fdd/fdd_audio_profiles.cfg", cfg_fn, 1024);
if (!ret) {
fdd_log("FDD Audio: Could not find profiles\n");
return;
}
profiles_ini = ini_read_ex(cfg_fn, 1);
if (profiles_ini == NULL) {
fdd_log("FDD Audio: Could not load profiles\n");
return;
}
audio_profile_count = 0;
/* Load profiles by trying known profile section names */
for (int i = 0; i < FDD_AUDIO_PROFILE_MAX && audio_profile_count < FDD_AUDIO_PROFILE_MAX; i++) {
char section_name[64];
snprintf(section_name, sizeof(section_name), "Profile \"%d\"", i);
ini_section_t section = ini_find_section(profiles_ini, section_name);
if (section) {
fdd_audio_profile_config_t *profile = &audio_profiles[audio_profile_count];
/* Load profile configuration */
profile->id = ini_section_get_int(section, "id", audio_profile_count);
const char *name = ini_section_get_string(section, "name", "Unknown");
strncpy(profile->name, name, sizeof(profile->name) - 1);
profile->name[sizeof(profile->name) - 1] = '\0';
const char *internal_name = ini_section_get_string(section, "internal_name", "unknown");
strncpy(profile->internal_name, internal_name, sizeof(profile->internal_name) - 1);
profile->internal_name[sizeof(profile->internal_name) - 1] = '\0';
/* Load sample configurations */
const char *filename = ini_section_get_string(section, "spindlemotor_start_file", "");
strncpy(profile->spindlemotor_start.filename, filename, sizeof(profile->spindlemotor_start.filename) - 1);
profile->spindlemotor_start.filename[sizeof(profile->spindlemotor_start.filename) - 1] = '\0';
profile->spindlemotor_start.volume = ini_section_get_double(section, "spindlemotor_start_volume", 1.0);
filename = ini_section_get_string(section, "spindlemotor_loop_file", "");
strncpy(profile->spindlemotor_loop.filename, filename, sizeof(profile->spindlemotor_loop.filename) - 1);
profile->spindlemotor_loop.filename[sizeof(profile->spindlemotor_loop.filename) - 1] = '\0';
profile->spindlemotor_loop.volume = ini_section_get_double(section, "spindlemotor_loop_volume", 1.0);
filename = ini_section_get_string(section, "spindlemotor_stop_file", "");
strncpy(profile->spindlemotor_stop.filename, filename, sizeof(profile->spindlemotor_stop.filename) - 1);
profile->spindlemotor_stop.filename[sizeof(profile->spindlemotor_stop.filename) - 1] = '\0';
profile->spindlemotor_stop.volume = ini_section_get_double(section, "spindlemotor_stop_volume", 1.0);
/* Load seek samples and seek times for each track count */
for (int track_count = 1; track_count <= MAX_SEEK_SAMPLES; track_count++) {
char key[128];
/* Seek up samples */
snprintf(key, sizeof(key), "seek_up_%dtrack_file", track_count);
filename = ini_section_get_string(section, key, "");
strncpy(profile->seek_up[track_count - 1].filename, filename,
sizeof(profile->seek_up[track_count - 1].filename) - 1);
profile->seek_up[track_count - 1].filename[sizeof(profile->seek_up[track_count - 1].filename) - 1] = '\0';
snprintf(key, sizeof(key), "seek_up_%dtrack_volume", track_count);
profile->seek_up[track_count - 1].volume = ini_section_get_double(section, key, 1.0);
/* Seek down samples */
snprintf(key, sizeof(key), "seek_down_%dtrack_file", track_count);
filename = ini_section_get_string(section, key, "");
strncpy(profile->seek_down[track_count - 1].filename, filename,
sizeof(profile->seek_down[track_count - 1].filename) - 1);
profile->seek_down[track_count - 1].filename[sizeof(profile->seek_down[track_count - 1].filename) - 1] = '\0';
snprintf(key, sizeof(key), "seek_down_%dtrack_volume", track_count);
profile->seek_down[track_count - 1].volume = ini_section_get_double(section, key, 1.0);
/* POST mode seek down samples */
snprintf(key, sizeof(key), "post_seek_down_%dtrack_file", track_count);
filename = ini_section_get_string(section, key, "");
strncpy(profile->post_seek_down[track_count - 1].filename, filename,
sizeof(profile->post_seek_down[track_count - 1].filename) - 1);
profile->post_seek_down[track_count - 1].filename[sizeof(profile->post_seek_down[track_count - 1].filename) - 1] = '\0';
snprintf(key, sizeof(key), "post_seek_down_%dtrack_volume", track_count);
profile->post_seek_down[track_count - 1].volume = ini_section_get_double(section, key, 1.0);
/* BIOS vendor-specific POST mode seek samples */
static const char *bios_prefixes[] = {
NULL, /* BIOS_VENDOR_UNKNOWN */
"amibios", /* BIOS_VENDOR_AMI */
"award", /* BIOS_VENDOR_AWARD */
"phoenix", /* BIOS_VENDOR_PHOENIX */
"ibm", /* BIOS_VENDOR_IBM */
"compaq", /* BIOS_VENDOR_COMPAQ */
NULL /* BIOS_VENDOR_OTHER */
};
for (int vendor = 1; vendor < BIOS_VENDOR_COUNT; vendor++) {
if (!bios_prefixes[vendor])
continue;
/* BIOS-specific POST mode seek up samples */
snprintf(key, sizeof(key), "%s_post_seek_up_%dtrack_file", bios_prefixes[vendor], track_count);
filename = ini_section_get_string(section, key, "");
strncpy(profile->bios_post_seek_up[vendor][track_count - 1].filename, filename,
sizeof(profile->bios_post_seek_up[vendor][track_count - 1].filename) - 1);
profile->bios_post_seek_up[vendor][track_count - 1].filename[sizeof(profile->bios_post_seek_up[vendor][track_count - 1].filename) - 1] = '\0';
snprintf(key, sizeof(key), "%s_post_seek_up_%dtrack_volume", bios_prefixes[vendor], track_count);
profile->bios_post_seek_up[vendor][track_count - 1].volume = ini_section_get_double(section, key, 1.0);
/* BIOS-specific POST mode seek down samples */
snprintf(key, sizeof(key), "%s_post_seek_down_%dtrack_file", bios_prefixes[vendor], track_count);
filename = ini_section_get_string(section, key, "");
strncpy(profile->bios_post_seek_down[vendor][track_count - 1].filename, filename,
sizeof(profile->bios_post_seek_down[vendor][track_count - 1].filename) - 1);
profile->bios_post_seek_down[vendor][track_count - 1].filename[sizeof(profile->bios_post_seek_down[vendor][track_count - 1].filename) - 1] = '\0';
snprintf(key, sizeof(key), "%s_post_seek_down_%dtrack_volume", bios_prefixes[vendor], track_count);
profile->bios_post_seek_down[vendor][track_count - 1].volume = ini_section_get_double(section, key, 1.0);
/* BIOS-specific POST mode seek time in milliseconds */
snprintf(key, sizeof(key), "%s_post_seek_%dtrack_time_ms", bios_prefixes[vendor], track_count);
profile->bios_post_seek_time_ms[vendor][track_count - 1] = ini_section_get_double(section, key, 0.0);
}
/* Seek time in milliseconds - used for FDC timing, not sample playback */
snprintf(key, sizeof(key), "seek_%dtrack_time_ms", track_count);
profile->seek_time_ms[track_count - 1] = ini_section_get_double(section, key, 6.0 * track_count);
/* POST mode seek time in milliseconds */
snprintf(key, sizeof(key), "post_seek_%dtrack_time_ms", track_count);
profile->post_seek_time_ms[track_count - 1] = ini_section_get_double(section, key, 0.0);
}
/* Load timing configurations */
profile->total_tracks = ini_section_get_int(section, "total_tracks", 80);
audio_profile_count++;
}
}
ini_close(profiles_ini);
fdd_log("FDD Audio: Loaded %d audio profiles\n", audio_profile_count);
}
void
load_profile_samples(int profile_id)
{
if (profile_id < 0 || profile_id >= audio_profile_count)
return;
fdd_audio_profile_config_t *config = &audio_profiles[profile_id];
drive_audio_samples_t *samples = &profile_samples[profile_id];
fdd_log("FDD Audio: Loading samples for profile %d (%s)\n",
profile_id, config->name);
/* Load samples if not already loaded */
if (samples->spindlemotor_start.buffer == NULL && config->spindlemotor_start.filename[0]) {
strcpy(samples->spindlemotor_start.filename, config->spindlemotor_start.filename);
samples->spindlemotor_start.volume = config->spindlemotor_start.volume;
samples->spindlemotor_start.buffer = load_wav(config->spindlemotor_start.filename,
&samples->spindlemotor_start.samples);
if (samples->spindlemotor_start.buffer) {
fdd_log(" Loaded spindlemotor_start: %s (%d samples, volume %.2f)\n",
config->spindlemotor_start.filename,
samples->spindlemotor_start.samples,
config->spindlemotor_start.volume);
} else {
fdd_log(" Failed to load spindlemotor_start: %s\n",
config->spindlemotor_start.filename);
}
}
if (samples->spindlemotor_loop.buffer == NULL && config->spindlemotor_loop.filename[0]) {
strcpy(samples->spindlemotor_loop.filename, config->spindlemotor_loop.filename);
samples->spindlemotor_loop.volume = config->spindlemotor_loop.volume;
samples->spindlemotor_loop.buffer = load_wav(config->spindlemotor_loop.filename,
&samples->spindlemotor_loop.samples);
if (samples->spindlemotor_loop.buffer) {
fdd_log(" Loaded spindlemotor_loop: %s (%d samples, volume %.2f)\n",
config->spindlemotor_loop.filename,
samples->spindlemotor_loop.samples,
config->spindlemotor_loop.volume);
} else {
fdd_log(" Failed to load spindlemotor_loop: %s\n",
config->spindlemotor_loop.filename);
}
}
if (samples->spindlemotor_stop.buffer == NULL && config->spindlemotor_stop.filename[0]) {
strcpy(samples->spindlemotor_stop.filename, config->spindlemotor_stop.filename);
samples->spindlemotor_stop.volume = config->spindlemotor_stop.volume;
samples->spindlemotor_stop.buffer = load_wav(config->spindlemotor_stop.filename,
&samples->spindlemotor_stop.samples);
if (samples->spindlemotor_stop.buffer) {
fdd_log(" Loaded spindlemotor_stop: %s (%d samples, volume %.2f)\n",
config->spindlemotor_stop.filename,
samples->spindlemotor_stop.samples,
config->spindlemotor_stop.volume);
} else {
fdd_log(" Failed to load spindlemotor_stop: %s\n",
config->spindlemotor_stop.filename);
}
}
/* Load individual seek samples for each track count */
int max_tracks = (config->total_tracks == 40) ? 39 : 79;
for (int track_count = 1; track_count <= max_tracks; track_count++) {
int idx = track_count - 1;
/* Load seek up sample */
if (samples->seek_up[idx].buffer == NULL && config->seek_up[idx].filename[0]) {
strcpy(samples->seek_up[idx].filename, config->seek_up[idx].filename);
samples->seek_up[idx].volume = config->seek_up[idx].volume;
samples->seek_up[idx].buffer = load_wav(config->seek_up[idx].filename,
&samples->seek_up[idx].samples);
if (samples->seek_up[idx].buffer) {
fdd_log(" Loaded seek_up[%d]: %s (%d samples, volume %.2f)\n",
idx, config->seek_up[idx].filename,
samples->seek_up[idx].samples, config->seek_up[idx].volume);
}
}
/* Load seek down sample */
if (samples->seek_down[idx].buffer == NULL && config->seek_down[idx].filename[0]) {
strcpy(samples->seek_down[idx].filename, config->seek_down[idx].filename);
samples->seek_down[idx].volume = config->seek_down[idx].volume;
samples->seek_down[idx].buffer = load_wav(config->seek_down[idx].filename,
&samples->seek_down[idx].samples);
if (samples->seek_down[idx].buffer) {
fdd_log(" Loaded seek_down[%d]: %s (%d samples, volume %.2f)\n",
idx, config->seek_down[idx].filename,
samples->seek_down[idx].samples, config->seek_down[idx].volume);
}
}
/* Load POST mode seek samples if configured */
if (config->post_seek_up[idx].filename[0]) {
if (samples->post_seek_up[idx].buffer == NULL) {
strcpy(samples->post_seek_up[idx].filename, config->post_seek_up[idx].filename);
samples->post_seek_up[idx].volume = config->post_seek_up[idx].volume;
samples->post_seek_up[idx].buffer = load_wav(config->post_seek_up[idx].filename,
&samples->post_seek_up[idx].samples);
if (samples->post_seek_up[idx].buffer) {
fdd_log(" Loaded POST seek_up[%d] (%d-track): %s (%d samples, volume %.2f)\n",
idx, track_count, config->post_seek_up[idx].filename,
samples->post_seek_up[idx].samples, config->post_seek_up[idx].volume);
}
}
}
if (config->post_seek_down[idx].filename[0]) {
if (samples->post_seek_down[idx].buffer == NULL) {
strcpy(samples->post_seek_down[idx].filename, config->post_seek_down[idx].filename);
samples->post_seek_down[idx].volume = config->post_seek_down[idx].volume;
samples->post_seek_down[idx].buffer = load_wav(config->post_seek_down[idx].filename,
&samples->post_seek_down[idx].samples);
if (samples->post_seek_down[idx].buffer) {
fdd_log(" Loaded POST seek_down[%d] (%d-track): %s (%d samples, volume %.2f)\n",
idx, track_count, config->post_seek_down[idx].filename,
samples->post_seek_down[idx].samples, config->post_seek_down[idx].volume);
}
}
}
/* Load BIOS vendor-specific POST mode seek samples if configured */
static const char *bios_names[] = {
"UNKNOWN", "AMI", "AWARD", "PHOENIX", "IBM", "COMPAQ", "OTHER"
};
for (int vendor = 1; vendor < BIOS_VENDOR_COUNT; vendor++) {
if (config->bios_post_seek_up[vendor][idx].filename[0]) {
if (samples->bios_post_seek_up[vendor][idx].buffer == NULL) {
strcpy(samples->bios_post_seek_up[vendor][idx].filename, config->bios_post_seek_up[vendor][idx].filename);
samples->bios_post_seek_up[vendor][idx].volume = config->bios_post_seek_up[vendor][idx].volume;
samples->bios_post_seek_up[vendor][idx].buffer = load_wav(config->bios_post_seek_up[vendor][idx].filename,
&samples->bios_post_seek_up[vendor][idx].samples);
if (samples->bios_post_seek_up[vendor][idx].buffer) {
fdd_log(" Loaded %s POST seek_up[%d] (%d-track): %s (%d samples, volume %.2f)\n",
bios_names[vendor], idx, track_count, config->bios_post_seek_up[vendor][idx].filename,
samples->bios_post_seek_up[vendor][idx].samples, config->bios_post_seek_up[vendor][idx].volume);
}
}
}
if (config->bios_post_seek_down[vendor][idx].filename[0]) {
if (samples->bios_post_seek_down[vendor][idx].buffer == NULL) {
strcpy(samples->bios_post_seek_down[vendor][idx].filename, config->bios_post_seek_down[vendor][idx].filename);
samples->bios_post_seek_down[vendor][idx].volume = config->bios_post_seek_down[vendor][idx].volume;
samples->bios_post_seek_down[vendor][idx].buffer = load_wav(config->bios_post_seek_down[vendor][idx].filename,
&samples->bios_post_seek_down[vendor][idx].samples);
if (samples->bios_post_seek_down[vendor][idx].buffer) {
fdd_log(" Loaded %s POST seek_down[%d] (%d-track): %s (%d samples, volume %.2f)\n",
bios_names[vendor], idx, track_count, config->bios_post_seek_down[vendor][idx].filename,
samples->bios_post_seek_down[vendor][idx].samples, config->bios_post_seek_down[vendor][idx].volume);
}
}
}
}
}
}
static drive_audio_samples_t *
get_drive_samples(int drive)
{
int profile_id = fdd_get_audio_profile(drive);
if (profile_id <= 0 || profile_id >= audio_profile_count)
return NULL;
/* Samples are preloaded during fdd_audio_init */
return &profile_samples[profile_id];
}
/* Public API functions */
int
fdd_audio_get_profile_count(void)
{
return audio_profile_count;
}
const fdd_audio_profile_config_t *
fdd_audio_get_profile(int id)
{
if (id < 0 || id >= audio_profile_count)
return NULL;
return &audio_profiles[id];
}
const char *
fdd_audio_get_profile_name(int id)
{
if (id < 0 || id >= audio_profile_count)
return NULL;
return audio_profiles[id].name;
}
const char *
fdd_audio_get_profile_internal_name(int id)
{
if (id < 0 || id >= audio_profile_count)
return NULL;
return audio_profiles[id].internal_name;
}
int
fdd_audio_get_profile_by_internal_name(const char *internal_name)
{
if (!internal_name || !*internal_name)
return 0;
if (audio_profile_count == 0)
fdd_audio_load_profiles();
for (int i = 0; i < audio_profile_count; i++) {
if (!strcmp(audio_profiles[i].internal_name, internal_name))
return i;
}
return 0;
}
double
fdd_audio_get_seek_time(int drive, int track_count, int is_seek_down)
{
int profile_id = fdd_get_audio_profile(drive);
if (profile_id < 0 || profile_id >= audio_profile_count) {
return 0;
}
fdd_audio_profile_config_t *profile = &audio_profiles[profile_id];
if (!profile)
return 0;
/* Get the maximum available seek sample for this profile */
int max_seek_tracks = (profile->total_tracks == 40) ? 39 : 79;
/* Clamp track_count to maximum */
if (track_count > max_seek_tracks)
track_count = max_seek_tracks;
/* Return configured seek time in microseconds */
if (track_count > 0 && track_count <= MAX_SEEK_SAMPLES) {
/* In POST mode, check for BIOS-specific timing first */
if (fdd_get_boot_status() == BIOS_BOOT_POST) {
int bios_vendor = fdd_audio_get_bios_vendor();
/* Check BIOS vendor-specific timing first */
if (bios_vendor > 0 && bios_vendor < BIOS_VENDOR_COUNT &&
profile->bios_post_seek_time_ms[bios_vendor][track_count - 1] > 0.0) {
return profile->bios_post_seek_time_ms[bios_vendor][track_count - 1] * 1000.0;
}
/* Fall back to generic POST timing */
if (profile->post_seek_time_ms[track_count - 1] > 0.0) {
return profile->post_seek_time_ms[track_count - 1] * 1000.0;
}
}
return profile->seek_time_ms[track_count - 1] * 1000.0;
}
/* Fallback */
return 0;
}
void
fdd_audio_init(void)
{
/* Load audio profiles configuration */
fdd_audio_load_profiles();
/* Initialize audio state for all drives */
for (int i = 0; i < FDD_NUM; i++) {
spindlemotor_pos[i] = 0;
spindlemotor_state[i] = MOTOR_STATE_STOPPED;
spindlemotor_fade_volume[i] = 1.0f;
spindlemotor_fade_samples_remaining[i] = 0;
/* Initialize all concurrent seek slots */
for (int j = 0; j < MAX_CONCURRENT_SEEKS; j++) {
seek_state[i][j].position = 0;
seek_state[i][j].active = 0;
seek_state[i][j].duration_samples = 0;
seek_state[i][j].from_track = -1;
seek_state[i][j].to_track = -1;
seek_state[i][j].track_diff = 0;
seek_state[i][j].sample_to_play = NULL;
}
}
/* Preload audio samples for each drive's selected profile */
for (int drive = 0; drive < FDD_NUM; drive++) {
int profile_id = fdd_get_audio_profile(drive);
if (profile_id > 0 && profile_id < audio_profile_count) {
load_profile_samples(profile_id);
}
}
/* Log only the active profiles used by configured drives */
fdd_audio_log_active_profiles();
/* Initialize sound thread */
sound_fdd_thread_init();
fdd_log("FDD Audio: Initialization complete\n");
}
void
fdd_audio_close(void)
{
fdd_log("FDD Audio: Shutting down audio system\n");
/* Free loaded profile samples */
for (int profile_id = 0; profile_id < audio_profile_count; profile_id++) {
drive_audio_samples_t *samples = &profile_samples[profile_id];
if (samples->spindlemotor_start.buffer) {
free(samples->spindlemotor_start.buffer);
samples->spindlemotor_start.buffer = NULL;
samples->spindlemotor_start.samples = 0;
}
if (samples->spindlemotor_loop.buffer) {
free(samples->spindlemotor_loop.buffer);
samples->spindlemotor_loop.buffer = NULL;
samples->spindlemotor_loop.samples = 0;
}
if (samples->spindlemotor_stop.buffer) {
free(samples->spindlemotor_stop.buffer);
samples->spindlemotor_stop.buffer = NULL;
samples->spindlemotor_stop.samples = 0;
}
/* Free individual seek samples */
for (int track_count = 0; track_count < MAX_SEEK_SAMPLES; track_count++) {
if (samples->seek_up[track_count].buffer) {
free(samples->seek_up[track_count].buffer);
samples->seek_up[track_count].buffer = NULL;
samples->seek_up[track_count].samples = 0;
}
if (samples->seek_down[track_count].buffer) {
free(samples->seek_down[track_count].buffer);
samples->seek_down[track_count].buffer = NULL;
samples->seek_down[track_count].samples = 0;
}
if (samples->post_seek_up[track_count].buffer) {
free(samples->post_seek_up[track_count].buffer);
samples->post_seek_up[track_count].buffer = NULL;
samples->post_seek_up[track_count].samples = 0;
}
if (samples->post_seek_down[track_count].buffer) {
free(samples->post_seek_down[track_count].buffer);
samples->post_seek_down[track_count].buffer = NULL;
samples->post_seek_down[track_count].samples = 0;
}
/* Free BIOS vendor-specific POST seek samples */
for (int vendor = 0; vendor < BIOS_VENDOR_COUNT; vendor++) {
if (samples->bios_post_seek_up[vendor][track_count].buffer) {
free(samples->bios_post_seek_up[vendor][track_count].buffer);
samples->bios_post_seek_up[vendor][track_count].buffer = NULL;
samples->bios_post_seek_up[vendor][track_count].samples = 0;
}
if (samples->bios_post_seek_down[vendor][track_count].buffer) {
free(samples->bios_post_seek_down[vendor][track_count].buffer);
samples->bios_post_seek_down[vendor][track_count].buffer = NULL;
samples->bios_post_seek_down[vendor][track_count].samples = 0;
}
}
}
}
sound_fdd_thread_end();
fdd_log("FDD Audio: Shutdown complete\n");
}
void
fdd_audio_set_motor_enable(int drive, int motor_enable)
{
if (!fdd_sounds_enabled || fdd_get_turbo(drive))
return;
drive_audio_samples_t *samples = get_drive_samples(drive);
if (!samples)
return;
fdd_log("FDD Audio Drive %d: Motor %s\n", drive, motor_enable ? "ON" : "OFF");
if (motor_enable && !motoron[drive]) {
/* Motor starting up */
if (spindlemotor_state[drive] == MOTOR_STATE_STOPPING) {
/* Interrupt stop sequence and transition back to loop */
fdd_log("FDD Audio Drive %d: Interrupting stop sequence, returning to loop\n", drive);
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 */
fdd_log("FDD Audio Drive %d: Starting motor (normal startup)\n", drive);
spindlemotor_state[drive] = MOTOR_STATE_STARTING;
spindlemotor_pos[drive] = 0;
spindlemotor_fade_volume[drive] = 1.0f;
spindlemotor_fade_samples_remaining[drive] = 0;
}
} else if (!motor_enable && motoron[drive]) {
/* Motor stopping */
fdd_log("FDD Audio Drive %d: Stopping motor\n", drive);
spindlemotor_state[drive] = MOTOR_STATE_STOPPING;
spindlemotor_pos[drive] = 0;
spindlemotor_fade_volume[drive] = 1.0f;
spindlemotor_fade_samples_remaining[drive] = FADE_SAMPLES;
}
}
void
fdd_audio_play_multi_track_seek(int drive, int from_track, int to_track)
{
if (!fdd_sounds_enabled || fdd_get_turbo(drive))
return;
if (drive < 0 || drive >= FDD_NUM)
return;
int track_diff = abs(from_track - to_track);
if (track_diff < 1)
return;
drive_audio_samples_t *samples = get_drive_samples(drive);
if (!samples)
return;
int is_seek_down = (to_track < from_track);
/* Get the profile to check total_tracks */
int profile_id = fdd_get_audio_profile(drive);
if (profile_id < 1 || profile_id >= audio_profile_count)
return;
fdd_audio_profile_config_t *profile = &audio_profiles[profile_id];
/* Determine the maximum available seek sample for this profile */
int max_seek_tracks = (profile->total_tracks == 40) ? 39 : 79;
/* Clamp track_diff to the maximum available sample */
if (track_diff > max_seek_tracks) {
fdd_log("FDD Audio Drive %d: Seek request for %d tracks exceeds maximum %d, clamping to %d\n",
drive, track_diff, max_seek_tracks, max_seek_tracks);
track_diff = max_seek_tracks;
}
int boot_status = fdd_get_boot_status();
int bios_vendor = fdd_audio_get_bios_vendor();
int idx = track_diff - 1;
int real_track_diff = to_track - from_track;
audio_sample_t *sample_to_use = NULL;
if (boot_status == BIOS_BOOT_POST) {
if (bios_vendor == BIOS_VENDOR_AMI) {
/* AMI BIOS POST mode: use AMI-specific samples if available */
/* AMI BIOS quirk: for single-track seeks down (except 10->9), do not play audio */
if (real_track_diff == -1 && (from_track != 10 || to_track != 9)) {
fdd_log("FDD Audio Drive %d: AMI BIOS quirk: for single-track seeks down (except 10->9), do not play audio\n", drive);
return;
}
/* For 10->9 seek, use the 1-track sample (which should be the 10-0 sound) */
sample_to_use = is_seek_down ? &samples->bios_post_seek_down[bios_vendor][idx] : &samples->bios_post_seek_up[bios_vendor][idx];
if (sample_to_use->buffer && sample_to_use->samples > 0) {
fdd_log("FDD Audio Drive %d: Using AMI BIOS POST mode seek sample (idx=%d, %s)\n",
drive, idx, is_seek_down ? "DOWN" : "UP");
} else {
/* Fall back to generic POST sample */
sample_to_use = is_seek_down ? &samples->post_seek_down[idx] : &samples->post_seek_up[idx];
if (sample_to_use->buffer && sample_to_use->samples > 0) {
fdd_log("FDD Audio Drive %d: AMI BIOS sample not available, using generic POST sample (idx=%d, %s)\n",
drive, idx, is_seek_down ? "DOWN" : "UP");
} else {
/* Fall back to normal sample */
fdd_log("FDD Audio Drive %d: POST sample not available, using normal sample\n", drive);
sample_to_use = is_seek_down ? &samples->seek_down[idx] : &samples->seek_up[idx];
}
}
} else if (bios_vendor > 0 && bios_vendor < BIOS_VENDOR_COUNT) {
/* Other known BIOS vendors: try vendor-specific samples first */
sample_to_use = is_seek_down ? &samples->bios_post_seek_down[bios_vendor][idx] : &samples->bios_post_seek_up[bios_vendor][idx];
if (sample_to_use->buffer && sample_to_use->samples > 0) {
fdd_log("FDD Audio Drive %d: Using BIOS vendor %d POST mode seek sample (idx=%d, %s)\n",
drive, bios_vendor, idx, is_seek_down ? "DOWN" : "UP");
} else {
/* Fall back to generic POST sample */
sample_to_use = is_seek_down ? &samples->post_seek_down[idx] : &samples->post_seek_up[idx];
if (sample_to_use->buffer && sample_to_use->samples > 0) {
fdd_log("FDD Audio Drive %d: BIOS-specific sample not available, using generic POST sample (idx=%d, %s)\n",
drive, idx, is_seek_down ? "DOWN" : "UP");
} else {
/* Fall back to normal sample */
fdd_log("FDD Audio Drive %d: POST sample not available, using normal sample\n", drive);
sample_to_use = is_seek_down ? &samples->seek_down[idx] : &samples->seek_up[idx];
}
}
} else {
/* Unknown BIOS vendor POST mode */
sample_to_use = is_seek_down ? &samples->post_seek_down[idx] : &samples->post_seek_up[idx];
if (!sample_to_use->buffer || sample_to_use->samples == 0) {
fdd_log("FDD Audio Drive %d: POST sample not available, using normal sample\n", drive);
sample_to_use = is_seek_down ? &samples->seek_down[idx] : &samples->seek_up[idx];
} else {
fdd_log("FDD Audio Drive %d: Using POST mode seek sample (idx=%d, %s)\n",
drive, idx, is_seek_down ? "DOWN" : "UP");
}
}
} else {
/* Use normal samples */
sample_to_use = is_seek_down ? &samples->seek_down[idx] : &samples->seek_up[idx];
}
/* Only proceed if we have the appropriate sample */
if (!sample_to_use || !sample_to_use->buffer || sample_to_use->samples == 0)
return;
fdd_log("FDD Audio Drive %d: Multi-track seek %d -> %d (%d tracks, %s, POST=%d)\n",
drive, from_track, to_track, track_diff, is_seek_down ? "DOWN" : "UP", boot_status);
/* Find an available seek slot */
int slot = -1;
for (int i = 0; i < MAX_CONCURRENT_SEEKS; i++) {
if (!seek_state[drive][i].active) {
slot = i;
break;
}
}
/* If no slot available, reuse the oldest (first) slot */
if (slot == -1) {
fdd_log("FDD Audio Drive %d: All seek slots in use, reusing slot 0\n", drive);
slot = 0;
}
/* Start new seek in the available slot */
seek_state[drive][slot].position = 0;
seek_state[drive][slot].active = 1;
seek_state[drive][slot].duration_samples = sample_to_use->samples;
seek_state[drive][slot].from_track = from_track;
seek_state[drive][slot].to_track = to_track;
seek_state[drive][slot].track_diff = track_diff;
seek_state[drive][slot].sample_to_play = sample_to_use;
fdd_log("FDD Audio Drive %d: Started seek in slot %d, duration %d samples\n",
drive, slot, sample_to_use->samples);
}
static int16_t *
load_wav(const char *filename, int *sample_count)
{
if ((filename == NULL) || (strlen(filename) == 0))
return NULL;
if (strstr(filename, "..") != NULL)
return NULL;
FILE *f = asset_fopen(filename, "rb");
if (f == NULL) {
fdd_log("FDD Audio: Failed to open WAV file: %s\n", filename);
return NULL;
}
wav_header_t hdr;
if (fread(&hdr, sizeof(hdr), 1, f) != 1) {
fdd_log("FDD Audio: Failed to read WAV header from: %s\n", filename);
fclose(f);
return NULL;
}
if (memcmp(hdr.riff, "RIFF", 4) || memcmp(hdr.wave, "WAVE", 4) || memcmp(hdr.fmt, "fmt ", 4) || memcmp(hdr.data, "data", 4)) {
fdd_log("FDD Audio: Invalid WAV format in file: %s\n", filename);
fclose(f);
return NULL;
}
/* Accept both mono and stereo, 16-bit PCM */
if (hdr.audio_format != 1 || hdr.bits_per_sample != 16 || (hdr.num_channels != 1 && hdr.num_channels != 2)) {
fdd_log("FDD Audio: Unsupported WAV format in %s (format: %d, bits: %d, channels: %d)\n",
filename, hdr.audio_format, hdr.bits_per_sample, hdr.num_channels);
fclose(f);
return NULL;
}
int input_samples = hdr.data_size / 2; /* 2 bytes per sample */
int16_t *input_data = malloc(hdr.data_size);
if (!input_data) {
fdd_log("FDD Audio: Failed to allocate memory for WAV data: %s\n", filename);
fclose(f);
return NULL;
}
if (fread(input_data, 1, hdr.data_size, f) != hdr.data_size) {
fdd_log("FDD Audio: Failed to read WAV data from: %s\n", filename);
free(input_data);
fclose(f);
return NULL;
}
fclose(f);
int16_t *output_data;
int output_samples;
if (hdr.num_channels == 1) {
/* Convert mono to stereo */
output_samples = input_samples; /* Number of stereo sample pairs */
output_data = malloc(input_samples * 2 * sizeof(int16_t)); /* Allocate for stereo */
if (!output_data) {
fdd_log("FDD Audio: Failed to allocate stereo conversion buffer for: %s\n", filename);
free(input_data);
return NULL;
}
/* Convert mono to stereo by duplicating each sample */
for (int i = 0; i < input_samples; i++) {
output_data[i * 2] = input_data[i]; /* Left channel */
output_data[i * 2 + 1] = input_data[i]; /* Right channel */
}
free(input_data);
fdd_log("FDD Audio: Loaded %s (mono->stereo, %d samples)\n", filename, output_samples);
} else {
/* Already stereo */
output_data = input_data;
output_samples = input_samples / 2; /* Number of stereo sample pairs */
fdd_log("FDD Audio: Loaded %s (stereo, %d samples)\n", filename, output_samples);
}
if (sample_count)
*sample_count = output_samples;
return output_data;
}
void
fdd_audio_callback(int16_t *buffer, int length)
{
/* Clear buffer */
memset(buffer, 0, length * sizeof(int16_t));
/* Check if any motor is running or transitioning, or any audio is active */
int any_audio_active = 0;
for (int drive = 0; drive < FDD_NUM; drive++) {
if (spindlemotor_state[drive] != MOTOR_STATE_STOPPED) {
any_audio_active = 1;
break;
}
for (int j = 0; j < MAX_CONCURRENT_SEEKS; j++) {
if (seek_state[drive][j].active) {
any_audio_active = 1;
break;
}
}
if (any_audio_active)
break;
}
if (!any_audio_active)
return;
float *float_buffer = (float *) buffer;
int16_t *int16_buffer = (int16_t *) buffer;
int samples_in_buffer = length / 2;
/* Process audio for all drives */
if (sound_is_float) {
for (int drive = 0; drive < FDD_NUM; drive++) {
drive_audio_samples_t *samples = get_drive_samples(drive);
if (!samples)
continue;
for (int i = 0; i < samples_in_buffer; i++) {
float left_sample = 0.0f;
float right_sample = 0.0f;
/* Process motor audio (unchanged) */
if (spindlemotor_state[drive] != MOTOR_STATE_STOPPED) {
switch (spindlemotor_state[drive]) {
case MOTOR_STATE_STARTING:
if (samples->spindlemotor_start.buffer && spindlemotor_pos[drive] < samples->spindlemotor_start.samples) {
left_sample = (float) samples->spindlemotor_start.buffer[spindlemotor_pos[drive] * 2] / 131072.0f * samples->spindlemotor_start.volume;
right_sample = (float) samples->spindlemotor_start.buffer[spindlemotor_pos[drive] * 2 + 1] / 131072.0f * samples->spindlemotor_start.volume;
spindlemotor_pos[drive]++;
} else {
spindlemotor_state[drive] = MOTOR_STATE_RUNNING;
spindlemotor_pos[drive] = 0;
}
break;
case MOTOR_STATE_RUNNING:
if (samples->spindlemotor_loop.buffer && samples->spindlemotor_loop.samples > 0) {
left_sample = (float) samples->spindlemotor_loop.buffer[spindlemotor_pos[drive] * 2] / 131072.0f * samples->spindlemotor_loop.volume;
right_sample = (float) samples->spindlemotor_loop.buffer[spindlemotor_pos[drive] * 2 + 1] / 131072.0f * samples->spindlemotor_loop.volume;
spindlemotor_pos[drive]++;
if (spindlemotor_pos[drive] >= samples->spindlemotor_loop.samples) {
spindlemotor_pos[drive] = 0;
}
}
break;
case MOTOR_STATE_STOPPING:
if (spindlemotor_fade_samples_remaining[drive] > 0) {
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;
if (samples->spindlemotor_loop.buffer && samples->spindlemotor_loop.samples > 0) {
int loop_pos = spindlemotor_pos[drive] % samples->spindlemotor_loop.samples;
loop_left = (float) samples->spindlemotor_loop.buffer[loop_pos * 2] / 131072.0f * samples->spindlemotor_loop.volume;
loop_right = (float) samples->spindlemotor_loop.buffer[loop_pos * 2 + 1] / 131072.0f * samples->spindlemotor_loop.volume;
}
if (samples->spindlemotor_stop.buffer && spindlemotor_pos[drive] < samples->spindlemotor_stop.samples) {
stop_left = (float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2] / 131072.0f * samples->spindlemotor_stop.volume;
stop_right = (float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2 + 1] / 131072.0f * samples->spindlemotor_stop.volume;
}
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]--;
spindlemotor_fade_volume[drive] = (float) spindlemotor_fade_samples_remaining[drive] / FADE_SAMPLES;
} else {
if (samples->spindlemotor_stop.buffer && spindlemotor_pos[drive] < samples->spindlemotor_stop.samples) {
left_sample = (float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2] / 131072.0f * samples->spindlemotor_stop.volume;
right_sample = (float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2 + 1] / 131072.0f * samples->spindlemotor_stop.volume;
spindlemotor_pos[drive]++;
} else {
spindlemotor_state[drive] = MOTOR_STATE_STOPPED;
}
}
break;
default:
break;
}
}
/* Process all concurrent seek audio slots */
for (int slot = 0; slot < MAX_CONCURRENT_SEEKS; slot++) {
if (!seek_state[drive][slot].active)
continue;
audio_sample_t *seek_sample = seek_state[drive][slot].sample_to_play;
if (seek_sample && seek_sample->buffer && seek_state[drive][slot].position < seek_sample->samples) {
/* Mix seek sound with existing audio */
float seek_left = (float) seek_sample->buffer[seek_state[drive][slot].position * 2] / 131072.0f * seek_sample->volume;
float seek_right = (float) seek_sample->buffer[seek_state[drive][slot].position * 2 + 1] / 131072.0f * seek_sample->volume;
left_sample += seek_left;
right_sample += seek_right;
seek_state[drive][slot].position++;
} else {
/* Seek sound finished */
seek_state[drive][slot].active = 0;
seek_state[drive][slot].position = 0;
seek_state[drive][slot].duration_samples = 0;
seek_state[drive][slot].from_track = -1;
seek_state[drive][slot].to_track = -1;
seek_state[drive][slot].track_diff = 0;
seek_state[drive][slot].sample_to_play = NULL;
}
}
/* Mix this drive's audio into the buffer */
float_buffer[i * 2] += left_sample;
float_buffer[i * 2 + 1] += right_sample;
}
}
} else {
/* int16 version - similar changes */
for (int drive = 0; drive < FDD_NUM; drive++) {
drive_audio_samples_t *samples = get_drive_samples(drive);
if (!samples)
continue;
for (int i = 0; i < samples_in_buffer; i++) {
int16_t left_sample = 0;
int16_t right_sample = 0;
/* Process motor audio (same as float version but with int16) */
if (spindlemotor_state[drive] != MOTOR_STATE_STOPPED) {
switch (spindlemotor_state[drive]) {
case MOTOR_STATE_STARTING:
if (samples->spindlemotor_start.buffer && spindlemotor_pos[drive] < samples->spindlemotor_start.samples) {
left_sample = (int16_t) ((float) samples->spindlemotor_start.buffer[spindlemotor_pos[drive] * 2] / 4.0f * samples->spindlemotor_start.volume);
right_sample = (int16_t) ((float) samples->spindlemotor_start.buffer[spindlemotor_pos[drive] * 2 + 1] / 4.0f * samples->spindlemotor_start.volume);
spindlemotor_pos[drive]++;
} else {
spindlemotor_state[drive] = MOTOR_STATE_RUNNING;
spindlemotor_pos[drive] = 0;
}
break;
case MOTOR_STATE_RUNNING:
if (samples->spindlemotor_loop.buffer && samples->spindlemotor_loop.samples > 0) {
left_sample = (int16_t) ((float) samples->spindlemotor_loop.buffer[spindlemotor_pos[drive] * 2] / 4.0f * samples->spindlemotor_loop.volume);
right_sample = (int16_t) ((float) samples->spindlemotor_loop.buffer[spindlemotor_pos[drive] * 2 + 1] / 4.0f * samples->spindlemotor_loop.volume);
spindlemotor_pos[drive]++;
if (spindlemotor_pos[drive] >= samples->spindlemotor_loop.samples) {
spindlemotor_pos[drive] = 0;
}
}
break;
case MOTOR_STATE_STOPPING:
if (spindlemotor_fade_samples_remaining[drive] > 0) {
float loop_volume = spindlemotor_fade_volume[drive];
float stop_volume = 1.0f - loop_volume;
int16_t loop_left = 0, loop_right = 0;
int16_t stop_left = 0, stop_right = 0;
if (samples->spindlemotor_loop.buffer && samples->spindlemotor_loop.samples > 0) {
int loop_pos = spindlemotor_pos[drive] % samples->spindlemotor_loop.samples;
loop_left = (int16_t) ((float) samples->spindlemotor_loop.buffer[loop_pos * 2] / 4.0f * samples->spindlemotor_loop.volume);
loop_right = (int16_t) ((float) samples->spindlemotor_loop.buffer[loop_pos * 2 + 1] / 4.0f * samples->spindlemotor_loop.volume);
}
if (samples->spindlemotor_stop.buffer && spindlemotor_pos[drive] < samples->spindlemotor_stop.samples) {
stop_left = (int16_t) ((float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2] / 4.0f * samples->spindlemotor_stop.volume);
stop_right = (int16_t) ((float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2 + 1] / 4.0f * samples->spindlemotor_stop.volume);
}
left_sample = (int16_t) (loop_left * loop_volume + stop_left * stop_volume);
right_sample = (int16_t) (loop_right * loop_volume + stop_right * stop_volume);
spindlemotor_pos[drive]++;
spindlemotor_fade_samples_remaining[drive]--;
spindlemotor_fade_volume[drive] = (float) spindlemotor_fade_samples_remaining[drive] / FADE_SAMPLES;
} else {
if (samples->spindlemotor_stop.buffer && spindlemotor_pos[drive] < samples->spindlemotor_stop.samples) {
left_sample = (int16_t) ((float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2] / 4.0f * samples->spindlemotor_stop.volume);
right_sample = (int16_t) ((float) samples->spindlemotor_stop.buffer[spindlemotor_pos[drive] * 2 + 1] / 4.0f * samples->spindlemotor_stop.volume);
spindlemotor_pos[drive]++;
} else {
spindlemotor_state[drive] = MOTOR_STATE_STOPPED;
}
}
break;
default:
break;
}
}
/* Process all concurrent seek audio slots */
for (int slot = 0; slot < MAX_CONCURRENT_SEEKS; slot++) {
if (!seek_state[drive][slot].active)
continue;
audio_sample_t *seek_sample = seek_state[drive][slot].sample_to_play;
if (seek_sample && seek_sample->buffer && seek_state[drive][slot].position < seek_sample->samples) {
/* Mix seek sound with existing audio */
int16_t seek_left = (int16_t) ((float) seek_sample->buffer[seek_state[drive][slot].position * 2] / 4.0f * seek_sample->volume);
int16_t seek_right = (int16_t) ((float) seek_sample->buffer[seek_state[drive][slot].position * 2 + 1] / 4.0f * seek_sample->volume);
left_sample += seek_left;
right_sample += seek_right;
seek_state[drive][slot].position++;
} else {
/* Seek sound finished */
seek_state[drive][slot].active = 0;
seek_state[drive][slot].position = 0;
seek_state[drive][slot].duration_samples = 0;
seek_state[drive][slot].from_track = -1;
seek_state[drive][slot].to_track = -1;
seek_state[drive][slot].track_diff = 0;
seek_state[drive][slot].sample_to_play = NULL;
}
}
/* Mix this drive's audio into the buffer */
int16_buffer[i * 2] += left_sample;
int16_buffer[i * 2 + 1] += right_sample;
}
}
}
}
#else
/* Stub implementations when audio is disabled */
void
fdd_audio_load_profiles(void)
{
}
int
fdd_audio_get_profile_count(void)
{
return 1;
}
const fdd_audio_profile_config_t *
fdd_audio_get_profile(int id)
{
static fdd_audio_profile_config_t none_profile = { 0, "None", "none" };
return (id == 0) ? &none_profile : NULL;
}
const char *
fdd_audio_get_profile_name(int id)
{
return (id == 0) ? "None" : NULL;
}
const char *
fdd_audio_get_profile_internal_name(int id)
{
return (id == 0) ? "none" : NULL;
}
int
fdd_audio_get_profile_by_internal_name(const char *internal_name)
{
return 0;
}
double
fdd_audio_get_seek_time(int drive, int track_count, int is_seek_down)
{
return 0;
}
void
fdd_audio_init(void)
{
}
void
fdd_audio_close(void)
{
}
void
fdd_audio_set_motor_enable(int drive, int motor_enable)
{
}
void
fdd_audio_play_multi_track_seek(int drive, int from_track, int to_track)
{
}
void
fdd_audio_callback(int16_t *buffer, int length)
{
memset(buffer, 0, length * sizeof(int16_t));
}
#endif /* DISABLE_FDD_AUDIO */