Merge pull request #140 from WizzardSK/floppy-led-sound

libretro: drive LEDs and floppy drive sound, from the hatari2014 core
This commit is contained in:
Craig authored and GitHub committed 2026-10-06 20:45:53 +01:00
commit 3e8bfc297f
8 files changed
+496 -1

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
include_directories(. ../.. ../includes ../debug ../falcon libretro-common/include)
add_library(UiRetro OBJECT audio.c disk_control.c harddisk.c joy_ui.c gui_event.c keymap.c
add_library(UiRetro OBJECT audio.c disk_control.c floppy_sound.c harddisk.c joy_ui.c gui_event.c keymap.c
main_retro.c microphone.c options.c screen.c statusbar.c timing.c vfs.c)
target_include_directories(UiRetro PRIVATE ${LIBRETRO_INCLUDE_DIR})
target_compile_definitions(UiRetro PRIVATE LIBRETRO)
+7
View File
@@ -18,6 +18,7 @@ const char Audio_fileid[] = "Hatari audio.c";
#include "dmaSnd.h"
#include "crossbar.h"
#include "video.h"
#include "floppy_sound.h"
bool bSoundWorking = false; /* Is sound OK */
@@ -80,12 +81,18 @@ void Audio_Unlock(void)
{
if (AudioMixBuffer_pos_read + nGeneratedSamples <= AUDIOMIXBUFFER_SIZE)
{
FloppySound_Mix(&AudioMixBuffer[AudioMixBuffer_pos_read][0],
nGeneratedSamples, nAudioFrequency);
audio_sample_batch_cb(&AudioMixBuffer[AudioMixBuffer_pos_read][0],
nGeneratedSamples);
}
else
{
int samples_at_end = AUDIOMIXBUFFER_SIZE - AudioMixBuffer_pos_read;
FloppySound_Mix(&AudioMixBuffer[AudioMixBuffer_pos_read][0],
samples_at_end, nAudioFrequency);
FloppySound_Mix(&AudioMixBuffer[0][0],
nGeneratedSamples - samples_at_end, nAudioFrequency);
audio_sample_batch_cb(&AudioMixBuffer[AudioMixBuffer_pos_read][0],
samples_at_end);
audio_sample_batch_cb(&AudioMixBuffer[0][0],
+256
View File
@@ -0,0 +1,256 @@
/*
Hatari - floppy_sound.c
This file is distributed under the GNU General Public License, version 2
or at your option any later version. Read the file gpl.txt for details.
Floppy drive sounds for the libretro core, from the hatari2014 core
(libretro/floppy_sound.c there): a click and seek sound when the led of
drive A or B comes on, repeated for as long as the floppy controller keeps
working on the drive, for both at the same time.
A sample of one's own can be put in the frontend's system directory as
floppy.raw: signed 16-bit little-endian stereo at 44100 Hz, no header
(ffmpeg -i click.wav -f s16le -ar 44100 -ac 2 floppy.raw). Without it a
synthetic one is used. Both are made to fit the core's audio rate, which
can change at run time.
*/
const char FloppySound_fileid[] = "Hatari floppy_sound.c";
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "main.h"
#include "main_retro.h"
#include "floppy_sound.h"
#include "vfs.h"
#define RAW_RATE 44100
static int16_t *raw_pcm; /* floppy.raw as loaded, at RAW_RATE */
static int raw_frames;
static int16_t *sample; /* what is mixed, at sample_rate */
static int sample_frames;
static int sample_rate;
static int play_pos[2] = { -1, -1 };
static bool enabled = true;
static int volume = 192; /* 0-256 */
/**
* The built-in sound, ~55 ms: a low mechanical thud, the stepper motor's
* buzz, a descending head seek chirp and a short chassis rattle.
*/
static int16_t *FloppySound_Synthesize(int rate, int *frames)
{
const float pi = 3.14159265f;
int n = rate * 55 / 1000;
int16_t *pcm = malloc(n * 2 * sizeof(*pcm));
int i;
if (!pcm)
return NULL;
for (i = 0; i < n; i++)
{
float t = (float)i / (float)rate;
float s = 0.0f;
/* thud: 120 Hz, decays in ~12 ms */
s += sinf(2.0f * pi * 120.0f * t) * expf(-t / 0.012f) * 0.55f;
/* stepper buzz: ~800 Hz with a wobble, 2 ms onset */
{
float onset = t > 0.002f ? 1.0f : t / 0.002f;
float freq = 800.0f + 60.0f * sinf(2.0f * pi * 180.0f * t);
s += sinf(2.0f * pi * freq * t) * onset * expf(-t / 0.008f) * 0.30f;
}
/* head seek chirp: 2400 -> 700 Hz over 25 ms */
if (t < 0.025f)
{
float freq = 2400.0f - 1700.0f * (t / 0.025f);
s += sinf(2.0f * pi * freq * t) * expf(-t / 0.018f) * 0.20f;
}
/* rattle: products of unrelated frequencies, 5-17 ms */
{
float t2 = t - 0.005f;
if (t2 > 0.0f && t2 < 0.012f)
{
float noise = sinf(2.0f * pi * 3700.0f * t2)
* sinf(2.0f * pi * 2300.0f * t2)
* sinf(2.0f * pi * 970.0f * t2);
s += noise * expf(-t2 / 0.005f) * 0.18f;
}
}
if (s > 1.0f)
s = 1.0f;
if (s < -1.0f)
s = -1.0f;
pcm[i * 2] = pcm[i * 2 + 1] = (int16_t)(s * 28000.0f);
}
*frames = n;
return pcm;
}
/**
* floppy.raw at another rate, linearly interpolated
*/
static int16_t *FloppySound_Resample(int rate, int *frames)
{
int n = (int)((int64_t)raw_frames * rate / RAW_RATE);
int16_t *pcm;
int i, c;
if (n <= 0 || !(pcm = malloc(n * 2 * sizeof(*pcm))))
return NULL;
for (i = 0; i < n; i++)
{
int64_t pos = (int64_t)i * RAW_RATE * 256 / rate;
int idx = (int)(pos >> 8), frac = (int)(pos & 255);
int next = idx + 1 < raw_frames ? idx + 1 : idx;
for (c = 0; c < 2; c++)
pcm[i * 2 + c] = (int16_t)((raw_pcm[idx * 2 + c] * (256 - frac) +
raw_pcm[next * 2 + c] * frac) >> 8);
}
*frames = n;
return pcm;
}
static void FloppySound_Prepare(int rate)
{
free(sample);
sample = NULL;
sample_frames = 0;
sample_rate = rate;
if (raw_pcm)
sample = FloppySound_Resample(rate, &sample_frames);
if (!sample)
sample = FloppySound_Synthesize(rate, &sample_frames);
play_pos[0] = play_pos[1] = -1;
}
void FloppySound_Init(const char *system_dir)
{
char path[FILENAME_MAX];
VFS_FILE f;
int64_t size;
FloppySound_UnInit();
if (!system_dir || !system_dir[0])
return;
snprintf(path, sizeof(path), "%s%sfloppy.raw", system_dir, RETRO_PATH_SEPARATOR);
f = VFS_fopen(path, "rb");
if (!f)
return;
size = VFS_fsize(f);
if (size >= 4 && size <= 16 * 1024 * 1024 && (raw_pcm = malloc(size)) != NULL)
{
if (VFS_fread(raw_pcm, 1, size, f) == (size_t)size)
{
raw_frames = (int)(size / 4);
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
{
int i;
for (i = 0; i < raw_frames * 2; i++)
{
uint16_t v = (uint16_t)raw_pcm[i];
raw_pcm[i] = (int16_t)((v >> 8) | (v << 8));
}
}
#endif
}
else
{
free(raw_pcm);
raw_pcm = NULL;
}
}
VFS_fclose(f);
}
void FloppySound_UnInit(void)
{
free(raw_pcm);
raw_pcm = NULL;
raw_frames = 0;
free(sample);
sample = NULL;
sample_frames = 0;
sample_rate = 0;
play_pos[0] = play_pos[1] = -1;
}
void FloppySound_SetEnabled(bool on)
{
enabled = on;
if (!on)
play_pos[0] = play_pos[1] = -1;
}
void FloppySound_SetVolume(int percent)
{
volume = percent * 256 / 100;
}
void FloppySound_Trigger(int drive)
{
if (enabled && (drive == 0 || drive == 1))
play_pos[drive] = 0;
}
bool FloppySound_Playing(int drive)
{
return (drive == 0 || drive == 1) && play_pos[drive] >= 0;
}
void FloppySound_Mix(int16_t *buf, int frames, int rate)
{
int d, i;
if (!enabled || rate <= 0 || (play_pos[0] < 0 && play_pos[1] < 0))
return;
if (rate != sample_rate)
{
int pos[2] = { play_pos[0], play_pos[1] };
FloppySound_Prepare(rate);
play_pos[0] = pos[0] >= 0 ? 0 : -1;
play_pos[1] = pos[1] >= 0 ? 0 : -1;
}
if (!sample)
return;
for (d = 0; d < 2; d++)
{
if (play_pos[d] < 0)
continue;
for (i = 0; i < frames && play_pos[d] < sample_frames; i++, play_pos[d]++)
{
int l = buf[i * 2] + ((sample[play_pos[d] * 2] * volume) >> 8);
int r = buf[i * 2 + 1] + ((sample[play_pos[d] * 2 + 1] * volume) >> 8);
buf[i * 2] = l > 32767 ? 32767 : l < -32768 ? -32768 : l;
buf[i * 2 + 1] = r > 32767 ? 32767 : r < -32768 ? -32768 : r;
}
if (play_pos[d] >= sample_frames)
play_pos[d] = -1;
}
}
+30
View File
@@ -0,0 +1,30 @@
/*
Hatari - floppy_sound.h
This file is distributed under the GNU General Public License, version 2
or at your option any later version. Read the file gpl.txt for details.
*/
#ifndef HATARI_RETRO_FLOPPY_SOUND_H
#define HATARI_RETRO_FLOPPY_SOUND_H
#include <stdbool.h>
#include <stdint.h>
/* Load floppy.raw from the system directory, if there is one */
void FloppySound_Init(const char *system_dir);
void FloppySound_UnInit(void);
void FloppySound_SetEnabled(bool enabled);
void FloppySound_SetVolume(int percent);
/* Start the drive sound for drive 0 (A) or 1 (B) */
void FloppySound_Trigger(int drive);
/* Whether the sound for drive 0 (A) or 1 (B) is still playing */
bool FloppySound_Playing(int drive);
/* Mix the playing drive sounds into 'frames' stereo frames at 'rate' Hz */
void FloppySound_Mix(int16_t *buf, int frames, int rate);
#endif /* ifndef HATARI_RETRO_FLOPPY_SOUND_H */
+34
View File
@@ -13,6 +13,7 @@
#include "disk_control.h"
#include "file.h"
#include "floppy.h"
#include "floppy_sound.h"
#include "harddisk.h"
#include "memorySnapShot.h"
#include "m68000.h"
@@ -20,6 +21,7 @@
#include "reset.h"
#include "screen.h"
#include "sound.h"
#include "statusbar.h"
#include "tos.h"
#include "vdi.h"
#include "version.h"
@@ -179,6 +181,7 @@ RETRO_API void retro_init(void)
if (environment_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &system_dir) && system_dir)
{
retro_system_directory = system_dir;
FloppySound_Init(system_dir);
snprintf(tos_path, sizeof(tos_path), "%s%s%s",
system_dir, RETRO_PATH_SEPARATOR, "tos.img");
@@ -210,6 +213,7 @@ RETRO_API void retro_deinit(void)
{
scratch_path[0] = '\0';
Main_UnInit();
FloppySound_UnInit();
}
RETRO_API void retro_get_system_info(struct retro_system_info *info)
@@ -247,6 +251,34 @@ RETRO_API void retro_reset(void)
Reset_Warm();
}
/**
* The frame with the drive leds drawn over it, in a
* copy: the emulator's own buffer is not drawn anew every frame where the
* picture stays the same.
*/
static uint32_t *Core_DrawOverlays(uint32_t *pixels, int width, int height, int pitch)
{
static uint32_t *overlay;
static size_t overlay_size;
size_t size = (size_t)pitch * height;
if (!pixels || !Statusbar_LedsVisible())
return pixels;
if (size > overlay_size)
{
uint32_t *bigger = realloc(overlay, size);
if (!bigger)
return pixels;
overlay = bigger;
overlay_size = size;
}
memcpy(overlay, pixels, size);
Statusbar_DrawLeds(overlay, width, height, pitch);
return overlay;
}
RETRO_API void retro_run(void)
{
bool options_updated = false;
@@ -282,7 +314,9 @@ RETRO_API void retro_run(void)
}
Screen_GetDimension(&pixels, &width, &height, &pitch);
pixels = Core_DrawOverlays(pixels, width, height, pitch);
video_refresh_cb(pixels, width, height, pitch);
Statusbar_FrameDone();
}
RETRO_API void retro_set_controller_port_device(unsigned port, unsigned device)
+6
View File
@@ -21,3 +21,9 @@ extern retro_video_refresh_t video_refresh_cb;
extern retro_input_state_t input_state_cb;
extern void Core_RefreshRateChanged(void);
extern void Retro_Log(int nType, const char *msg);
/* statusbar.c: the drive leds, drawn over the frame */
void Statusbar_ShowLeds(bool show);
bool Statusbar_LedsVisible(void);
void Statusbar_DrawLeds(uint32_t *pixels, int width, int height, int pitch);
void Statusbar_FrameDone(void);
+49
View File
@@ -9,6 +9,7 @@
#include "configuration.h"
#include "fdc.h"
#include "floppy_sound.h"
#include "main_retro.h"
#include "options.h"
#include "reset.h"
@@ -350,6 +351,50 @@ static struct retro_core_option_v2_definition option_defs_us[] = {
},
"off"
},
{
"hatari_drive_leds",
"Drive LEDs",
"Drive LEDs",
"Show drive activity in the top right corner of the screen: an amber LED per floppy drive, as on an STFM, and a green one for the hard disk.",
NULL,
"floppy",
{
{ "enabled", NULL },
{ "disabled", NULL },
{ NULL, NULL },
},
"enabled"
},
{
"hatari_floppy_sound",
"Floppy Drive Sound",
"Floppy Drive Sound",
"Play a mechanical floppy drive sound when a drive is accessed. For a sound of your own, put floppy.raw (signed 16-bit stereo at 44100 Hz, no header) in the frontend's system directory.",
NULL,
"floppy",
{
{ "enabled", NULL },
{ "disabled", NULL },
{ NULL, NULL },
},
"enabled"
},
{
"hatari_floppy_sound_volume",
"Floppy Drive Sound Volume",
"Floppy Drive Sound Volume",
"Volume of the floppy drive sound.",
NULL,
"floppy",
{
{ "25", "25%" },
{ "50", "50%" },
{ "75", "75%" },
{ "100", "100%" },
{ NULL, NULL },
},
"75"
},
{
"hatari_drive_a_enable",
"Drive A Enabled",
@@ -817,6 +862,10 @@ void Core_ApplyRuntimeOptions(void)
FDC_Drive_Set_Enable(1, ConfigureParams.DiskImage.EnableDriveB);
FDC_Drive_Set_NumberOfHeads(0, ConfigureParams.DiskImage.DriveA_NumberOfHeads);
FDC_Drive_Set_NumberOfHeads(1, ConfigureParams.DiskImage.DriveB_NumberOfHeads);
Statusbar_ShowLeds(Core_VarBool("hatari_drive_leds", true));
FloppySound_SetEnabled(Core_VarBool("hatari_floppy_sound", true));
FloppySound_SetVolume(Core_VarInt("hatari_floppy_sound_volume", 75));
/* --- Atari Screen --- */
str = Core_VarStr("hatari_monitor_type", "rgb");
+113
View File
@@ -5,6 +5,11 @@
or at your option any later version. Read the file gpl.txt for details.
Code to draw statusbar area, floppy leds etc.
The libretro core has no statusbar area: the drive LEDs are drawn over the
top right corner of the frame instead, a small amber bar per floppy drive
like the one on an STFM, and a green one for the hard disk (from the
hatari2014 core). A floppy LED going on also starts the drive sound.
*/
const char Statusbar_fileid[] = "Hatari statusbar.c";
@@ -23,8 +28,17 @@ const char Statusbar_fileid[] = "Hatari statusbar.c";
#include "blitter.h"
#include "str.h"
#include "lilo.h"
#include "floppy_sound.h"
#include "main_retro.h"
#define HD_LED_FRAMES 10 /* frames the HD led stays on */
static bool leds_shown = true;
static drive_led_t floppy_led[2];
static bool floppy_busy[2]; /* the controller has worked on the drive this frame */
static int hd_led_frames;
/**
* Return statusbar height for given width and height
*/
@@ -59,6 +73,8 @@ int Statusbar_GetHeight(void)
*/
void Statusbar_EnableHDLed(drive_led_t state)
{
if (state != LED_STATE_OFF)
hd_led_frames = HD_LED_FRAMES;
}
/**
@@ -68,8 +84,105 @@ void Statusbar_EnableHDLed(drive_led_t state)
void Statusbar_SetFloppyLed(drive_index_t drive, drive_led_t state)
{
assert(drive == DRIVE_LED_A || drive == DRIVE_LED_B);
if (state == LED_STATE_ON_BUSY)
floppy_busy[drive] = true;
floppy_led[drive] = state;
}
/**
* Show or hide the drive leds drawn over the frame
*/
void Statusbar_ShowLeds(bool show)
{
leds_shown = show;
}
/**
* Whether any drive led is to be drawn over this frame
*/
bool Statusbar_LedsVisible(void)
{
return leds_shown && (floppy_led[0] != LED_STATE_OFF ||
floppy_led[1] != LED_STATE_OFF || hd_led_frames > 0);
}
/**
* Once per retro_run: what times the HD led out, and what starts the floppy
* sound. Drive leds are looked at once a frame, as the hatari2014 core did:
* the sound starts when a led has come on since the last frame, and plays
* again each time it has finished for as long as the controller has worked
* on the drive during the frame - so a load sounds for as long as it lasts,
* and the sample is never cut off to start it over. TOS selecting drive A
* and B in turn every eight VBLs to check for a disk change is over within
* the frame and carries out no command, so it is not heard.
*/
void Statusbar_FrameDone(void)
{
static bool was_on[2];
int d;
for (d = 0; d < 2; d++)
{
bool on = floppy_led[d] != LED_STATE_OFF;
bool busy = floppy_busy[d] || floppy_led[d] == LED_STATE_ON_BUSY;
if ((on && !was_on[d]) || (busy && !FloppySound_Playing(d)))
FloppySound_Trigger(d);
was_on[d] = on;
floppy_busy[d] = false;
}
if (hd_led_frames > 0)
hd_led_frames--;
}
/**
* Draw the drive leds into a copy of the frame about to be handed to the
* frontend
*/
void Statusbar_DrawLeds(uint32_t *pixels, int width, int height, int pitch)
{
static const uint32_t floppy_color = 0xF8D800; /* STFM amber */
static const uint32_t hd_color = 0x30E030;
bool on[3];
int led_w, led_h, gap, right, top, i, x, y;
int stride = pitch / 4;
if (!Statusbar_LedsVisible() || !pixels)
return;
on[0] = floppy_led[0] != LED_STATE_OFF;
on[1] = floppy_led[1] != LED_STATE_OFF;
on[2] = hd_led_frames > 0;
/* The hatari2014 core's 13x5 bar in a 384 pixel wide frame, scaled */
led_w = width * 13 / 384;
led_h = height * 5 / 276;
if (led_w < 7)
led_w = 7;
if (led_w > 64)
led_w = 64;
if (led_h < 3)
led_h = 3;
if (led_h > 16)
led_h = 16;
gap = led_w / 2;
right = width - led_w / 2;
top = led_h;
if (right - 3 * led_w - 2 * gap < 0 || top + led_h > height)
return;
/* A rightmost, then B, then the hard disk */
for (i = 0; i < 3; i++)
{
int left = right - (i + 1) * led_w - i * gap;
if (!on[i])
continue;
for (y = top; y < top + led_h; y++)
for (x = left; x < left + led_w; x++)
pixels[y * stride + x] = i < 2 ? floppy_color : hd_color;
}
}
/**
* Set TOS etc information and initial help message