view 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 source

#include <algorithm>
#include <cstdlib>
#include <cstdio>
#include <cstdarg>
#include <cmath>
#include <atomic>

#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
# define OUTSDL_FORMAT SDL_AUDIO_F32
#else
# define OUTSDL_FORMAT SDL_AUDIO_S32
#endif

/* ----------------------------------------------------------------------------------------- */
/* 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(const GUID& p_device, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth);
	~OutputSDL();

	virtual double get_latency() override;
	virtual void process_samples(const audio_chunk &p_chunk) override;
	virtual void update(bool &p_ready) override;
	virtual void pause(bool p_state) override;
	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
	// buffer to convert f64 -> f32
	std::vector<float> buffer_;
#endif
	// the current spec of the audio stream
	SDL_AudioSpec spec_;

	std::atomic<int> needed_;

#define FUNC(type, x, params, callparams) static decltype(&SDL_##x) sdl3_##x;
#include "foo_out_sdl_funcs.h"
};

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
{
	// uhhhh
	needed_.store(0);

	if (!LoadSDL())
		return; // uh oh

	// increment subsystem counter
	// hope this succeeds!!!
	if (!sdl3_InitSubSystem(SDL_INIT_AUDIO))
		logf(logf_level::error, sdl3_GetError());

	/* make a guess */
	spec_.format = OUTSDL_FORMAT;
	spec_.channels = 2; /* Stereo is most likely. Who cares about surround? :) */
	spec_.freq = 44100; /* guess CD quality */

	// TODO supply output devices
	stream_ = sdl3_OpenAudioDeviceStream(GUIDtoDevID(p_device), &spec_, OutputSDL::AudioStreamCallback, this);
	if (!stream_)
		logf(logf_level::error, "%s", sdl3_GetError());
}

OutputSDL::~OutputSDL()
{
	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
	 * 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
	if (!sdl3_SetAudioStreamFormat(stream_, &spec_, nullptr))
		logf(logf_level::error, sdl3_GetError());
}

double OutputSDL::get_latency()
{
	// ??? I don't know
	return 0.016;
}

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 */
	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;

	sz = p_chunk.get_data_size();
	if (sz > buffer_.size())
		buffer_.resize(sz);

	float *buf = buffer_.data();

	/* Convert to f32 */
	audio_math::convert(p_chunk.get_data(), buf, sz);

	needed_ -= sz * sizeof(float);

	/* 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
}

void OutputSDL::update(bool &p_ready) { p_ready = update_v2() > 0; }

size_t OutputSDL::update_v2()
{
	return (std::max)(needed_.load(), 0) / (int)sizeof(float);
}

void OutputSDL::pause(bool p_state)
{
	logf(logf_level::info, "pause? %d", (int)p_state);
	bool v;
	if (p_state) {
		v = sdl3_PauseAudioStreamDevice(stream_);
	} else {
		v = sdl3_ResumeAudioStreamDevice(stream_);
	}
	if (!v) logf(logf_level::error, "pause: %s", sdl3_GetError());
}

// . these are easy
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)
{
	/* 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;
}

void OutputSDL::g_enum_devices(output_device_enum_callback& p_callback)
{
	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);
}

bool OutputSDL::LoadSDL(void)
{
	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;

////////////////////////////////////////////////////////////////////////////////

const char *about = "Copyright (c) paper <paper@tflc.us>, 2026\n";

// very beta ;)
DECLARE_COMPONENT_VERSION("SDL output", "0.1", about);
VALIDATE_COMPONENT_FILENAME("foo_out_sdl.dll");