diff foo_out_sdl.cc @ 1:20d02a178406 default tip

*: check in everything else yay
author Paper <paper@tflc.us>
date Mon, 05 Jan 2026 02:15:46 -0500
parents e9bb126753e7
children
line wrap: on
line diff
--- a/foo_out_sdl.cc	Sat Jan 03 23:52:56 2026 -0500
+++ b/foo_out_sdl.cc	Mon Jan 05 02:15:46 2026 -0500
@@ -1,7 +1,15 @@
-/* SDL output for foobar2000.
- * Somewhat scuffed since SDL does not provide latency */
+#include <algorithm>
+#include <cstdlib>
+#include <cstdio>
+#include <cstdarg>
+#include <cmath>
+#include <atomic>
 
-#include <SDL3/SDL.h>
+#include "SDL3/SDL.h"
+
+#include "foobar2000/helpers/foobar2000+atl.h"
+#include "foobar2000/SDK/core_api.h"
+#include "foobar2000/SDK/output.h"
 
 #if audio_sample_size == 32
 # define OUTSDL_USE_NATIVE_F32 1
@@ -10,9 +18,56 @@
 # define OUTSDL_FORMAT SDL_AUDIO_S32
 #endif
 
-class OutputSDL : public output {
+/* ----------------------------------------------------------------------------------------- */
+/* logf: printf-like interface for printing into the console
+ * a.k.a. "paper hates std::stringstream"
+ * doing it this way results in a smaller binary as well */
+
+enum class logf_level {
+	info,
+	error,
+};
+
+static int vlogf(logf_level level, const char* format, std::va_list ap)
+{
+	char buf[1024];
+	int r;
+
+	r = std::vsnprintf(buf, sizeof(buf), format, ap);
+	if (r < 0)
+		return -1;
+
+	switch (level) {
+	case logf_level::info:
+		console::info(buf);
+		break;
+	case logf_level::error:
+		console::error(buf);
+		break;
+	}
+
+	return r;
+}
+
+static int logf(logf_level level, const char* format, ...)
+{
+	std::va_list ap;
+	int r;
+
+	va_start(ap, format);
+
+	r = vlogf(level, format, ap);
+
+	va_end(ap);
+
+	return r;
+}
+
+/* ----------------------------------------------------------------------------------------- */
+
+class OutputSDL : public output_v4 {
 public:
-	OutputSDL();
+	OutputSDL(const GUID& p_device, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth);
 	~OutputSDL();
 
 	virtual double get_latency() override;
@@ -22,42 +77,77 @@
 	virtual void flush() override;
 	virtual void force_play() override;
 	virtual void volume_set(double p_val) override;
+	virtual bool is_progressing() override;
+	virtual size_t update_v2() override;
+
+	static GUID g_get_guid() { return { 0x90033ef5, 0x6703, 0x44a0, { 0x98, 0x47, 0x53, 0x99, 0x1, 0x53, 0x3b, 0xd6 } }; }
+	static void g_enum_devices(output_device_enum_callback &p_callback);
+	static bool g_advanced_settings_query() { return false; }
+	static bool g_needs_bitdepth_config() { return false; }
+	static bool g_needs_dither_config() { return false; }
+	static bool g_needs_device_list_prefixes() { return true; }
+	static bool g_supports_multiple_streams() { return false; }
+	static bool g_is_high_latency() { return true; }
+	static uint32_t g_extra_flags() { return 0; }
+	static void g_advanced_settings_popup(HWND p_parent, POINT p_menupoint) {}
+	static const char* g_get_name() { return "SDL"; }
 
 private:
+	// ---------- Static members --------------
+	// These are for loading SDL, as well as
+	// converting to/from GUIDs.
+	static HMODULE sdl_;
+
+	static GUID DevIDtoGUID(SDL_AudioDeviceID id);
+	static SDL_AudioDeviceID GUIDtoDevID(const GUID &id);
+
+	static bool LoadSDL();
+	static bool IsSDLLoaded();
+	static void UnloadSDL();
+	// ----------------------------------------
+
+	static void SDLCALL AudioStreamCallback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount);
+
+	void StreamCallback(int amount);
+
 	void ReinitStream(unsigned int channels, unsigned int freq);
 
+	// the audio stream itself
 	SDL_AudioStream *stream_;
 #ifndef OUTSDL_USE_NATIVE_F32
-	std::vector<t_int32> buffer_;
+	// buffer to convert f64 -> f32
+	std::vector<float> buffer_;
 #endif
+	// the current spec of the audio stream
 	SDL_AudioSpec spec_;
-	HANDLE sdl_;
 
-#define FUNC(type, x, params, callparams) decltype(&SDL_##x) sdl3_##x;
+	std::atomic<int> needed_;
+
+#define FUNC(type, x, params, callparams) static decltype(&SDL_##x) sdl3_##x;
 #include "foo_out_sdl_funcs.h"
 };
 
