mirror of
https://github.com/86Box/86Box.git
synced 2026-02-25 04:45:31 -07:00
962 lines
34 KiB
C
962 lines
34 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.
|
|
*
|
|
* Hard disk audio emulation.
|
|
*
|
|
* Authors: Toni Riikonen, <riikonen.toni@gmail.com>
|
|
*
|
|
* Copyright 2026 Toni Riikonen.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <86box/86box.h>
|
|
#include <86box/hdd.h>
|
|
#include <86box/hdd_audio.h>
|
|
#include <86box/sound.h>
|
|
#include <86box/sound_util.h>
|
|
#include <86box/thread.h>
|
|
#include <86box/plat.h>
|
|
#include <86box/path.h>
|
|
#include <86box/ini.h>
|
|
#include <86box/mem.h>
|
|
#include <86box/rom.h>
|
|
|
|
/* Maximum number of simultaneous seek sounds per HDD */
|
|
#define HDD_MAX_SEEK_VOICES_PER_HDD 8
|
|
|
|
/* Maximum number of HDDs with audio emulation */
|
|
#define HDD_AUDIO_MAX_DRIVES 8
|
|
|
|
typedef struct {
|
|
int active;
|
|
int position;
|
|
float volume;
|
|
int profile_id; /* Which profile's seek sound to use */
|
|
} hdd_seek_voice_t;
|
|
|
|
/* Per-HDD audio state */
|
|
typedef struct {
|
|
int hdd_index; /* Index into hdd[] array */
|
|
int profile_id; /* Audio profile ID */
|
|
hdd_spindle_state_t spindle_state;
|
|
int spindle_pos;
|
|
int spindle_transition_pos;
|
|
hdd_seek_voice_t seek_voices[HDD_MAX_SEEK_VOICES_PER_HDD];
|
|
} hdd_audio_drive_state_t;
|
|
|
|
/* Audio samples structure for a profile */
|
|
typedef struct {
|
|
int16_t *spindle_start_buffer;
|
|
int spindle_start_samples;
|
|
float spindle_start_volume;
|
|
int16_t *spindle_loop_buffer;
|
|
int spindle_loop_samples;
|
|
float spindle_loop_volume;
|
|
int16_t *spindle_stop_buffer;
|
|
int spindle_stop_samples;
|
|
float spindle_stop_volume;
|
|
int16_t *seek_buffer;
|
|
int seek_samples;
|
|
float seek_volume;
|
|
int loaded;
|
|
} hdd_audio_samples_t;
|
|
|
|
/* Global audio profile configurations */
|
|
static hdd_audio_profile_config_t audio_profiles[HDD_AUDIO_PROFILE_MAX];
|
|
static int audio_profile_count = 0;
|
|
|
|
/* Per-profile loaded samples */
|
|
static hdd_audio_samples_t profile_samples[HDD_AUDIO_PROFILE_MAX];
|
|
|
|
/* Per-HDD audio states */
|
|
static hdd_audio_drive_state_t drive_states[HDD_AUDIO_MAX_DRIVES];
|
|
static int active_drive_count = 0;
|
|
|
|
static mutex_t *hdd_audio_mutex = NULL;
|
|
|
|
/* Load audio profiles from configuration file */
|
|
void
|
|
hdd_audio_load_profiles(void)
|
|
{
|
|
ini_t profiles_ini;
|
|
char cfg_fn[1024] = { 0 };
|
|
|
|
int ret = asset_getfile("assets/sounds/hdd/hdd_audio_profiles.cfg", cfg_fn, 1024);
|
|
if (!ret) {
|
|
pclog("HDD Audio: Could not find hdd_audio_profiles.cfg\n");
|
|
return;
|
|
}
|
|
|
|
profiles_ini = ini_read_ex(cfg_fn, 1);
|
|
if (profiles_ini == NULL) {
|
|
pclog("HDD Audio: Failed to load hdd_audio_profiles.cfg\n");
|
|
return;
|
|
}
|
|
|
|
audio_profile_count = 0;
|
|
|
|
/* Load profiles by trying known profile section names */
|
|
for (int i = 0; i < HDD_AUDIO_PROFILE_MAX && audio_profile_count < HDD_AUDIO_PROFILE_MAX; i++) {
|
|
char section_name[64];
|
|
sprintf(section_name, "Profile \"%d\"", i);
|
|
|
|
ini_section_t cat = ini_find_section(profiles_ini, section_name);
|
|
if (cat == NULL)
|
|
continue;
|
|
|
|
hdd_audio_profile_config_t *config = &audio_profiles[audio_profile_count];
|
|
memset(config, 0, sizeof(hdd_audio_profile_config_t));
|
|
|
|
config->id = ini_section_get_int(cat, "id", i);
|
|
|
|
const char *name = ini_section_get_string(cat, "name", "Unknown");
|
|
strncpy(config->name, name, sizeof(config->name) - 1);
|
|
|
|
const char *internal_name = ini_section_get_string(cat, "internal_name", "unknown");
|
|
strncpy(config->internal_name, internal_name, sizeof(config->internal_name) - 1);
|
|
|
|
config->rpm = ini_section_get_int(cat, "rpm", 0);
|
|
|
|
/* Load spindle motor sample files */
|
|
const char *file = ini_section_get_string(cat, "spindlemotor_start_file", "");
|
|
strncpy(config->spindlemotor_start.filename, file, sizeof(config->spindlemotor_start.filename) - 1);
|
|
config->spindlemotor_start.volume = (float) ini_section_get_double(cat, "spindlemotor_start_volume", 1.0);
|
|
|
|
file = ini_section_get_string(cat, "spindlemotor_loop_file", "");
|
|
strncpy(config->spindlemotor_loop.filename, file, sizeof(config->spindlemotor_loop.filename) - 1);
|
|
config->spindlemotor_loop.volume = (float) ini_section_get_double(cat, "spindlemotor_loop_volume", 1.0);
|
|
|
|
file = ini_section_get_string(cat, "spindlemotor_stop_file", "");
|
|
strncpy(config->spindlemotor_stop.filename, file, sizeof(config->spindlemotor_stop.filename) - 1);
|
|
config->spindlemotor_stop.volume = (float) ini_section_get_double(cat, "spindlemotor_stop_volume", 1.0);
|
|
|
|
/* Load seek sample file */
|
|
file = ini_section_get_string(cat, "seek_track_file", "");
|
|
strncpy(config->seek_track.filename, file, sizeof(config->seek_track.filename) - 1);
|
|
config->seek_track.volume = (float) ini_section_get_double(cat, "seek_track_volume", 1.0);
|
|
|
|
pclog("HDD Audio: Loaded profile %d: %s (%s)\n",
|
|
audio_profile_count, config->name, config->internal_name);
|
|
|
|
audio_profile_count++;
|
|
}
|
|
|
|
ini_close(profiles_ini);
|
|
|
|
pclog("HDD Audio: Loaded %d audio profiles\n", audio_profile_count);
|
|
}
|
|
|
|
/* Public API functions */
|
|
int
|
|
hdd_audio_get_profile_count(void)
|
|
{
|
|
return audio_profile_count;
|
|
}
|
|
|
|
const hdd_audio_profile_config_t *
|
|
hdd_audio_get_profile(int id)
|
|
{
|
|
if (id < 0 || id >= audio_profile_count)
|
|
return NULL;
|
|
return &audio_profiles[id];
|
|
}
|
|
|
|
const char *
|
|
hdd_audio_get_profile_name(int id)
|
|
{
|
|
if (id < 0 || id >= audio_profile_count)
|
|
return NULL;
|
|
return audio_profiles[id].name;
|
|
}
|
|
|
|
const char *
|
|
hdd_audio_get_profile_internal_name(int id)
|
|
{
|
|
if (id < 0 || id >= audio_profile_count)
|
|
return NULL;
|
|
return audio_profiles[id].internal_name;
|
|
}
|
|
|
|
uint32_t
|
|
hdd_audio_get_profile_rpm(int id)
|
|
{
|
|
if (id < 0 || id >= audio_profile_count)
|
|
return 0;
|
|
return audio_profiles[id].rpm;
|
|
}
|
|
|
|
int
|
|
hdd_audio_get_profile_by_internal_name(const char *internal_name)
|
|
{
|
|
if (!internal_name)
|
|
return 0;
|
|
|
|
for (int i = 0; i < audio_profile_count; i++) {
|
|
if (strcmp(audio_profiles[i].internal_name, internal_name) == 0)
|
|
return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
hdd_audio_close(void)
|
|
{
|
|
/* Free all loaded profile samples */
|
|
for (int i = 0; i < HDD_AUDIO_PROFILE_MAX; i++) {
|
|
if (profile_samples[i].spindle_start_buffer) {
|
|
free(profile_samples[i].spindle_start_buffer);
|
|
profile_samples[i].spindle_start_buffer = NULL;
|
|
}
|
|
if (profile_samples[i].spindle_loop_buffer) {
|
|
free(profile_samples[i].spindle_loop_buffer);
|
|
profile_samples[i].spindle_loop_buffer = NULL;
|
|
}
|
|
if (profile_samples[i].spindle_stop_buffer) {
|
|
free(profile_samples[i].spindle_stop_buffer);
|
|
profile_samples[i].spindle_stop_buffer = NULL;
|
|
}
|
|
if (profile_samples[i].seek_buffer) {
|
|
free(profile_samples[i].seek_buffer);
|
|
profile_samples[i].seek_buffer = NULL;
|
|
}
|
|
profile_samples[i].loaded = 0;
|
|
}
|
|
|
|
if (hdd_audio_mutex) {
|
|
thread_close_mutex(hdd_audio_mutex);
|
|
hdd_audio_mutex = NULL;
|
|
}
|
|
}
|
|
|
|
/* Load samples for a specific profile */
|
|
static void
|
|
hdd_audio_load_profile_samples(int profile_id)
|
|
{
|
|
if (profile_id < 0 || profile_id >= audio_profile_count)
|
|
return;
|
|
|
|
hdd_audio_profile_config_t *config = &audio_profiles[profile_id];
|
|
hdd_audio_samples_t *samples = &profile_samples[profile_id];
|
|
|
|
/* Already loaded? */
|
|
if (samples->loaded)
|
|
return;
|
|
|
|
/* Profile 0 is "None" - no audio */
|
|
if (profile_id == 0 || strcmp(config->internal_name, "none") == 0) {
|
|
samples->loaded = 1;
|
|
return;
|
|
}
|
|
|
|
pclog("HDD Audio: Loading samples for profile %d (%s)\n", profile_id, config->name);
|
|
|
|
/* Load spindle loop (main running sound) */
|
|
if (config->spindlemotor_loop.filename[0]) {
|
|
samples->spindle_loop_buffer = sound_load_wav(
|
|
config->spindlemotor_loop.filename,
|
|
&samples->spindle_loop_samples);
|
|
if (samples->spindle_loop_buffer) {
|
|
samples->spindle_loop_volume = config->spindlemotor_loop.volume;
|
|
pclog("HDD Audio: Loaded spindle loop, %d frames\n", samples->spindle_loop_samples);
|
|
} else {
|
|
pclog("HDD Audio: Failed to load spindle loop: %s\n", config->spindlemotor_loop.filename);
|
|
}
|
|
}
|
|
|
|
/* Load spindle start */
|
|
if (config->spindlemotor_start.filename[0]) {
|
|
samples->spindle_start_buffer = sound_load_wav(
|
|
config->spindlemotor_start.filename,
|
|
&samples->spindle_start_samples);
|
|
if (samples->spindle_start_buffer) {
|
|
samples->spindle_start_volume = config->spindlemotor_start.volume;
|
|
pclog("HDD Audio: Loaded spindle start, %d frames\n", samples->spindle_start_samples);
|
|
}
|
|
}
|
|
|
|
/* Load spindle stop */
|
|
if (config->spindlemotor_stop.filename[0]) {
|
|
samples->spindle_stop_buffer = sound_load_wav(
|
|
config->spindlemotor_stop.filename,
|
|
&samples->spindle_stop_samples);
|
|
if (samples->spindle_stop_buffer) {
|
|
samples->spindle_stop_volume = config->spindlemotor_stop.volume;
|
|
pclog("HDD Audio: Loaded spindle stop, %d frames\n", samples->spindle_stop_samples);
|
|
}
|
|
}
|
|
|
|
/* Load seek sound */
|
|
if (config->seek_track.filename[0]) {
|
|
samples->seek_buffer = sound_load_wav(
|
|
config->seek_track.filename,
|
|
&samples->seek_samples);
|
|
if (samples->seek_buffer) {
|
|
samples->seek_volume = config->seek_track.volume;
|
|
pclog("HDD Audio: Loaded seek sound, %d frames (%.1f ms)\n",
|
|
samples->seek_samples, (float)samples->seek_samples / 48.0f);
|
|
} else {
|
|
pclog("HDD Audio: Failed to load seek sound: %s\n", config->seek_track.filename);
|
|
}
|
|
}
|
|
|
|
samples->loaded = 1;
|
|
}
|
|
|
|
/* Find drive state for a given HDD index, or NULL if not tracked */
|
|
static hdd_audio_drive_state_t *
|
|
hdd_audio_find_drive_state(int hdd_index)
|
|
{
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
if (drive_states[i].hdd_index == hdd_index)
|
|
return &drive_states[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
hdd_audio_init(void)
|
|
{
|
|
/* Initialize profile samples */
|
|
memset(profile_samples, 0, sizeof(profile_samples));
|
|
memset(drive_states, 0, sizeof(drive_states));
|
|
active_drive_count = 0;
|
|
|
|
pclog("HDD Audio Init: audio_profile_count=%d\n", audio_profile_count);
|
|
|
|
/* Create mutex BEFORE loading samples or calling spinup */
|
|
if (!hdd_audio_mutex)
|
|
hdd_audio_mutex = thread_create_mutex();
|
|
|
|
/* Find all HDDs with valid audio profiles and initialize their states */
|
|
for (int i = 0; i < HDD_NUM && active_drive_count < HDD_AUDIO_MAX_DRIVES; i++) {
|
|
if (hdd[i].bus_type != HDD_BUS_DISABLED && hdd[i].audio_profile > 0) {
|
|
pclog("HDD Audio Init: HDD %d bus_type=%d audio_profile=%d\n",
|
|
i, hdd[i].bus_type, hdd[i].audio_profile);
|
|
|
|
hdd_audio_drive_state_t *state = &drive_states[active_drive_count];
|
|
state->hdd_index = i;
|
|
state->profile_id = hdd[i].audio_profile;
|
|
state->spindle_state = HDD_SPINDLE_STOPPED;
|
|
state->spindle_pos = 0;
|
|
state->spindle_transition_pos = 0;
|
|
|
|
/* Initialize seek voices for this drive */
|
|
for (int v = 0; v < HDD_MAX_SEEK_VOICES_PER_HDD; v++) {
|
|
state->seek_voices[v].active = 0;
|
|
state->seek_voices[v].position = 0;
|
|
state->seek_voices[v].volume = 1.0f;
|
|
state->seek_voices[v].profile_id = state->profile_id;
|
|
}
|
|
|
|
/* Load samples for this profile if not already loaded */
|
|
hdd_audio_load_profile_samples(state->profile_id);
|
|
|
|
pclog("HDD Audio: Initialized drive %d with profile %d (%s)\n",
|
|
i, state->profile_id,
|
|
hdd_audio_get_profile_name(state->profile_id));
|
|
|
|
active_drive_count++;
|
|
}
|
|
}
|
|
|
|
pclog("HDD Audio Init: %d active drives with audio\n", active_drive_count);
|
|
|
|
/* Start spindle motors for all active drives */
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
hdd_audio_spinup_drive(drive_states[i].hdd_index);
|
|
}
|
|
|
|
sound_hdd_thread_init();
|
|
}
|
|
|
|
void
|
|
hdd_audio_reset(void)
|
|
{
|
|
pclog("HDD Audio: Reset\n");
|
|
|
|
/* Lock mutex to prevent audio callback from accessing buffers during reset */
|
|
if (hdd_audio_mutex)
|
|
thread_wait_mutex(hdd_audio_mutex);
|
|
|
|
/* Reset all drive states */
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
drive_states[i].spindle_state = HDD_SPINDLE_STOPPED;
|
|
drive_states[i].spindle_pos = 0;
|
|
drive_states[i].spindle_transition_pos = 0;
|
|
for (int v = 0; v < HDD_MAX_SEEK_VOICES_PER_HDD; v++) {
|
|
drive_states[i].seek_voices[v].active = 0;
|
|
drive_states[i].seek_voices[v].position = 0;
|
|
drive_states[i].seek_voices[v].volume = 1.0f;
|
|
}
|
|
}
|
|
active_drive_count = 0;
|
|
|
|
/* Free previously loaded samples (but keep profiles) */
|
|
for (int i = 0; i < HDD_AUDIO_PROFILE_MAX; i++) {
|
|
if (profile_samples[i].spindle_start_buffer) {
|
|
free(profile_samples[i].spindle_start_buffer);
|
|
profile_samples[i].spindle_start_buffer = NULL;
|
|
}
|
|
if (profile_samples[i].spindle_loop_buffer) {
|
|
free(profile_samples[i].spindle_loop_buffer);
|
|
profile_samples[i].spindle_loop_buffer = NULL;
|
|
}
|
|
if (profile_samples[i].spindle_stop_buffer) {
|
|
free(profile_samples[i].spindle_stop_buffer);
|
|
profile_samples[i].spindle_stop_buffer = NULL;
|
|
}
|
|
if (profile_samples[i].seek_buffer) {
|
|
free(profile_samples[i].seek_buffer);
|
|
profile_samples[i].seek_buffer = NULL;
|
|
}
|
|
profile_samples[i].loaded = 0;
|
|
}
|
|
|
|
if (hdd_audio_mutex)
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
|
|
/* Find all HDDs with valid audio profiles and initialize their states */
|
|
for (int i = 0; i < HDD_NUM && active_drive_count < HDD_AUDIO_MAX_DRIVES; i++) {
|
|
if (hdd[i].bus_type != HDD_BUS_DISABLED && hdd[i].audio_profile > 0) {
|
|
pclog("HDD Audio Reset: HDD %d audio_profile=%d\n", i, hdd[i].audio_profile);
|
|
|
|
hdd_audio_drive_state_t *state = &drive_states[active_drive_count];
|
|
state->hdd_index = i;
|
|
state->profile_id = hdd[i].audio_profile;
|
|
state->spindle_state = HDD_SPINDLE_STOPPED;
|
|
state->spindle_pos = 0;
|
|
state->spindle_transition_pos = 0;
|
|
|
|
/* Initialize seek voices for this drive */
|
|
for (int v = 0; v < HDD_MAX_SEEK_VOICES_PER_HDD; v++) {
|
|
state->seek_voices[v].active = 0;
|
|
state->seek_voices[v].position = 0;
|
|
state->seek_voices[v].volume = 1.0f;
|
|
state->seek_voices[v].profile_id = state->profile_id;
|
|
}
|
|
|
|
/* Load samples for this profile if not already loaded */
|
|
hdd_audio_load_profile_samples(state->profile_id);
|
|
|
|
pclog("HDD Audio: Reset drive %d with profile %d (%s)\n",
|
|
i, state->profile_id,
|
|
hdd_audio_get_profile_name(state->profile_id));
|
|
|
|
active_drive_count++;
|
|
}
|
|
}
|
|
|
|
pclog("HDD Audio Reset: %d active drives with audio\n", active_drive_count);
|
|
|
|
/* Start spindle motors for all active drives */
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
hdd_audio_spinup_drive(drive_states[i].hdd_index);
|
|
}
|
|
}
|
|
|
|
void
|
|
hdd_audio_seek(hard_disk_t *hdd_drive, uint32_t new_cylinder)
|
|
{
|
|
uint32_t cylinder_diff = abs((int) hdd_drive->cur_cylinder - (int) new_cylinder);
|
|
|
|
if (cylinder_diff == 0)
|
|
return;
|
|
|
|
/* Find the drive state for this HDD */
|
|
hdd_audio_drive_state_t *drive_state = NULL;
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
if (&hdd[drive_states[i].hdd_index] == hdd_drive) {
|
|
drive_state = &drive_states[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* If no drive state found, drive has no audio profile */
|
|
if (!drive_state)
|
|
return;
|
|
|
|
int profile_id = drive_state->profile_id;
|
|
|
|
/* No audio profile selected */
|
|
if (profile_id == 0 || profile_id >= audio_profile_count)
|
|
return;
|
|
|
|
/* Load samples if not already loaded */
|
|
if (!profile_samples[profile_id].loaded)
|
|
hdd_audio_load_profile_samples(profile_id);
|
|
|
|
hdd_audio_samples_t *samples = &profile_samples[profile_id];
|
|
|
|
if (!samples->seek_buffer || samples->seek_samples == 0) {
|
|
return;
|
|
}
|
|
|
|
/* Mutex must exist */
|
|
if (!hdd_audio_mutex)
|
|
return;
|
|
|
|
int min_seek_spacing = 0;
|
|
if (hdd_drive->cyl_switch_usec > 0)
|
|
min_seek_spacing = (int)(hdd_drive->cyl_switch_usec * 48000.0 / 1000000.0);
|
|
|
|
thread_wait_mutex(hdd_audio_mutex);
|
|
|
|
/* Check if we should skip due to minimum spacing (per-drive) */
|
|
for (int v = 0; v < HDD_MAX_SEEK_VOICES_PER_HDD; v++) {
|
|
if (drive_state->seek_voices[v].active) {
|
|
int pos = drive_state->seek_voices[v].position;
|
|
if (pos >= 0 && pos < min_seek_spacing) {
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Find a free seek voice for this drive */
|
|
for (int v = 0; v < HDD_MAX_SEEK_VOICES_PER_HDD; v++) {
|
|
if (!drive_state->seek_voices[v].active) {
|
|
drive_state->seek_voices[v].active = 1;
|
|
drive_state->seek_voices[v].position = 0;
|
|
drive_state->seek_voices[v].volume = samples->seek_volume;
|
|
drive_state->seek_voices[v].profile_id = profile_id;
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
return;
|
|
}
|
|
}
|
|
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
}
|
|
|
|
/* Spinup a specific drive by HDD index */
|
|
void
|
|
hdd_audio_spinup_drive(int hdd_index)
|
|
{
|
|
hdd_audio_drive_state_t *state = hdd_audio_find_drive_state(hdd_index);
|
|
if (!state)
|
|
return;
|
|
|
|
if (state->spindle_state == HDD_SPINDLE_RUNNING || state->spindle_state == HDD_SPINDLE_STARTING)
|
|
return;
|
|
|
|
pclog("HDD Audio: Spinup requested for drive %d (current state: %d)\n", hdd_index, state->spindle_state);
|
|
|
|
if (hdd_audio_mutex)
|
|
thread_wait_mutex(hdd_audio_mutex);
|
|
state->spindle_state = HDD_SPINDLE_STARTING;
|
|
state->spindle_transition_pos = 0;
|
|
if (hdd_audio_mutex)
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
}
|
|
|
|
/* Spindown a specific drive by HDD index */
|
|
void
|
|
hdd_audio_spindown_drive(int hdd_index)
|
|
{
|
|
hdd_audio_drive_state_t *state = hdd_audio_find_drive_state(hdd_index);
|
|
if (!state)
|
|
return;
|
|
|
|
if (state->spindle_state == HDD_SPINDLE_STOPPED || state->spindle_state == HDD_SPINDLE_STOPPING)
|
|
return;
|
|
|
|
pclog("HDD Audio: Spindown requested for drive %d (current state: %d)\n", hdd_index, state->spindle_state);
|
|
|
|
if (hdd_audio_mutex)
|
|
thread_wait_mutex(hdd_audio_mutex);
|
|
state->spindle_state = HDD_SPINDLE_STOPPING;
|
|
state->spindle_transition_pos = 0;
|
|
if (hdd_audio_mutex)
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
}
|
|
|
|
/* Legacy functions for backward compatibility - operate on all drives */
|
|
void
|
|
hdd_audio_spinup(void)
|
|
{
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
hdd_audio_spinup_drive(drive_states[i].hdd_index);
|
|
}
|
|
}
|
|
|
|
void
|
|
hdd_audio_spindown(void)
|
|
{
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
hdd_audio_spindown_drive(drive_states[i].hdd_index);
|
|
}
|
|
}
|
|
|
|
hdd_spindle_state_t
|
|
hdd_audio_get_spindle_state(void)
|
|
{
|
|
/* Return running if any drive is running */
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
if (drive_states[i].spindle_state == HDD_SPINDLE_RUNNING)
|
|
return HDD_SPINDLE_RUNNING;
|
|
}
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
if (drive_states[i].spindle_state == HDD_SPINDLE_STARTING)
|
|
return HDD_SPINDLE_STARTING;
|
|
}
|
|
for (int i = 0; i < active_drive_count; i++) {
|
|
if (drive_states[i].spindle_state == HDD_SPINDLE_STOPPING)
|
|
return HDD_SPINDLE_STOPPING;
|
|
}
|
|
return HDD_SPINDLE_STOPPED;
|
|
}
|
|
|
|
hdd_spindle_state_t
|
|
hdd_audio_get_drive_spindle_state(int hdd_index)
|
|
{
|
|
hdd_audio_drive_state_t *state = hdd_audio_find_drive_state(hdd_index);
|
|
if (!state)
|
|
return HDD_SPINDLE_STOPPED;
|
|
return state->spindle_state;
|
|
}
|
|
|
|
/* Helper: Mix spindle start sound into float buffer */
|
|
static void
|
|
hdd_audio_mix_spindle_start_float(hdd_audio_drive_state_t *state, hdd_audio_samples_t *samples,
|
|
float *float_buffer, int frames_in_buffer)
|
|
{
|
|
if (!samples->spindle_start_buffer || samples->spindle_start_samples <= 0) {
|
|
state->spindle_state = HDD_SPINDLE_RUNNING;
|
|
state->spindle_pos = 0;
|
|
return;
|
|
}
|
|
|
|
float start_volume = samples->spindle_start_volume;
|
|
for (int i = 0; i < frames_in_buffer && state->spindle_transition_pos < samples->spindle_start_samples; i++) {
|
|
float left_sample = (float) samples->spindle_start_buffer[state->spindle_transition_pos * 2] / 131072.0f * start_volume;
|
|
float right_sample = (float) samples->spindle_start_buffer[state->spindle_transition_pos * 2 + 1] / 131072.0f * start_volume;
|
|
float_buffer[i * 2] += left_sample;
|
|
float_buffer[i * 2 + 1] += right_sample;
|
|
state->spindle_transition_pos++;
|
|
}
|
|
|
|
if (state->spindle_transition_pos >= samples->spindle_start_samples) {
|
|
state->spindle_state = HDD_SPINDLE_RUNNING;
|
|
state->spindle_pos = 0;
|
|
pclog("HDD Audio: Drive %d spinup complete, now running\n", state->hdd_index);
|
|
}
|
|
}
|
|
|
|
/* Helper: Mix spindle loop sound into float buffer */
|
|
static void
|
|
hdd_audio_mix_spindle_loop_float(hdd_audio_drive_state_t *state, hdd_audio_samples_t *samples,
|
|
float *float_buffer, int frames_in_buffer)
|
|
{
|
|
if (!samples->spindle_loop_buffer || samples->spindle_loop_samples <= 0)
|
|
return;
|
|
|
|
float spindle_volume = samples->spindle_loop_volume;
|
|
for (int i = 0; i < frames_in_buffer; i++) {
|
|
float left_sample = (float) samples->spindle_loop_buffer[state->spindle_pos * 2] / 131072.0f * spindle_volume;
|
|
float right_sample = (float) samples->spindle_loop_buffer[state->spindle_pos * 2 + 1] / 131072.0f * spindle_volume;
|
|
float_buffer[i * 2] += left_sample;
|
|
float_buffer[i * 2 + 1] += right_sample;
|
|
|
|
state->spindle_pos++;
|
|
if (state->spindle_pos >= samples->spindle_loop_samples) {
|
|
state->spindle_pos = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Helper: Mix spindle stop sound into float buffer */
|
|
static void
|
|
hdd_audio_mix_spindle_stop_float(hdd_audio_drive_state_t *state, hdd_audio_samples_t *samples,
|
|
float *float_buffer, int frames_in_buffer)
|
|
{
|
|
if (!samples->spindle_stop_buffer || samples->spindle_stop_samples <= 0) {
|
|
state->spindle_state = HDD_SPINDLE_STOPPED;
|
|
return;
|
|
}
|
|
|
|
float stop_volume = samples->spindle_stop_volume;
|
|
for (int i = 0; i < frames_in_buffer && state->spindle_transition_pos < samples->spindle_stop_samples; i++) {
|
|
float left_sample = (float) samples->spindle_stop_buffer[state->spindle_transition_pos * 2] / 131072.0f * stop_volume;
|
|
float right_sample = (float) samples->spindle_stop_buffer[state->spindle_transition_pos * 2 + 1] / 131072.0f * stop_volume;
|
|
float_buffer[i * 2] += left_sample;
|
|
float_buffer[i * 2 + 1] += right_sample;
|
|
state->spindle_transition_pos++;
|
|
}
|
|
|
|
if (state->spindle_transition_pos >= samples->spindle_stop_samples) {
|
|
state->spindle_state = HDD_SPINDLE_STOPPED;
|
|
pclog("HDD Audio: Drive %d spindown complete, now stopped\n", state->hdd_index);
|
|
}
|
|
}
|
|
|
|
/* Helper: Mix seek sounds into float buffer */
|
|
static void
|
|
hdd_audio_mix_seek_float(hdd_audio_drive_state_t *state, float *float_buffer, int frames_in_buffer)
|
|
{
|
|
for (int v = 0; v < HDD_MAX_SEEK_VOICES_PER_HDD; v++) {
|
|
if (!state->seek_voices[v].active)
|
|
continue;
|
|
|
|
int seek_profile_id = state->seek_voices[v].profile_id;
|
|
hdd_audio_samples_t *seek_samples = &profile_samples[seek_profile_id];
|
|
if (!seek_samples->seek_buffer || seek_samples->seek_samples == 0)
|
|
continue;
|
|
|
|
float voice_vol = state->seek_voices[v].volume;
|
|
int pos = state->seek_voices[v].position;
|
|
if (pos < 0) pos = 0;
|
|
|
|
for (int i = 0; i < frames_in_buffer && pos < seek_samples->seek_samples; i++, pos++) {
|
|
float seek_left = (float) seek_samples->seek_buffer[pos * 2] / 131072.0f * voice_vol;
|
|
float seek_right = (float) seek_samples->seek_buffer[pos * 2 + 1] / 131072.0f * voice_vol;
|
|
|
|
float_buffer[i * 2] += seek_left;
|
|
float_buffer[i * 2 + 1] += seek_right;
|
|
}
|
|
|
|
if (pos >= seek_samples->seek_samples) {
|
|
state->seek_voices[v].active = 0;
|
|
state->seek_voices[v].position = 0;
|
|
} else {
|
|
state->seek_voices[v].position = pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Helper: Mix spindle start sound into int16 buffer */
|
|
static void
|
|
hdd_audio_mix_spindle_start_int16(hdd_audio_drive_state_t *state, hdd_audio_samples_t *samples,
|
|
int16_t *buffer, int frames_in_buffer)
|
|
{
|
|
if (!samples->spindle_start_buffer || samples->spindle_start_samples <= 0) {
|
|
state->spindle_state = HDD_SPINDLE_RUNNING;
|
|
state->spindle_pos = 0;
|
|
return;
|
|
}
|
|
|
|
float start_volume = samples->spindle_start_volume;
|
|
for (int i = 0; i < frames_in_buffer && state->spindle_transition_pos < samples->spindle_start_samples; i++) {
|
|
int32_t left = buffer[i * 2] + (int32_t)(samples->spindle_start_buffer[state->spindle_transition_pos * 2] * start_volume);
|
|
int32_t right = buffer[i * 2 + 1] + (int32_t)(samples->spindle_start_buffer[state->spindle_transition_pos * 2 + 1] * start_volume);
|
|
if (left > 32767) left = 32767;
|
|
if (left < -32768) left = -32768;
|
|
if (right > 32767) right = 32767;
|
|
if (right < -32768) right = -32768;
|
|
buffer[i * 2] = (int16_t) left;
|
|
buffer[i * 2 + 1] = (int16_t) right;
|
|
state->spindle_transition_pos++;
|
|
}
|
|
|
|
if (state->spindle_transition_pos >= samples->spindle_start_samples) {
|
|
state->spindle_state = HDD_SPINDLE_RUNNING;
|
|
state->spindle_pos = 0;
|
|
pclog("HDD Audio: Drive %d spinup complete, now running\n", state->hdd_index);
|
|
}
|
|
}
|
|
|
|
/* Helper: Mix spindle loop sound into int16 buffer */
|
|
static void
|
|
hdd_audio_mix_spindle_loop_int16(hdd_audio_drive_state_t *state, hdd_audio_samples_t *samples,
|
|
int16_t *buffer, int frames_in_buffer)
|
|
{
|
|
if (!samples->spindle_loop_buffer || samples->spindle_loop_samples <= 0)
|
|
return;
|
|
|
|
float spindle_volume = samples->spindle_loop_volume;
|
|
for (int i = 0; i < frames_in_buffer; i++) {
|
|
int32_t left = buffer[i * 2] + (int32_t)(samples->spindle_loop_buffer[state->spindle_pos * 2] * spindle_volume);
|
|
int32_t right = buffer[i * 2 + 1] + (int32_t)(samples->spindle_loop_buffer[state->spindle_pos * 2 + 1] * spindle_volume);
|
|
if (left > 32767) left = 32767;
|
|
if (left < -32768) left = -32768;
|
|
if (right > 32767) right = 32767;
|
|
if (right < -32768) right = -32768;
|
|
buffer[i * 2] = (int16_t) left;
|
|
buffer[i * 2 + 1] = (int16_t) right;
|
|
|
|
state->spindle_pos++;
|
|
if (state->spindle_pos >= samples->spindle_loop_samples) {
|
|
state->spindle_pos = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Helper: Mix spindle stop sound into int16 buffer */
|
|
static void
|
|
hdd_audio_mix_spindle_stop_int16(hdd_audio_drive_state_t *state, hdd_audio_samples_t *samples,
|
|
int16_t *buffer, int frames_in_buffer)
|
|
{
|
|
if (!samples->spindle_stop_buffer || samples->spindle_stop_samples <= 0) {
|
|
state->spindle_state = HDD_SPINDLE_STOPPED;
|
|
return;
|
|
}
|
|
|
|
float stop_volume = samples->spindle_stop_volume;
|
|
for (int i = 0; i < frames_in_buffer && state->spindle_transition_pos < samples->spindle_stop_samples; i++) {
|
|
int32_t left = buffer[i * 2] + (int32_t)(samples->spindle_stop_buffer[state->spindle_transition_pos * 2] * stop_volume);
|
|
int32_t right = buffer[i * 2 + 1] + (int32_t)(samples->spindle_stop_buffer[state->spindle_transition_pos * 2 + 1] * stop_volume);
|
|
if (left > 32767) left = 32767;
|
|
if (left < -32768) left = -32768;
|
|
if (right > 32767) right = 32767;
|
|
if (right < -32768) right = -32768;
|
|
buffer[i * 2] = (int16_t) left;
|
|
buffer[i * 2 + 1] = (int16_t) right;
|
|
state->spindle_transition_pos++;
|
|
}
|
|
|
|
if (state->spindle_transition_pos >= samples->spindle_stop_samples) {
|
|
state->spindle_state = HDD_SPINDLE_STOPPED;
|
|
pclog("HDD Audio: Drive %d spindown complete, now stopped\n", state->hdd_index);
|
|
}
|
|
}
|
|
|
|
/* Helper: Mix seek sounds into int16 buffer */
|
|
static void
|
|
hdd_audio_mix_seek_int16(hdd_audio_drive_state_t *state, int16_t *buffer, int frames_in_buffer)
|
|
{
|
|
for (int v = 0; v < HDD_MAX_SEEK_VOICES_PER_HDD; v++) {
|
|
if (!state->seek_voices[v].active)
|
|
continue;
|
|
|
|
int seek_profile_id = state->seek_voices[v].profile_id;
|
|
hdd_audio_samples_t *seek_samples = &profile_samples[seek_profile_id];
|
|
if (!seek_samples->seek_buffer || seek_samples->seek_samples == 0)
|
|
continue;
|
|
|
|
float voice_vol = state->seek_voices[v].volume;
|
|
int pos = state->seek_voices[v].position;
|
|
if (pos < 0) pos = 0;
|
|
|
|
for (int i = 0; i < frames_in_buffer && pos < seek_samples->seek_samples; i++, pos++) {
|
|
int32_t left = buffer[i * 2] + (int32_t)(seek_samples->seek_buffer[pos * 2] * voice_vol);
|
|
int32_t right = buffer[i * 2 + 1] + (int32_t)(seek_samples->seek_buffer[pos * 2 + 1] * voice_vol);
|
|
|
|
if (left > 32767) left = 32767;
|
|
if (left < -32768) left = -32768;
|
|
if (right > 32767) right = 32767;
|
|
if (right < -32768) right = -32768;
|
|
|
|
buffer[i * 2] = (int16_t) left;
|
|
buffer[i * 2 + 1] = (int16_t) right;
|
|
}
|
|
|
|
if (pos >= seek_samples->seek_samples) {
|
|
state->seek_voices[v].active = 0;
|
|
state->seek_voices[v].position = 0;
|
|
} else {
|
|
state->seek_voices[v].position = pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Process a single drive's audio in float mode */
|
|
static void
|
|
hdd_audio_process_drive_float(hdd_audio_drive_state_t *state, float *float_buffer, int frames_in_buffer)
|
|
{
|
|
int profile_id = state->profile_id;
|
|
|
|
if (profile_id <= 0 || profile_id >= HDD_AUDIO_PROFILE_MAX)
|
|
return;
|
|
|
|
hdd_audio_samples_t *samples = &profile_samples[profile_id];
|
|
if (!samples->loaded)
|
|
return;
|
|
|
|
/* Handle spindle states for this drive */
|
|
switch (state->spindle_state) {
|
|
case HDD_SPINDLE_STARTING:
|
|
hdd_audio_mix_spindle_start_float(state, samples, float_buffer, frames_in_buffer);
|
|
break;
|
|
case HDD_SPINDLE_RUNNING:
|
|
hdd_audio_mix_spindle_loop_float(state, samples, float_buffer, frames_in_buffer);
|
|
break;
|
|
case HDD_SPINDLE_STOPPING:
|
|
hdd_audio_mix_spindle_stop_float(state, samples, float_buffer, frames_in_buffer);
|
|
break;
|
|
case HDD_SPINDLE_STOPPED:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* Seek sounds - only play when spindle is running */
|
|
if (samples->seek_buffer && samples->seek_samples > 0 &&
|
|
hdd_audio_mutex && state->spindle_state == HDD_SPINDLE_RUNNING) {
|
|
thread_wait_mutex(hdd_audio_mutex);
|
|
hdd_audio_mix_seek_float(state, float_buffer, frames_in_buffer);
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
}
|
|
}
|
|
|
|
/* Process a single drive's audio in int16 mode */
|
|
static void
|
|
hdd_audio_process_drive_int16(hdd_audio_drive_state_t *state, int16_t *buffer, int frames_in_buffer)
|
|
{
|
|
int profile_id = state->profile_id;
|
|
|
|
if (profile_id <= 0 || profile_id >= HDD_AUDIO_PROFILE_MAX)
|
|
return;
|
|
|
|
hdd_audio_samples_t *samples = &profile_samples[profile_id];
|
|
if (!samples->loaded)
|
|
return;
|
|
|
|
/* Handle spindle states for this drive */
|
|
switch (state->spindle_state) {
|
|
case HDD_SPINDLE_STARTING:
|
|
hdd_audio_mix_spindle_start_int16(state, samples, buffer, frames_in_buffer);
|
|
break;
|
|
case HDD_SPINDLE_RUNNING:
|
|
hdd_audio_mix_spindle_loop_int16(state, samples, buffer, frames_in_buffer);
|
|
break;
|
|
case HDD_SPINDLE_STOPPING:
|
|
hdd_audio_mix_spindle_stop_int16(state, samples, buffer, frames_in_buffer);
|
|
break;
|
|
case HDD_SPINDLE_STOPPED:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* Seek sounds - only play when spindle is running */
|
|
if (samples->seek_buffer && samples->seek_samples > 0 &&
|
|
hdd_audio_mutex && state->spindle_state == HDD_SPINDLE_RUNNING) {
|
|
thread_wait_mutex(hdd_audio_mutex);
|
|
hdd_audio_mix_seek_int16(state, buffer, frames_in_buffer);
|
|
thread_release_mutex(hdd_audio_mutex);
|
|
}
|
|
}
|
|
|
|
void
|
|
hdd_audio_callback(int16_t *buffer, int length)
|
|
{
|
|
int frames_in_buffer = length / 2;
|
|
|
|
if (sound_is_float) {
|
|
float *float_buffer = (float *) buffer;
|
|
|
|
/* Initialize buffer to silence */
|
|
for (int i = 0; i < length; i++) {
|
|
float_buffer[i] = 0.0f;
|
|
}
|
|
|
|
/* Process each active drive */
|
|
for (int d = 0; d < active_drive_count; d++) {
|
|
hdd_audio_process_drive_float(&drive_states[d], float_buffer, frames_in_buffer);
|
|
}
|
|
} else {
|
|
/* Initialize buffer to silence */
|
|
for (int i = 0; i < length; i++) {
|
|
buffer[i] = 0;
|
|
}
|
|
|
|
/* Process each active drive */
|
|
for (int d = 0; d < active_drive_count; d++) {
|
|
hdd_audio_process_drive_int16(&drive_states[d], buffer, frames_in_buffer);
|
|
}
|
|
}
|
|
} |