Mercurial > foo_out_sdl
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 0:e9bb126753e7 | 1:20d02a178406 |
|---|---|
| 1 /* SDL output for foobar2000. | 1 #include <algorithm> |
| 2 * Somewhat scuffed since SDL does not provide latency */ | 2 #include <cstdlib> |
| 3 | 3 #include <cstdio> |
| 4 #include <SDL3/SDL.h> | 4 #include <cstdarg> |
| 5 #include <cmath> | |
| 6 #include <atomic> | |
| 7 | |
| 8 #include "SDL3/SDL.h" | |
| 9 | |
| 10 #include "foobar2000/helpers/foobar2000+atl.h" | |
| 11 #include "foobar2000/SDK/core_api.h" | |
| 12 #include "foobar2000/SDK/output.h" | |
| 5 | 13 |
| 6 #if audio_sample_size == 32 | 14 #if audio_sample_size == 32 |
| 7 # define OUTSDL_USE_NATIVE_F32 1 | 15 # define OUTSDL_USE_NATIVE_F32 1 |
| 8 # define OUTSDL_FORMAT SDL_AUDIO_F32 | 16 # define OUTSDL_FORMAT SDL_AUDIO_F32 |
| 9 #else | 17 #else |
| 10 # define OUTSDL_FORMAT SDL_AUDIO_S32 | 18 # define OUTSDL_FORMAT SDL_AUDIO_S32 |
| 11 #endif | 19 #endif |
| 12 | 20 |
| 13 class OutputSDL : public output { | 21 /* ----------------------------------------------------------------------------------------- */ |
| 22 /* logf: printf-like interface for printing into the console | |
| 23 * a.k.a. "paper hates std::stringstream" | |
| 24 * doing it this way results in a smaller binary as well */ | |
| 25 | |
| 26 enum class logf_level { | |
| 27 info, | |
| 28 error, | |
| 29 }; | |
| 30 | |
| 31 static int vlogf(logf_level level, const char* format, std::va_list ap) | |
| 32 { | |
| 33 char buf[1024]; | |
| 34 int r; | |
| 35 | |
| 36 r = std::vsnprintf(buf, sizeof(buf), format, ap); | |
| 37 if (r < 0) | |
| 38 return -1; | |
| 39 | |
| 40 switch (level) { | |
| 41 case logf_level::info: | |
| 42 console::info(buf); | |
| 43 break; | |
| 44 case logf_level::error: | |
| 45 console::error(buf); | |
| 46 break; | |
| 47 } | |
| 48 | |
| 49 return r; | |
| 50 } | |
| 51 | |
| 52 static int logf(logf_level level, const char* format, ...) | |
| 53 { | |
| 54 std::va_list ap; | |
| 55 int r; | |
| 56 | |
| 57 va_start(ap, format); | |
| 58 | |
| 59 r = vlogf(level, format, ap); | |
| 60 | |
| 61 va_end(ap); | |
| 62 | |
| 63 return r; | |
| 64 } | |
| 65 | |
| 66 /* ----------------------------------------------------------------------------------------- */ | |
| 67 | |
| 68 class OutputSDL : public output_v4 { | |
| 14 public: | 69 public: |
| 15 OutputSDL(); | 70 OutputSDL(const GUID& p_device, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth); |
| 16 ~OutputSDL(); | 71 ~OutputSDL(); |
| 17 | 72 |
| 18 virtual double get_latency() override; | 73 virtual double get_latency() override; |
| 19 virtual void process_samples(const audio_chunk &p_chunk) override; | 74 virtual void process_samples(const audio_chunk &p_chunk) override; |
| 20 virtual void update(bool &p_ready) override; | 75 virtual void update(bool &p_ready) override; |
| 21 virtual void pause(bool p_state) override; | 76 virtual void pause(bool p_state) override; |
| 22 virtual void flush() override; | 77 virtual void flush() override; |
| 23 virtual void force_play() override; | 78 virtual void force_play() override; |
| 24 virtual void volume_set(double p_val) override; | 79 virtual void volume_set(double p_val) override; |
| 80 virtual bool is_progressing() override; | |
| 81 virtual size_t update_v2() override; | |
| 82 | |
| 83 static GUID g_get_guid() { return { 0x90033ef5, 0x6703, 0x44a0, { 0x98, 0x47, 0x53, 0x99, 0x1, 0x53, 0x3b, 0xd6 } }; } | |
| 84 static void g_enum_devices(output_device_enum_callback &p_callback); | |
| 85 static bool g_advanced_settings_query() { return false; } | |
| 86 static bool g_needs_bitdepth_config() { return false; } | |
| 87 static bool g_needs_dither_config() { return false; } | |
| 88 static bool g_needs_device_list_prefixes() { return true; } | |
| 89 static bool g_supports_multiple_streams() { return false; } | |
| 90 static bool g_is_high_latency() { return true; } | |
| 91 static uint32_t g_extra_flags() { return 0; } | |
| 92 static void g_advanced_settings_popup(HWND p_parent, POINT p_menupoint) {} | |
| 93 static const char* g_get_name() { return "SDL"; } | |
| 25 | 94 |
| 26 private: | 95 private: |
| 96 // ---------- Static members -------------- | |
| 97 // These are for loading SDL, as well as | |
| 98 // converting to/from GUIDs. | |
| 99 static HMODULE sdl_; | |
| 100 | |
| 101 static GUID DevIDtoGUID(SDL_AudioDeviceID id); | |
| 102 static SDL_AudioDeviceID GUIDtoDevID(const GUID &id); | |
| 103 | |
| 104 static bool LoadSDL(); | |
| 105 static bool IsSDLLoaded(); | |
| 106 static void UnloadSDL(); | |
| 107 // ---------------------------------------- | |
| 108 | |
| 109 static void SDLCALL AudioStreamCallback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount); | |
| 110 | |
| 111 void StreamCallback(int amount); | |
| 112 | |
| 27 void ReinitStream(unsigned int channels, unsigned int freq); | 113 void ReinitStream(unsigned int channels, unsigned int freq); |
| 28 | 114 |
| 115 // the audio stream itself | |
| 29 SDL_AudioStream *stream_; | 116 SDL_AudioStream *stream_; |
| 30 #ifndef OUTSDL_USE_NATIVE_F32 | 117 #ifndef OUTSDL_USE_NATIVE_F32 |
| 31 std::vector<t_int32> buffer_; | 118 // buffer to convert f64 -> f32 |
| 119 std::vector<float> buffer_; | |
| 32 #endif | 120 #endif |
| 121 // the current spec of the audio stream | |
| 33 SDL_AudioSpec spec_; | 122 SDL_AudioSpec spec_; |
| 34 HANDLE sdl_; | 123 |
| 35 | 124 std::atomic<int> needed_; |
| 36 #define FUNC(type, x, params, callparams) decltype(&SDL_##x) sdl3_##x; | 125 |
| 126 #define FUNC(type, x, params, callparams) static decltype(&SDL_##x) sdl3_##x; | |
| 37 #include "foo_out_sdl_funcs.h" | 127 #include "foo_out_sdl_funcs.h" |
| 38 }; | 128 }; |
| 39 | 129 |
| 40 OutputSDL::OutputSDL() | 130 HMODULE OutputSDL::sdl_ = nullptr; |
| 41 : sdl_(nullptr) | 131 #define FUNC(type, x, params, callparams) decltype(&SDL_##x) OutputSDL::sdl3_##x = nullptr; |
| 42 , stream_(nullptr) | 132 #include "foo_out_sdl_funcs.h" |
| 133 | |
| 134 OutputSDL::OutputSDL(const GUID &p_device, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth) | |
| 135 : stream_(nullptr) | |
| 43 #ifndef OUTSDL_USE_NATIVE_F32 | 136 #ifndef OUTSDL_USE_NATIVE_F32 |
| 44 /* sane default size */ | 137 /* sane default size */ |
| 45 , buffer_(8096) | 138 , buffer_(8096) |
| 46 #endif | 139 #endif |
| 47 { | 140 { |
| 48 sdl_ = LoadLibraryA("foo_out_sdl_wrapper.dll"); | 141 // uhhhh |
| 49 if (!sdl3_) | 142 needed_.store(0); |
| 50 return; | 143 |
| 51 | 144 if (!LoadSDL()) |
| 52 #define FUNC(type, x, params, callparams) \ | 145 return; // uh oh |
| 53 sdl3_##x = (decltype(&SDL_##x))GetProcAddress(sdl_, "SDL_" #x); \ | |
| 54 if (!sdl3_##x) \ | |
| 55 return; | |
| 56 #include "foo_out_sdl_funcs.h" | |
| 57 | 146 |
| 58 // increment subsystem counter | 147 // increment subsystem counter |
| 59 // hope this succeeds!!! | 148 // hope this succeeds!!! |
| 60 sdl3_InitSubSystem(SDL_INIT_AUDIO); | 149 if (!sdl3_InitSubSystem(SDL_INIT_AUDIO)) |
| 150 logf(logf_level::error, sdl3_GetError()); | |
| 61 | 151 |
| 62 /* make a guess */ | 152 /* make a guess */ |
| 63 spec_.format = OUTSDL_FORMAT; | 153 spec_.format = OUTSDL_FORMAT; |
| 64 spec_.channels = 2; /* Stereo is most likely. Who cares about surround? :) */ | 154 spec_.channels = 2; /* Stereo is most likely. Who cares about surround? :) */ |
| 65 spec_.freq = 44100; /* guess CD quality */ | 155 spec_.freq = 44100; /* guess CD quality */ |
| 66 | 156 |
| 67 // TODO supply output devices | 157 // TODO supply output devices |
| 68 stream_ = sdl3_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec_, nullptr, nullptr); | 158 stream_ = sdl3_OpenAudioDeviceStream(GUIDtoDevID(p_device), &spec_, OutputSDL::AudioStreamCallback, this); |
| 159 if (!stream_) | |
| 160 logf(logf_level::error, "%s", sdl3_GetError()); | |
| 69 } | 161 } |
| 70 | 162 |
| 71 OutputSDL::~OutputSDL() | 163 OutputSDL::~OutputSDL() |
| 72 { | 164 { |
| 73 if (sdl_ && stream_ && sdl3_DestroyAudioStream) | 165 if (!IsSDLLoaded()) |
| 74 sdl3_DestroyAudioStream(stream_); | 166 return; /* nothing to do */ |
| 75 if (sdl_ && sdl3_QuitSubSystem) | 167 |
| 76 sdl3_QuitSubSystem(SDL_INIT_AUDIO); | 168 sdl3_DestroyAudioStream(stream_); |
| 77 if (sdl_) | 169 sdl3_QuitSubSystem(SDL_INIT_AUDIO); |
| 78 FreeLibrary(sdl_); | 170 UnloadSDL(); |
| 171 } | |
| 172 | |
| 173 void OutputSDL::StreamCallback(int total_amount) | |
| 174 { | |
| 175 /* Don't need to do anything when additional_amount == 0 */ | |
| 176 if (!total_amount) return; | |
| 177 | |
| 178 needed_ += total_amount; | |
| 179 } | |
| 180 | |
| 181 void SDLCALL OutputSDL::AudioStreamCallback(void *userdata, SDL_AudioStream *stream, int a, int t) | |
| 182 { | |
| 183 // Simply forwards to the main stream callback | |
| 184 OutputSDL *This = reinterpret_cast<OutputSDL *>(userdata); | |
| 185 | |
| 186 // assert(This->stream_ == stream); | |
| 187 | |
| 188 // Seems to work fine ? | |
| 189 This->StreamCallback(t); | |
| 79 } | 190 } |
| 80 | 191 |
| 81 void OutputSDL::ReinitStream(unsigned int channels, unsigned int freq) | 192 void OutputSDL::ReinitStream(unsigned int channels, unsigned int freq) |
| 82 { | 193 { |
| 83 // Fast path; no change | 194 /* Fast path; no change |
| 84 // I'm assuming that the channel input config is the same for fb2k | 195 * This is the common case. Most music is 44.1khz and stereo. */ |
| 85 // and for SDL. This may not be the case. Whatever.. | |
| 86 if (spec_.channels == channels && spec_.freq == freq) | 196 if (spec_.channels == channels && spec_.freq == freq) |
| 87 return; | 197 return; |
| 88 | 198 |
| 199 spec_.format = OUTSDL_FORMAT; | |
| 89 spec_.channels = channels; | 200 spec_.channels = channels; |
| 90 spec_.freq = freq; | 201 spec_.freq = freq; |
| 91 | 202 |
| 203 logf(logf_level::info, "SDL: setting freq and channels %d, %d", channels, freq); | |
| 204 | |
| 92 // tell SDL about our change | 205 // tell SDL about our change |
| 93 sdl3_SetAudioStreamFormat(stream_, &spec_, nullptr); | 206 if (!sdl3_SetAudioStreamFormat(stream_, &spec_, nullptr)) |
| 94 } | 207 logf(logf_level::error, sdl3_GetError()); |
| 95 | 208 } |
| 96 virtual double OutputSDL::get_latency() | 209 |
| 97 { | 210 double OutputSDL::get_latency() |
| 98 // TODO assume that one buffer's worth is queued | 211 { |
| 99 return 0.016; // i guess | 212 // ??? I don't know |
| 100 } | 213 return 0.016; |
| 101 | 214 } |
| 102 virtual void OutputSDL::process_samples(const audio_chunk &p_chunk) | 215 |
| 216 void OutputSDL::process_samples(const audio_chunk &p_chunk) | |
| 103 { | 217 { |
| 104 // Reinitialize stream with possibly new values for channels and frequency | 218 // Reinitialize stream with possibly new values for channels and frequency |
| 105 ReinitStream(p_chunk.get_channels(), p_chunk.get_srate()); | 219 ReinitStream(p_chunk.get_channels(), p_chunk.get_srate()); |
| 106 | 220 |
| 107 /* NOTE this is only actually tested with stereo */ | 221 /* NOTE this is only actually tested with stereo */ |
| 108 #ifdef OUTSDL_USE_NATIVE_F32 | 222 #ifdef OUTSDL_USE_NATIVE_F32 |
| 223 t_size sz = p_chunk.get_data_size() * sizeof(float); | |
| 109 /* audio_sample is 32-bit floating point; SDL can use this directly */ | 224 /* audio_sample is 32-bit floating point; SDL can use this directly */ |
| 110 sdl3_PutAudioStreamData(stream_, p_chunk.get_data(), p_chunk.get_data_size()); | 225 if (!sdl3_PutAudioStreamData(stream_, p_chunk.get_data(), sz)) |
| 226 logf(logf_level::error, "SDL: %s", sdl3_GetError()); | |
| 227 needed_ -= sz; | |
| 111 #else | 228 #else |
| 112 /* Expand the buffer if necessary */ | 229 /* Expand the buffer if necessary */ |
| 113 t_size sz = p_chunk.get_data_size(); | 230 t_size sz; |
| 114 | 231 |
| 232 sz = p_chunk.get_data_size(); | |
| 115 if (sz > buffer_.size()) | 233 if (sz > buffer_.size()) |
| 116 buffer_.resize(sz); | 234 buffer_.resize(sz); |
| 117 | 235 |
| 118 /* Convert to int32 */ | 236 float *buf = buffer_.data(); |
| 119 audio_math::convert_to_int32(p_chunk.get_data(), sz, buffer_.data(), buffer_.size()); | 237 |
| 120 | 238 /* Convert to f32 */ |
| 121 /* Add int32 audio to stream */ | 239 audio_math::convert(p_chunk.get_data(), buf, sz); |
| 122 sdl3_PutAudioStreamData(stream_, buffer_.data(), buffer_.size()); | 240 |
| 241 needed_ -= sz * sizeof(float); | |
| 242 | |
| 243 /* Add f32 audio to stream */ | |
| 244 while (sz > 0) { | |
| 245 /* no possible loss of data here; we cap size_t to int */ | |
| 246 int to = static_cast<int>((std::min)(sz, INT_MIN / sizeof(float))); | |
| 247 | |
| 248 sdl3_PutAudioStreamData(stream_, buffer_.data(), to * sizeof(float)); | |
| 249 | |
| 250 sz -= to; | |
| 251 buf += to; | |
| 252 } | |
| 123 #endif | 253 #endif |
| 124 } | 254 } |
| 125 | 255 |
| 126 virtual void OutputSDL::update(bool &p_ready) | 256 void OutputSDL::update(bool &p_ready) { p_ready = update_v2() > 0; } |
| 127 { | 257 |
| 128 /* seems legit */ | 258 size_t OutputSDL::update_v2() |
| 129 p_ready = (sdl3_GetAudioStreamQueued(stream_) == 0); | 259 { |
| 130 } | 260 return (std::max)(needed_.load(), 0) / (int)sizeof(float); |
| 131 | 261 } |
| 132 virtual void OutputSDL::pause(bool p_state) | 262 |
| 133 { | 263 void OutputSDL::pause(bool p_state) |
| 264 { | |
| 265 logf(logf_level::info, "pause? %d", (int)p_state); | |
| 266 bool v; | |
| 134 if (p_state) { | 267 if (p_state) { |
| 135 sdl3_PauseAudioStreamDevice(stream_); | 268 v = sdl3_PauseAudioStreamDevice(stream_); |
| 136 } else { | 269 } else { |
| 137 sdl3_ResumeAudioStreamDevice(stream_); | 270 v = sdl3_ResumeAudioStreamDevice(stream_); |
| 138 } | 271 } |
| 272 if (!v) logf(logf_level::error, "pause: %s", sdl3_GetError()); | |
| 139 } | 273 } |
| 140 | 274 |
| 141 // . these are easy | 275 // . these are easy |
| 142 virtual void OutputSDL::flush() | 276 void OutputSDL::flush() |
| 143 { | 277 { |
| 144 sdl3_ClearAudioStream(stream_); | 278 if (!sdl3_ClearAudioStream(stream_)) |
| 145 } | 279 logf(logf_level::error, "flush: %s", sdl3_GetError()); |
| 146 | 280 } |
| 147 virtual void OutputSDL::force_play() | 281 |
| 148 { | 282 void OutputSDL::force_play() |
| 149 sdl3_FlushAudioStream(stream_); | 283 { |
| 150 } | 284 if (!sdl3_FlushAudioStream(stream_)) |
| 151 | 285 logf(logf_level::error, "force_play: %s", sdl3_GetError()); |
| 152 virtual void OutputSDL::volume_set(double p_val) | 286 } |
| 153 { | 287 |
| 154 sdl3_SetAudioStreamGain(stream_, p_val); | 288 void OutputSDL::volume_set(double p_val) |
| 289 { | |
| 290 /* fb2k provides this as dB for some reason, so convert it back | |
| 291 * to linear which is what normal programs use. */ | |
| 292 double p_linear = std::pow(10.0, p_val / 10.0); | |
| 293 | |
| 294 if (!sdl3_SetAudioStreamGain(stream_, /* shut up msvc */static_cast<float>(p_linear))) | |
| 295 logf(logf_level::error, "volume_set: %s", sdl3_GetError()); | |
| 296 } | |
| 297 | |
| 298 /* conversion to/from GUID and SDL_AudioDeviceID */ | |
| 299 GUID OutputSDL::DevIDtoGUID(SDL_AudioDeviceID id) | |
| 300 { | |
| 301 GUID g = {0}; | |
| 302 g.Data1 = id; | |
| 303 return g; | |
| 304 } | |
| 305 | |
| 306 SDL_AudioDeviceID OutputSDL::GUIDtoDevID(const GUID &g) | |
| 307 { | |
| 308 return g.Data1; | |
| 309 } | |
| 310 | |
| 311 void OutputSDL::g_enum_devices(output_device_enum_callback& p_callback) | |
| 312 { | |
| 313 int i, count; | |
| 314 | |
| 315 if (!LoadSDL()) | |
| 316 return; /* uh oh */ | |
| 317 | |
| 318 SDL_AudioDeviceID *devs = sdl3_GetAudioPlaybackDevices(&count); | |
| 319 | |
| 320 for (i = 0; i < count; i++) { | |
| 321 const char *name = sdl3_GetAudioDeviceName(devs[i]); | |
| 322 if (!name) continue; | |
| 323 | |
| 324 p_callback.on_device(DevIDtoGUID(devs[i]), name, std::strlen(name)); | |
| 325 } | |
| 326 | |
| 327 // Add default device too for brevity | |
| 328 p_callback.on_device(DevIDtoGUID(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK), "default", 7); | |
| 329 } | |
| 330 | |
| 331 bool OutputSDL::LoadSDL(void) | |
| 332 { | |
| 333 if (IsSDLLoaded()) | |
| 334 return true; | |
| 335 | |
| 336 // Unload any failed attempts | |
| 337 UnloadSDL(); | |
| 338 | |
| 339 sdl_ = LoadLibrary("SDL3.dll"); | |
| 340 if (!sdl_) { | |
| 341 logf(logf_level::error, "Failed to load SDL3.dll!"); | |
| 342 return false; | |
| 343 } | |
| 344 | |
| 345 #define FUNC(type, x, params, callparams) \ | |
| 346 sdl3_##x = (decltype(&SDL_##x))GetProcAddress(sdl_, "SDL_" #x); | |
| 347 #include "foo_out_sdl_funcs.h" | |
| 348 | |
| 349 return IsSDLLoaded(); | |
| 350 } | |
| 351 | |
| 352 bool OutputSDL::IsSDLLoaded() | |
| 353 { | |
| 354 return (sdl_ | |
| 355 #define FUNC(type, x, params, callparams) && (!!sdl3_##x) | |
| 356 #include "foo_out_sdl_funcs.h" | |
| 357 ); | |
| 358 } | |
| 359 | |
| 360 void OutputSDL::UnloadSDL() | |
| 361 { | |
| 362 // kill off everything | |
| 363 if (sdl_) { | |
| 364 FreeLibrary(sdl_); | |
| 365 sdl_ = nullptr; | |
| 366 } | |
| 367 | |
| 368 #define FUNC(type, x, params, callparams) sdl3_##x = nullptr; | |
| 369 #include "foo_out_sdl_funcs.h" | |
| 370 } | |
| 371 | |
| 372 bool OutputSDL::is_progressing() | |
| 373 { | |
| 374 return true; // ? | |
| 155 } | 375 } |
| 156 | 376 |
| 157 static output_factory_t<OutputSDL> g_output_sdl_factory; | 377 static output_factory_t<OutputSDL> g_output_sdl_factory; |
| 158 | 378 |
| 159 //////////////////////////////////////////////////////////////////////////////// | 379 //////////////////////////////////////////////////////////////////////////////// |
