mirror of
https://github.com/86Box/86Box.git
synced 2026-02-22 09:35:32 -07:00
- 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.
1351 lines
59 KiB
C
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 */
|