|
1
|
1 #include <algorithm>
|
|
|
2 #include <cstdlib>
|
|
|
3 #include <cstdio>
|
|
|
4 #include <cstdarg>
|
|
|
5 #include <cmath>
|
|
|
6 #include <atomic>
|
|
0
|
7
|
|
1
|
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"
|
|
0
|
13
|
|
|
14 #if audio_sample_size == 32
|
|
|
15 # define OUTSDL_USE_NATIVE_F32 1
|
|
|
16 # define OUTSDL_FORMAT SDL_AUDIO_F32
|
|
|
17 #else
|
|
|
18 # define OUTSDL_FORMAT SDL_AUDIO_S32
|
|
|
19 #endif
|
|
|
20
|
|
1
|
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 {
|
|
0
|
69 public:
|
|
1
|
70 OutputSDL(const GUID& p_device, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth);
|
|
0
|
71 ~OutputSDL();
|
|
|
72
|
|
|
73 virtual double get_latency() override;
|
|
|
74 virtual void process_samples(const audio_chunk &p_chunk) override;
|
|
|
75 virtual void update(bool &p_ready) override;
|
|
|
76 virtual void pause(bool p_state) override;
|
|
|
77 virtual void flush() override;
|
|
|
78 virtual void force_play() override;
|
|
|
79 virtual void volume_set(double p_val) override;
|
|
1
|
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"; }
|
|
0
|
94
|
|
|
95 private:
|
|
1
|
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
|
|
0
|
113 void ReinitStream(unsigned int channels, unsigned int freq);
|
|
|
114
|
|
1
|
115 // the audio stream itself
|
|
0
|
116 SDL_AudioStream *stream_;
|
|
|
117 #ifndef OUTSDL_USE_NATIVE_F32
|
|
1
|
118 // buffer to convert f64 -> f32
|
|
|
119 std::vector<float> buffer_;
|
|
0
|
120 #endif
|
|
1
|
121 // the current spec of the audio stream
|
|
0
|
122 SDL_AudioSpec spec_;
|
|
|
123
|
|
1
|
124 std::atomic<int> needed_;
|
|
|
125
|
|
|
126 #define FUNC(type, x, params, callparams) static decltype(&SDL_##x) sdl3_##x;
|
|
0
|
127 #include "foo_out_sdl_funcs.h"
|
|
|
128 };
|
|
|
129
|
|
1
|
130 HMODULE OutputSDL::sdl_ = nullptr;
|
|
|
131 #define FUNC(type, x, params, callparams) decltype(&SDL_##x) OutputSDL::sdl3_##x = 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)
|
|
0
|
136 #ifndef OUTSDL_USE_NATIVE_F32
|
|
|
137 /* sane default size */
|
|
|
138 , buffer_(8096)
|
|
|
139 #endif
|
|
|
140 {
|
|
1
|
141 // uhhhh
|
|
|
142 needed_.store(0);
|
|
0
|
143
|
|
1
|
144 if (!LoadSDL())
|
|
|
145 return; // uh oh
|
|
0
|
146
|
|
|
147 // increment subsystem counter
|
|
|
148 // hope this succeeds!!!
|
|
1
|
149 if (!sdl3_InitSubSystem(SDL_INIT_AUDIO))
|
|
|
150 logf(logf_level::error, sdl3_GetError());
|
|
0
|
151
|
|
|
152 /* make a guess */
|
|
|
153 spec_.format = OUTSDL_FORMAT;
|
|
|
154 spec_.channels = 2; /* Stereo is most likely. Who cares about surround? :) */
|
|
|
155 spec_.freq = 44100; /* guess CD quality */
|
|
|
156
|
|
|
157 // TODO supply output devices
|
|
1
|
158 stream_ = sdl3_OpenAudioDeviceStream(GUIDtoDevID(p_device), &spec_, OutputSDL::AudioStreamCallback, this);
|
|
|
159 if (!stream_)
|
|
|
160 logf(logf_level::error, "%s", sdl3_GetError());
|
|
0
|
161 }
|
|
|
162
|
|
|
163 OutputSDL::~OutputSDL()
|
|
|
164 {
|
|
1
|
165 if (!IsSDLLoaded())
|
|
|
166 return; /* nothing to do */
|
|
|
167
|
|
|
168 sdl3_DestroyAudioStream(stream_);
|
|
|
169 sdl3_QuitSubSystem(SDL_INIT_AUDIO);
|
|
|
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);
|
|
0
|
190 }
|
|
|
191
|
|
|
192 void OutputSDL::ReinitStream(unsigned int channels, unsigned int freq)
|
|
|
193 {
|
|
1
|
194 /* Fast path; no change
|
|
|
195 * This is the common case. Most music is 44.1khz and stereo. */
|
|
0
|
196 if (spec_.channels == channels && spec_.freq == freq)
|
|
|
197 return;
|
|
|
198
|
|
1
|
199 spec_.format = OUTSDL_FORMAT;
|
|
0
|
200 spec_.channels = channels;
|
|
|
201 spec_.freq = freq;
|
|
|
202
|
|
1
|
203 logf(logf_level::info, "SDL: setting freq and channels %d, %d", channels, freq);
|
|
|
204
|
|
0
|
205 // tell SDL about our change
|
|
1
|
206 if (!sdl3_SetAudioStreamFormat(stream_, &spec_, nullptr))
|
|
|
207 logf(logf_level::error, sdl3_GetError());
|
|
0
|
208 }
|
|
|
209
|
|
1
|
210 double OutputSDL::get_latency()
|
|
0
|
211 {
|
|
1
|
212 // ??? I don't know
|
|
|
213 return 0.016;
|
|
0
|
214 }
|
|
|
215
|
|
1
|
216 void OutputSDL::process_samples(const audio_chunk &p_chunk)
|
|
0
|
217 {
|
|
|
218 // Reinitialize stream with possibly new values for channels and frequency
|
|
|
219 ReinitStream(p_chunk.get_channels(), p_chunk.get_srate());
|
|
|
220
|
|
|
221 /* NOTE this is only actually tested with stereo */
|
|
|
222 #ifdef OUTSDL_USE_NATIVE_F32
|
|
1
|
223 t_size sz = p_chunk.get_data_size() * sizeof(float);
|
|
0
|
224 /* audio_sample is 32-bit floating point; SDL can use this directly */
|
|
1
|
225 if (!sdl3_PutAudioStreamData(stream_, p_chunk.get_data(), sz))
|
|
|
226 logf(logf_level::error, "SDL: %s", sdl3_GetError());
|
|
|
227 needed_ -= sz;
|
|
0
|
228 #else
|
|
|
229 /* Expand the buffer if necessary */
|
|
1
|
230 t_size sz;
|
|
0
|
231
|
|
1
|
232 sz = p_chunk.get_data_size();
|
|
0
|
233 if (sz > buffer_.size())
|
|
|
234 buffer_.resize(sz);
|
|
|
235
|
|
1
|
236 float *buf = buffer_.data();
|
|
|
237
|
|
|
238 /* Convert to f32 */
|
|
|
239 audio_math::convert(p_chunk.get_data(), buf, sz);
|
|
|
240
|
|
|
241 needed_ -= sz * sizeof(float);
|
|
0
|
242
|
|
1
|
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 }
|
|
0
|
253 #endif
|
|
|
254 }
|
|
|
255
|
|
1
|
256 void OutputSDL::update(bool &p_ready) { p_ready = update_v2() > 0; }
|
|
|
257
|
|
|
258 size_t OutputSDL::update_v2()
|
|
0
|
259 {
|
|
1
|
260 return (std::max)(needed_.load(), 0) / (int)sizeof(float);
|
|
0
|
261 }
|
|
|
262
|
|
1
|
263 void OutputSDL::pause(bool p_state)
|
|
0
|
264 {
|
|
1
|
265 logf(logf_level::info, "pause? %d", (int)p_state);
|
|
|
266 bool v;
|
|
0
|
267 if (p_state) {
|
|
1
|
268 v = sdl3_PauseAudioStreamDevice(stream_);
|
|
0
|
269 } else {
|
|
1
|
270 v = sdl3_ResumeAudioStreamDevice(stream_);
|
|
0
|
271 }
|
|
1
|
272 if (!v) logf(logf_level::error, "pause: %s", sdl3_GetError());
|
|
0
|
273 }
|
|
|
274
|
|
|
275 // . these are easy
|
|
1
|
276 void OutputSDL::flush()
|
|
|
277 {
|
|
|
278 if (!sdl3_ClearAudioStream(stream_))
|
|
|
279 logf(logf_level::error, "flush: %s", sdl3_GetError());
|
|
|
280 }
|
|
|
281
|
|
|
282 void OutputSDL::force_play()
|
|
|
283 {
|
|
|
284 if (!sdl3_FlushAudioStream(stream_))
|
|
|
285 logf(logf_level::error, "force_play: %s", sdl3_GetError());
|
|
|
286 }
|
|
|
287
|
|
|
288 void OutputSDL::volume_set(double p_val)
|
|
0
|
289 {
|
|
1
|
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;
|
|
0
|
309 }
|
|
|
310
|
|
1
|
311 void OutputSDL::g_enum_devices(output_device_enum_callback& p_callback)
|
|
0
|
312 {
|
|
1
|
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);
|
|
0
|
329 }
|
|
|
330
|
|
1
|
331 bool OutputSDL::LoadSDL(void)
|
|
0
|
332 {
|
|
1
|
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; // ?
|
|
0
|
375 }
|
|
|
376
|
|
|
377 static output_factory_t<OutputSDL> g_output_sdl_factory;
|
|
|
378
|
|
|
379 ////////////////////////////////////////////////////////////////////////////////
|
|
|
380
|
|
|
381 const char *about = "Copyright (c) paper <paper@tflc.us>, 2026\n";
|
|
|
382
|
|
|
383 // very beta ;)
|
|
|
384 DECLARE_COMPONENT_VERSION("SDL output", "0.1", about);
|
|
|
385 VALIDATE_COMPONENT_FILENAME("foo_out_sdl.dll");
|