diff --git a/src/86box.c b/src/86box.c index b2311f0e2..3d9356462 100644 --- a/src/86box.c +++ b/src/86box.c @@ -83,6 +83,7 @@ #include <86box/fdc.h> #include <86box/fdc_ext.h> #include <86box/hdd.h> +#include <86box/hdd_audio.h> #include <86box/hdc.h> #include <86box/hdc_ide.h> #include <86box/scsi.h> @@ -1476,6 +1477,8 @@ pc_init_modules(void) fdd_audio_load_profiles(); fdd_audio_init(); } + + hdd_audio_init(); sound_init(); @@ -1719,6 +1722,9 @@ pc_reset_hard_init(void) fdd_reset(); + /* Reset HDD audio to pick up any profile changes */ + hdd_audio_reset(); + /* Reset and reconfigure the SCSI layer. */ scsi_card_init(); diff --git a/src/config.c b/src/config.c index d5deded9d..6f8d1d6e6 100644 --- a/src/config.c +++ b/src/config.c @@ -51,6 +51,7 @@ #include <86box/lpt.h> #include <86box/serial.h> #include <86box/hdd.h> +#include <86box/hdd_audio.h> #include <86box/hdc.h> #include <86box/hdc_ide.h> #include <86box/fdd.h> @@ -1247,6 +1248,8 @@ load_hard_disks(void) uint32_t board = 0; uint32_t dev = 0; + hdd_audio_load_profiles(); + memset(temp, '\0', sizeof(temp)); for (uint8_t c = 0; c < HDD_NUM; c++) { sprintf(temp, "hdd_%02i_parameters", c + 1); @@ -1315,6 +1318,11 @@ load_hard_disks(void) p = ini_section_get_string(cat, temp, tmp2); hdd[c].speed_preset = hdd_preset_get_from_internal_name(p); + /* Audio Profile */ + sprintf(temp, "hdd_%02i_audio", c + 1); + p = ini_section_get_string(cat, temp, "none"); + hdd[c].audio_profile = hdd_audio_get_profile_by_internal_name(p); + /* MFM/RLL */ sprintf(temp, "hdd_%02i_mfm_channel", c + 1); if (hdd[c].bus_type == HDD_BUS_MFM) @@ -3458,6 +3466,17 @@ save_hard_disks(void) ini_section_delete_var(cat, temp); else ini_section_set_string(cat, temp, hdd_preset_get_internal_name(hdd[c].speed_preset)); + + sprintf(temp, "hdd_%02i_audio", c + 1); + if (!hdd_is_valid(c) || hdd[c].audio_profile == 0) { + ini_section_delete_var(cat, temp); + } else { + const char *internal_name = hdd_audio_get_profile_internal_name(hdd[c].audio_profile); + if (internal_name && strcmp(internal_name, "none") != 0) + ini_section_set_string(cat, temp, internal_name); + else + ini_section_delete_var(cat, temp); + } } ini_delete_section_if_empty(config, cat); diff --git a/src/disk/hdd.c b/src/disk/hdd.c index f6d769b9e..cd34b66fa 100644 --- a/src/disk/hdd.c +++ b/src/disk/hdd.c @@ -39,7 +39,6 @@ hdd_init(void) { /* Clear all global data. */ memset(hdd, 0x00, sizeof(hdd)); - hdd_audio_init(); return 0; } diff --git a/src/disk/hdd_audio.c b/src/disk/hdd_audio.c index 54419c244..bc3f9ed95 100644 --- a/src/disk/hdd_audio.c +++ b/src/disk/hdd_audio.c @@ -1,11 +1,33 @@ -#include +/* + * 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, + * + * Copyright 2026 Toni Riikonen. + */ + + #include #include #include +#include #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 */ #define HDD_MAX_SEEK_VOICES 8 @@ -16,36 +38,277 @@ typedef struct { float volume; } hdd_seek_voice_t; -static int16_t *hdd_spindle_sound_buffer = NULL; -static int16_t *hdd_seek_sound_buffer = NULL; -static int hdd_samples; -static int hdd_seek_samples; +/* Audio samples structure for a profile */ +typedef struct { + int16_t *spindle_start_buffer; + int spindle_start_samples; + int16_t *spindle_loop_buffer; + int spindle_loop_samples; + int16_t *spindle_stop_buffer; + int spindle_stop_samples; + int16_t *seek_buffer; + int seek_samples; + float spindle_volume; + 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]; + +/* Active profile for audio playback (first HDD with valid profile) */ +static int active_audio_profile = 0; static hdd_seek_voice_t hdd_seek_voices[HDD_MAX_SEEK_VOICES]; 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); + + /* 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; +} + +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_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) { + 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) { + 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; +} + void hdd_audio_init(void) { - hdd_spindle_sound_buffer = sound_load_wav( - "assets/sounds/hdd/1993 IBM H3171/1993_IBM_H3171_3.5_SPINDLE_RUNNING.wav", - &hdd_samples); - - if (hdd_spindle_sound_buffer) { - pclog("HDD Audio: Loaded spindle sound, %d frames\n", hdd_samples); - } else { - pclog("HDD Audio: Failed to load spindle sound!\n"); + /* Initialize profile samples */ + memset(profile_samples, 0, sizeof(profile_samples)); + + pclog("HDD Audio Init: audio_profile_count=%d\n", audio_profile_count); + + /* Find first HDD with a valid audio profile and load its samples */ + active_audio_profile = 0; + for (int i = 0; i < HDD_NUM; i++) { + if (hdd[i].bus_type != HDD_BUS_DISABLED) { + pclog("HDD Audio Init: HDD %d bus_type=%d audio_profile=%d\n", + i, hdd[i].bus_type, hdd[i].audio_profile); + if (hdd[i].audio_profile > 0) { + active_audio_profile = hdd[i].audio_profile; + pclog("HDD Audio: Using profile %d from HDD %d\n", active_audio_profile, i); + break; + } + } } - - hdd_seek_sound_buffer = sound_load_wav( - "assets/sounds/hdd/1993 IBM H3171/1993_IBM_H3171_3.5_SEEK_1TRACK.wav", - &hdd_seek_samples); - - if (hdd_seek_sound_buffer) { - pclog("HDD Audio: Loaded seek sound, %d frames (%.1f ms)\n", - hdd_seek_samples, (float)hdd_seek_samples / 48.0f); - } else { - pclog("HDD Audio: Failed to load seek sound!\n"); + + pclog("HDD Audio Init: active_audio_profile=%d\n", active_audio_profile); + + /* Load samples for the active profile */ + if (active_audio_profile > 0 && active_audio_profile < audio_profile_count) { + hdd_audio_load_profile_samples(active_audio_profile); } for (int i = 0; i < HDD_MAX_SEEK_VOICES; i++) { @@ -60,28 +323,87 @@ hdd_audio_init(void) } void -hdd_audio_seek(hard_disk_t *hdd, uint32_t new_cylinder) +hdd_audio_reset(void) { - uint32_t cylinder_diff = abs((int) hdd->cur_cylinder - (int) new_cylinder); + pclog("HDD Audio: Reset\n"); + + /* 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; + } + + /* Reset seek voices */ + for (int i = 0; i < HDD_MAX_SEEK_VOICES; i++) { + hdd_seek_voices[i].active = 0; + hdd_seek_voices[i].position = 0; + hdd_seek_voices[i].volume = 1.0f; + } + + /* Find new active profile from current HDD configuration */ + active_audio_profile = 0; + for (int i = 0; i < HDD_NUM; i++) { + if (hdd[i].bus_type != HDD_BUS_DISABLED) { + pclog("HDD Audio Reset: HDD %d audio_profile=%d\n", i, hdd[i].audio_profile); + if (hdd[i].audio_profile > 0) { + active_audio_profile = hdd[i].audio_profile; + pclog("HDD Audio: Reset with profile %d from HDD %d\n", active_audio_profile, i); + break; + } + } + } + + /* Load samples for the active profile */ + if (active_audio_profile > 0 && active_audio_profile < audio_profile_count) { + hdd_audio_load_profile_samples(active_audio_profile); + } +} + +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; - pclog("HDD Audio Seek: cur_cyl=%u -> new_cyl=%u, diff=%u, speed_preset=%d, cyl_switch_usec=%.1f\n", - hdd->cur_cylinder, new_cylinder, cylinder_diff, hdd->speed_preset, - hdd->cyl_switch_usec); - - if (hdd->speed_preset == 0) + /* Use the drive's audio profile, fallback to active profile */ + int profile_id = hdd_drive->audio_profile; + if (profile_id == 0) + profile_id = active_audio_profile; + + /* No audio profile selected */ + if (profile_id == 0 || profile_id >= audio_profile_count) return; - - if (!hdd_seek_sound_buffer || hdd_seek_samples == 0) { - pclog("HDD Audio Seek: No seek sound buffer loaded!\n"); + + /* 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; } int min_seek_spacing = 0; - if (hdd->cyl_switch_usec > 0) - min_seek_spacing = (int)(hdd->cyl_switch_usec * 48000.0 / 1000000.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); @@ -95,22 +417,16 @@ hdd_audio_seek(hard_disk_t *hdd, uint32_t new_cylinder) } } - int active_count = 0; - for (int i = 0; i < HDD_MAX_SEEK_VOICES; i++) { if (!hdd_seek_voices[i].active) { hdd_seek_voices[i].active = 1; hdd_seek_voices[i].position = 0; - hdd_seek_voices[i].volume = 0.5f; - pclog("HDD Audio Seek: Using free voice %d\n", i); + hdd_seek_voices[i].volume = samples->seek_volume; thread_release_mutex(hdd_audio_mutex); return; } - active_count++; } - pclog("HDD Audio Seek: All %d voices active, skipping seek\n", active_count); - thread_release_mutex(hdd_audio_mutex); } @@ -118,23 +434,28 @@ void hdd_audio_callback(int16_t *buffer, int length) { static int spindle_pos = 0; - static int debug_counter = 0; - const float spindle_volume = 0.2f; - const float seek_volume = 0.5f; int frames_in_buffer = length / 2; + /* Get active profile samples */ + hdd_audio_samples_t *samples = NULL; + if (active_audio_profile > 0 && active_audio_profile < HDD_AUDIO_PROFILE_MAX) { + samples = &profile_samples[active_audio_profile]; + } + if (sound_is_float) { float *float_buffer = (float *) buffer; - if (hdd_spindle_sound_buffer && hdd_samples > 0) { + /* Spindle sound from profile */ + if (samples && samples->spindle_loop_buffer && samples->spindle_loop_samples > 0) { + float spindle_volume = samples->spindle_volume; for (int i = 0; i < frames_in_buffer; i++) { - float left_sample = (float) hdd_spindle_sound_buffer[spindle_pos * 2] / 131072.0f * spindle_volume; - float right_sample = (float) hdd_spindle_sound_buffer[spindle_pos * 2 + 1] / 131072.0f * spindle_volume; + float left_sample = (float) samples->spindle_loop_buffer[spindle_pos * 2] / 131072.0f * spindle_volume; + float right_sample = (float) samples->spindle_loop_buffer[spindle_pos * 2 + 1] / 131072.0f * spindle_volume; float_buffer[i * 2] = left_sample; float_buffer[i * 2 + 1] = right_sample; spindle_pos++; - if (spindle_pos >= hdd_samples) { + if (spindle_pos >= samples->spindle_loop_samples) { spindle_pos = 0; } } @@ -144,7 +465,8 @@ hdd_audio_callback(int16_t *buffer, int length) } } - if (hdd_seek_sound_buffer && hdd_seek_samples > 0 && hdd_audio_mutex) { + /* Seek sounds from profile */ + if (samples && samples->seek_buffer && samples->seek_samples > 0 && hdd_audio_mutex) { thread_wait_mutex(hdd_audio_mutex); for (int v = 0; v < HDD_MAX_SEEK_VOICES; v++) { @@ -155,15 +477,15 @@ hdd_audio_callback(int16_t *buffer, int length) int pos = hdd_seek_voices[v].position; if (pos < 0) pos = 0; - for (int i = 0; i < frames_in_buffer && pos < hdd_seek_samples; i++, pos++) { - float seek_left = (float) hdd_seek_sound_buffer[pos * 2] / 131072.0f * seek_volume * voice_vol; - float seek_right = (float) hdd_seek_sound_buffer[pos * 2 + 1] / 131072.0f * seek_volume * voice_vol; + for (int i = 0; i < frames_in_buffer && pos < samples->seek_samples; i++, pos++) { + float seek_left = (float) samples->seek_buffer[pos * 2] / 131072.0f * voice_vol; + float seek_right = (float) 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 >= hdd_seek_samples) { + if (pos >= samples->seek_samples) { hdd_seek_voices[v].active = 0; hdd_seek_voices[v].position = 0; } else { @@ -173,19 +495,16 @@ hdd_audio_callback(int16_t *buffer, int length) thread_release_mutex(hdd_audio_mutex); } - - if (debug_counter++ % 100 == 0) { - pclog("HDD Audio: float_buffer[0]=%.6f, float_buffer[1]=%.6f, spindle_pos=%d, frames=%d\n", - float_buffer[0], float_buffer[1], spindle_pos, frames_in_buffer); - } } else { - if (hdd_spindle_sound_buffer && hdd_samples > 0) { + /* Spindle sound from profile */ + if (samples && samples->spindle_loop_buffer && samples->spindle_loop_samples > 0) { + float spindle_volume = samples->spindle_volume; for (int i = 0; i < frames_in_buffer; i++) { - buffer[i * 2] = hdd_spindle_sound_buffer[spindle_pos * 2]; - buffer[i * 2 + 1] = hdd_spindle_sound_buffer[spindle_pos * 2 + 1]; + buffer[i * 2] = (int16_t)(samples->spindle_loop_buffer[spindle_pos * 2] * spindle_volume); + buffer[i * 2 + 1] = (int16_t)(samples->spindle_loop_buffer[spindle_pos * 2 + 1] * spindle_volume); spindle_pos++; - if (spindle_pos >= hdd_samples) { + if (spindle_pos >= samples->spindle_loop_samples) { spindle_pos = 0; } } @@ -195,19 +514,21 @@ hdd_audio_callback(int16_t *buffer, int length) } } - if (hdd_seek_sound_buffer && hdd_seek_samples > 0 && hdd_audio_mutex) { + /* Seek sounds from profile */ + if (samples && samples->seek_buffer && samples->seek_samples > 0 && hdd_audio_mutex) { thread_wait_mutex(hdd_audio_mutex); for (int v = 0; v < HDD_MAX_SEEK_VOICES; v++) { if (!hdd_seek_voices[v].active) continue; + float voice_vol = hdd_seek_voices[v].volume; int pos = hdd_seek_voices[v].position; if (pos < 0) pos = 0; - for (int i = 0; i < frames_in_buffer && pos < hdd_seek_samples; i++, pos++) { - int32_t left = buffer[i * 2] + hdd_seek_sound_buffer[pos * 2] / 2; - int32_t right = buffer[i * 2 + 1] + hdd_seek_sound_buffer[pos * 2 + 1] / 2; + for (int i = 0; i < frames_in_buffer && pos < samples->seek_samples; i++, pos++) { + int32_t left = buffer[i * 2] + (int32_t)(samples->seek_buffer[pos * 2] * voice_vol); + int32_t right = buffer[i * 2 + 1] + (int32_t)(samples->seek_buffer[pos * 2 + 1] * voice_vol); if (left > 32767) left = 32767; if (left < -32768) left = -32768; @@ -218,7 +539,7 @@ hdd_audio_callback(int16_t *buffer, int length) buffer[i * 2 + 1] = (int16_t) right; } - if (pos >= hdd_seek_samples) { + if (pos >= samples->seek_samples) { hdd_seek_voices[v].active = 0; hdd_seek_voices[v].position = 0; } else { diff --git a/src/include/86box/hdd.h b/src/include/86box/hdd.h index 88855552b..ddf8fdbe6 100644 --- a/src/include/86box/hdd.h +++ b/src/include/86box/hdd.h @@ -172,6 +172,7 @@ typedef struct hard_disk_t { uint32_t hpc; uint32_t tracks; uint32_t speed_preset; + uint32_t audio_profile; uint32_t num_zones; uint32_t phy_cyl; diff --git a/src/include/86box/hdd_audio.h b/src/include/86box/hdd_audio.h index 1e6d7b103..050bff0a0 100644 --- a/src/include/86box/hdd_audio.h +++ b/src/include/86box/hdd_audio.h @@ -1,6 +1,63 @@ +/* + * 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. + * + * Definitions for the hard disk audio emulation. + * + * Authors: Toni Riikonen, + * + * Copyright 2026 Toni Riikonen. + */ +#ifndef EMU_HDD_AUDIO_H +#define EMU_HDD_AUDIO_H + #include #include <86box/hdd.h> +#ifdef __cplusplus +extern "C" { +#endif + +#define HDD_AUDIO_PROFILE_MAX 64 + +/* Audio sample configuration structure */ +typedef struct { + char filename[512]; + float volume; +} hdd_audio_sample_config_t; + +/* HDD audio profile configuration */ +typedef struct { + int id; + char name[128]; + char internal_name[64]; + hdd_audio_sample_config_t spindlemotor_start; + hdd_audio_sample_config_t spindlemotor_loop; + hdd_audio_sample_config_t spindlemotor_stop; + hdd_audio_sample_config_t seek_track; +} hdd_audio_profile_config_t; + +/* Functions for profile management */ +extern void hdd_audio_load_profiles(void); +extern int hdd_audio_get_profile_count(void); +extern const hdd_audio_profile_config_t *hdd_audio_get_profile(int id); +extern const char *hdd_audio_get_profile_name(int id); +extern const char *hdd_audio_get_profile_internal_name(int id); +extern int hdd_audio_get_profile_by_internal_name(const char *internal_name); + +/* HDD audio initialization and cleanup */ extern void hdd_audio_init(void); +extern void hdd_audio_reset(void); +extern void hdd_audio_close(void); extern void hdd_audio_callback(int16_t *buffer, int length); -extern void hdd_audio_seek(hard_disk_t *hdd, uint32_t new_cylinder); \ No newline at end of file +extern void hdd_audio_seek(hard_disk_t *hdd, uint32_t new_cylinder); + +#ifdef __cplusplus +} +#endif + +#endif /* EMU_HDD_AUDIO_H */ \ No newline at end of file diff --git a/src/qt/qt_settingsharddisks.cpp b/src/qt/qt_settingsharddisks.cpp index 0f12c8e6f..7939cb989 100644 --- a/src/qt/qt_settingsharddisks.cpp +++ b/src/qt/qt_settingsharddisks.cpp @@ -20,6 +20,7 @@ extern "C" { #include <86box/86box.h> #include <86box/hdd.h> +#include <86box/hdd_audio.h> } #include @@ -36,6 +37,7 @@ const int ColumnHeads = 3; const int ColumnSectors = 4; const int ColumnSize = 5; const int ColumnSpeed = 6; +const int ColumnAudio = 7; const int DataBus = Qt::UserRole; const int DataBusChannel = Qt::UserRole + 1; @@ -103,6 +105,11 @@ addRow(QAbstractItemModel *model, hard_disk_t *hd) auto speedIndex = model->index(row, ColumnSpeed); model->setData(speedIndex, QObject::tr(hdd_preset_getname(hd->speed_preset))); model->setData(speedIndex, hd->speed_preset, Qt::UserRole); + + auto audioIndex = model->index(row, ColumnAudio); + const char *audioName = hdd_audio_get_profile_name(hd->audio_profile); + model->setData(audioIndex, audioName ? QObject::tr(audioName) : QObject::tr("None")); + model->setData(audioIndex, hd->audio_profile, Qt::UserRole); } SettingsHarddisks::SettingsHarddisks(QWidget *parent) @@ -113,7 +120,7 @@ SettingsHarddisks::SettingsHarddisks(QWidget *parent) hard_disk_icon = QIcon(":/settings/qt/icons/hard_disk.ico"); - QAbstractItemModel *model = new QStandardItemModel(0, 7, this); + QAbstractItemModel *model = new QStandardItemModel(0, 8, this); model->setHeaderData(ColumnBus, Qt::Horizontal, tr("Bus")); model->setHeaderData(ColumnFilename, Qt::Horizontal, tr("File")); model->setHeaderData(ColumnCylinders, Qt::Horizontal, tr("C")); @@ -121,6 +128,7 @@ SettingsHarddisks::SettingsHarddisks(QWidget *parent) model->setHeaderData(ColumnSectors, Qt::Horizontal, tr("S")); model->setHeaderData(ColumnSize, Qt::Horizontal, tr("MiB")); model->setHeaderData(ColumnSpeed, Qt::Horizontal, tr("Model")); + model->setHeaderData(ColumnAudio, Qt::Horizontal, tr("Audio")); ui->tableView->setModel(model); for (int i = 0; i < HDD_NUM; i++) { @@ -139,6 +147,16 @@ SettingsHarddisks::SettingsHarddisks(QWidget *parent) onTableRowChanged(QModelIndex()); Harddrives::populateBuses(ui->comboBoxBus->model()); + + /* Populate audio profile combobox */ + int profile_count = hdd_audio_get_profile_count(); + for (int i = 0; i < profile_count; i++) { + const char *name = hdd_audio_get_profile_name(i); + if (name) { + ui->comboBoxAudio->addItem(name, i); + } + } + on_comboBoxBus_currentIndexChanged(0); } @@ -162,6 +180,7 @@ SettingsHarddisks::save() hdd[i].hpc = idx.siblingAtColumn(ColumnHeads).data().toUInt(); hdd[i].spt = idx.siblingAtColumn(ColumnSectors).data().toUInt(); hdd[i].speed_preset = idx.siblingAtColumn(ColumnSpeed).data(Qt::UserRole).toUInt(); + hdd[i].audio_profile = idx.siblingAtColumn(ColumnAudio).data(Qt::UserRole).toUInt(); QByteArray fileName = idx.siblingAtColumn(ColumnFilename).data(Qt::UserRole).toString().toUtf8(); strncpy(hdd[i].fn, fileName.data(), sizeof(hdd[i].fn) - 1); @@ -272,6 +291,24 @@ SettingsHarddisks::on_comboBoxSpeed_currentIndexChanged(int index) } } +void +SettingsHarddisks::on_comboBoxAudio_currentIndexChanged(int index) +{ + if (index < 0) + return; + + auto idx = ui->tableView->selectionModel()->currentIndex(); + if (idx.isValid()) { + auto *model = ui->tableView->model(); + auto col = idx.siblingAtColumn(ColumnAudio); + int prof = ui->comboBoxAudio->currentData(Qt::UserRole).toInt(); + model->setData(col, prof, Qt::UserRole); + + const char *audioName = hdd_audio_get_profile_name(prof); + model->setData(col, audioName ? QObject::tr(audioName) : QObject::tr("None")); + } +} + void SettingsHarddisks::onTableRowChanged(const QModelIndex ¤t) { @@ -279,13 +316,16 @@ SettingsHarddisks::onTableRowChanged(const QModelIndex ¤t) ui->labelBus->setHidden(hidden); ui->labelChannel->setHidden(hidden); ui->labelSpeed->setHidden(hidden); + ui->labelAudio->setHidden(hidden); ui->comboBoxBus->setHidden(hidden); ui->comboBoxChannel->setHidden(hidden); ui->comboBoxSpeed->setHidden(hidden); + ui->comboBoxAudio->setHidden(hidden); uint32_t bus = current.siblingAtColumn(ColumnBus).data(DataBus).toUInt(); uint32_t busChannel = current.siblingAtColumn(ColumnBus).data(DataBusChannel).toUInt(); uint32_t speed = current.siblingAtColumn(ColumnSpeed).data(Qt::UserRole).toUInt(); + uint32_t audio = current.siblingAtColumn(ColumnAudio).data(Qt::UserRole).toUInt(); auto *model = ui->comboBoxBus->model(); auto match = model->match(model->index(0, 0), Qt::UserRole, bus); @@ -302,6 +342,11 @@ SettingsHarddisks::onTableRowChanged(const QModelIndex ¤t) if (!match.isEmpty()) ui->comboBoxSpeed->setCurrentIndex(match.first().row()); + model = ui->comboBoxAudio->model(); + match = model->match(model->index(0, 0), Qt::UserRole, audio); + if (!match.isEmpty()) + ui->comboBoxAudio->setCurrentIndex(match.first().row()); + reloadBusChannels(); } diff --git a/src/qt/qt_settingsharddisks.hpp b/src/qt/qt_settingsharddisks.hpp index f892a79cd..b8d298bb4 100644 --- a/src/qt/qt_settingsharddisks.hpp +++ b/src/qt/qt_settingsharddisks.hpp @@ -24,6 +24,7 @@ private slots: void on_comboBoxBus_currentIndexChanged(int index); void on_comboBoxChannel_currentIndexChanged(int index); void on_comboBoxSpeed_currentIndexChanged(int index); + void on_comboBoxAudio_currentIndexChanged(int index); void on_pushButtonNew_clicked(); void on_pushButtonExisting_clicked(); diff --git a/src/qt/qt_settingsharddisks.ui b/src/qt/qt_settingsharddisks.ui index d65b1d0c1..a6f134bdc 100644 --- a/src/qt/qt_settingsharddisks.ui +++ b/src/qt/qt_settingsharddisks.ui @@ -90,6 +90,20 @@ + + + + Audio: + + + + + + + 30 + + +