-OutputSDL::OutputSDL()
-	: sdl_(nullptr)
-	, stream_(nullptr)
+HMODULE OutputSDL::sdl_ = nullptr;
+#define FUNC(type, x, params, callparams) decltype(&SDL_##x) OutputSDL::sdl3_##x = nullptr;
+#include "foo_out_sdl_funcs.h"
+
+OutputSDL::OutputSDL(const GUID &p_device, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth)
+	: stream_(nullptr)
 #ifndef OUTSDL_USE_NATIVE_F32
 	/* sane default size */
 	, buffer_(8096)
 #endif
 {
-	sdl_ = LoadLibraryA("foo_out_sdl_wrapper.dll");
-	if (!sdl3_)
-		return;
+	// uhhhh
+	needed_.store(0);
 
-#define FUNC(type, x, params, callparams) \
-	sdl3_##x = (decltype(&SDL_##x))GetProcAddress(sdl_, "SDL_" #x); \
-	if (!sdl3_##x) \
-		return;
-#include "foo_out_sdl_funcs.h"
+	if (!LoadSDL())
+		return; // uh oh
 
 	// increment subsystem counter
 	// hope this succeeds!!!
-	sdl3_InitSubSystem(SDL_INIT_AUDIO);
+	if (!sdl3_InitSubSystem(SDL_INIT_AUDIO))
+		logf(logf_level::error, sdl3_GetError());
 
 	/* make a guess */
 	spec_.format = OUTSDL_FORMAT;
@@ -65,93 +155,223 @@
 	spec_.freq = 44100; /* guess CD quality */
 
 	// TODO supply output devices
-	stream_ = sdl3_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec_, nullptr, nullptr);
+	stream_ = sdl3_OpenAudioDeviceStream(GUIDtoDevID(p_device), &spec_, OutputSDL::AudioStreamCallback, this);
+	if (!stream_)
+		logf(logf_level::error, "%s", sdl3_GetError());
 }
 
 OutputSDL::~OutputSDL()
 {
-	if (sdl_ && stream_ && sdl3_DestroyAudioStream)
-		sdl3_DestroyAudioStream(stream_);
-	if (sdl_ && sdl3_QuitSubSystem)
-		sdl3_QuitSubSystem(SDL_INIT_AUDIO);
-	if (sdl_)
-		FreeLibrary(sdl_);
+	if (!IsSDLLoaded())
+		return; /* nothing to do */
+
+	sdl3_DestroyAudioStream(stream_);
+	sdl3_QuitSubSystem(SDL_INIT_AUDIO);
+	UnloadSDL();
+}
+
+void OutputSDL::StreamCallback(int total_amount)
+{
+	/* Don't need to do anything when additional_amount == 0 */
+	if (!total_amount) return;
+
+	needed_ += total_amount;
+}
+
+void SDLCALL OutputSDL::AudioStreamCallback(void *userdata, SDL_AudioStream *stream, int a, int t)
+{
+	// Simply forwards to the main stream callback
+	OutputSDL *This = reinterpret_cast<OutputSDL *>(userdata);
+
+	// assert(This->stream_ == stream);
+
+	// Seems to work fine ?
+	This->StreamCallback(t);
 }
 
 void OutputSDL::ReinitStream(unsigned int channels, unsigned int freq)
 {
-	// Fast path; no change
-	// I'm assuming that the channel input config is the same for fb2k
-	// and for SDL. This may not be the case. Whatever..
+	/* Fast path; no change
+	 * This is the common case. Most music is 44.1khz and stereo. */
 	if (spec_.channels == channels && spec_.freq == freq)
 		return;
 
+	spec_.format = OUTSDL_FORMAT;
 	spec_.channels = channels;
 	spec_.freq = freq;
 
+	logf(logf_level::info, "SDL: setting freq and channels %d, %d", channels, freq);
+
 	// tell SDL about our change
-	sdl3_SetAudioStreamFormat(stream_, &spec_, nullptr);
+	if (!sdl3_SetAudioStreamFormat(stream_, &spec_, nullptr))
+		logf(logf_level::error, sdl3_GetError());
 }
 
-virtual double OutputSDL::get_latency()
+double OutputSDL::get_latency()
 {
-	// TODO assume that one buffer's worth is queued
-	return 0.016; // i guess
+	// ??? I don't know
+	return 0.016;
 }
 
-virtual void OutputSDL::process_samples(const audio_chunk &p_chunk)
+void OutputSDL::process_samples(const audio_chunk &p_chunk)
 {
 	// Reinitialize stream with possibly new values for channels and frequency
 	ReinitStream(p_chunk.get_channels(), p_chunk.get_srate());
 
 	/* NOTE this is only actually tested with stereo */
 #ifdef OUTSDL_USE_NATIVE_F32
+	t_size sz = p_chunk.get_data_size() * sizeof(float);
 	/* audio_sample is 32-bit floating point; SDL can use this directly */
-	sdl3_PutAudioStreamData(stream_, p_chunk.get_data(), p_chunk.get_data_size());
+	if (!sdl3_PutAudioStreamData(stream_, p_chunk.get_data(), sz))
+		logf(logf_level::error, "SDL: %s", sdl3_GetError());
+	needed_ -= sz;
 #else
 	/* Expand the buffer if necessary */
-	t_size sz = p_chunk.get_data_size();
+	t_size sz;
 
+	sz = p_chunk.get_data_size();
 	if (sz > buffer_.size())
 		buffer_.resize(sz);
 
-	/* Convert to int32 */
-	audio_math::convert_to_int32(p_chunk.get_data(), sz, buffer_.data(), buffer_.size());
+	float *buf = buffer_.data();
+
+	/* Convert to f32 */
+	audio_math::convert(p_chunk.get_data(), buf, sz);
+
+	needed_ -= sz * sizeof(float);
 
-	/* Add int32 audio to stream */
-	sdl3_PutAudioStreamData(stream_, buffer_.data(), buffer_.size());
+	/* Add f32 audio to stream */
+	while (sz > 0) {
+		/* no possible loss of data here; we cap size_t to int */
+		int to = static_cast<int>((std::min)(sz, INT_MIN / sizeof(float)));
+
+		sdl3_PutAudioStreamData(stream_, buffer_.data(), to * sizeof(float));
+
+		sz -= to;
+		buf += to;
+	}
 #endif
 }
 
-virtual void OutputSDL::update(bool &p_ready)
+void OutputSDL::update(bool &p_ready) { p_ready = update_v2() > 0; }
+
+size_t OutputSDL::update_v2()
 {
-	/* seems legit */
-	p_ready = (sdl3_GetAudioStreamQueued(stream_) == 0);
+	return (std::max)(needed_.load(), 0) / (int)sizeof(float);
 }
 
-virtual void OutputSDL::pause(bool p_state)
+void OutputSDL::pause(bool p_state)
 {
+	logf(logf_level::info, "pause? %d", (int)p_state);
+	bool v;
 	if (p_state) {
-		sdl3_PauseAudioStreamDevice(stream_);
+		v = sdl3_PauseAudioStreamDevice(stream_);
 	} else {
-		sdl3_ResumeAudioStreamDevice(stream_);
+		v = sdl3_ResumeAudioStreamDevice(stream_);
 	}
+	if (!v) logf(logf_level::error, "pause: %s", sdl3_GetError());
 }
 
 // . these are easy
-virtual void OutputSDL::flush()
+void OutputSDL::flush()
+{
+	if (!sdl3_ClearAudioStream(stream_))
+		logf(logf_level::error, "flush: %s", sdl3_GetError());
+}
+
+void OutputSDL::force_play()
+{
+	if (!sdl3_FlushAudioStream(stream_))
+		logf(logf_level::error, "force_play: %s", sdl3_GetError());
+}
+
+void OutputSDL::volume_set(double p_val)
 {
-	sdl3_ClearAudioStream(stream_);
+	/* fb2k provides this as dB for some reason, so convert it back
+	 * to linear which is what normal programs use. */
+	double p_linear = std::pow(10.0, p_val / 10.0);
+
+	if (!sdl3_SetAudioStreamGain(stream_, /* shut up msvc */static_cast<float>(p_linear)))
+		logf(logf_level::error, "volume_set: %s", sdl3_GetError());
+}
+
+/* conversion to/from GUID and SDL_AudioDeviceID */
+GUID OutputSDL::DevIDtoGUID(SDL_AudioDeviceID id)
+{
+	GUID g = {0};
+	g.Data1 = id;
+	return g;
+}
+
+SDL_AudioDeviceID OutputSDL::GUIDtoDevID(const GUID &g)
+{
+	return g.Data1;
 }
 
-virtual void OutputSDL::force_play()
+void OutputSDL::g_enum_devices(output_device_enum_callback& p_callback)
 {
-	sdl3_FlushAudioStream(stream_);
+	int i, count;
+
+	if (!LoadSDL())
+		return; /* uh oh */
+
+	SDL_AudioDeviceID *devs = sdl3_GetAudioPlaybackDevices(&count);
+
+	for (i = 0; i < count; i++) {
+		const char *name = sdl3_GetAudioDeviceName(devs[i]);
+		if (!name) continue;
+	
+		p_callback.on_device(DevIDtoGUID(devs[i]), name, std::strlen(name));
+	}
+
+	// Add default device too for brevity
+	p_callback.on_device(DevIDtoGUID(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK), "default", 7);
 }
 
-virtual void OutputSDL::volume_set(double p_val)
+bool OutputSDL::LoadSDL(void)
 {
-	sdl3_SetAudioStreamGain(stream_, p_val);
+	if (IsSDLLoaded())
+		return true;
+
+	// Unload any failed attempts
+	UnloadSDL();
+
+	sdl_ = LoadLibrary("SDL3.dll");
+	if (!sdl_) {
+		logf(logf_level::error, "Failed to load SDL3.dll!");
+		return false;
+	}
+
+#define FUNC(type, x, params, callparams) \
+	sdl3_##x = (decltype(&SDL_##x))GetProcAddress(sdl_, "SDL_" #x);
+#include "foo_out_sdl_funcs.h"
+
+	return IsSDLLoaded();
+}
+
+bool OutputSDL::IsSDLLoaded()
+{
+	return (sdl_
+#define FUNC(type, x, params, callparams) && (!!sdl3_##x)
+#include "foo_out_sdl_funcs.h"
+		);
+}
+
+void OutputSDL::UnloadSDL()
+{
+	// kill off everything
+	if (sdl_) {
+		FreeLibrary(sdl_);
+		sdl_ = nullptr;
+	}
+
+#define FUNC(type, x, params, callparams) sdl3_##x = nullptr;
+#include "foo_out_sdl_funcs.h"
+}
+
+bool OutputSDL::is_progressing()
+{
+	return true; // ?
 }
 
 static output_factory_t<OutputSDL> g_output_sdl_factory;