1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
5 *
6 * Distributed under the terms of the ISC license; see accompanying file
7 * "COPYING" for details.
8 *
9 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
10 * See accompanying file "TRADEMARK" for details.
11 *
12 * To redistribute this file separately, substitute the full license texts
13 * for the above references.
14 */
15
16#include "C4Application.h"
17#include "C4AudioSystemSdl.h"
18#include "C4Config.h"
19#include "C4Log.h"
20
21#include "StdHelpers.h"
22#include "StdSdlSubSystem.h"
23
24#include <algorithm>
25#include <atomic>
26#include <cmath>
27#include <format>
28#include <stdexcept>
29#include <optional>
30#include <span>
31
32#include <SDL_mixer.h>
33
34class C4AudioSystemSdl : public C4AudioSystem
35{
36public:
37 C4AudioSystemSdl(int maxChannels, bool preferLinearResampling);
38 ~C4AudioSystemSdl() noexcept override;
39
40 void FadeOutMusic(std::int32_t ms) override;
41 bool IsMusicPlaying() const override;
42 void PlayMusic(const MusicFile *const music, bool loop) override;
43 void SetMusicVolume(float volume) override;
44 void StopMusic() override;
45 void UnpauseMusic() override;
46
47private:
48 static constexpr int Frequency = 44100;
49 static constexpr Uint16 Format = AUDIO_S16SYS;
50 static constexpr int NumChannels = 2;
51 static constexpr int BytesPerSecond =
52 Frequency * (SDL_AUDIO_BITSIZE(Format) / 8) * NumChannels;
53 static constexpr int InvalidChannel{-1};
54
55 // Smart pointers for SDL_mixer objects
56 using SDLMixChunkUniquePtr = C4DeleterFunctionUniquePtr<Mix_FreeChunk>;
57 using SDLMixMusicUniquePtr = C4DeleterFunctionUniquePtr<Mix_FreeMusic>;
58
59 std::optional<StdSdlSubSystem> system;
60
61 static void ThrowIfFailed(const char *funcName, bool failed, std::string_view errorMessage = {});
62
63 template <typename T>
64 using SampleLoadFunc = T *(*)(SDL_RWops *, int);
65
66 template <typename T>
67 static T *LoadSampleCheckMpegLayer3Header(SampleLoadFunc<T> loadFunc, const char *funcName, const void *buf, const std::size_t size);
68
69public:
70
71 class MusicFileSdl : public MusicFile
72 {
73 public:
74 MusicFileSdl(const void *buf, std::size_t size);
75
76 private:
77 SDLMixMusicUniquePtr sample;
78
79 friend class C4AudioSystemSdl;
80 };
81
82 MusicFile *CreateMusicFile(const void *buf, std::size_t size) override { return new MusicFileSdl{buf, size}; }
83
84 class SoundFileSdl;
85
86 class SoundChannelSdl : public SoundChannel
87 {
88 public:
89 // Plays sound file. If supported by audio library, playback starts paused.
90 SoundChannelSdl(const SoundFileSdl *const sound, bool loop);
91 ~SoundChannelSdl() override;
92
93 public:
94 bool IsPlaying() const override;
95 void SetPosition(std::uint32_t ms) override;
96 void SetVolumeAndPan(float volume, float pan) override;
97 void Unpause() override;
98 int GetChannelId() const { return channel.load(m: std::memory_order_acquire); }
99 void ClearChannelId() { channel.store(i: InvalidChannel, m: std::memory_order_release); }
100
101 private:
102 std::atomic_int channel;
103 };
104
105 SoundChannel *CreateSoundChannel(const SoundFile *const sound, bool loop) override
106 {
107 const auto channel = new SoundChannelSdl{static_cast<const SoundFileSdl *>(sound), loop};
108 playingChannels[channel->GetChannelId()] = channel;
109 return channel;
110 }
111
112 class SoundFileSdl : public SoundFile
113 {
114 public:
115 SoundFileSdl(const void *buf, std::size_t size);
116
117 public:
118 std::uint32_t GetDuration() const override;
119
120 private:
121 const SDLMixChunkUniquePtr sample;
122
123 friend class C4AudioSystemSdl;
124 };
125
126 virtual SoundFile *CreateSoundFile(const void *buf, std::size_t size) override { return new SoundFileSdl{buf, size}; }
127
128private:
129 std::vector<SoundChannelSdl *> playingChannels;
130
131 static inline C4AudioSystemSdl *instance{nullptr};
132 static void ChannelFinished(int channel);
133};
134
135// this is used instead of MIX_MAX_VOLUME, because MIX_MAX_VOLUME is very loud and easily leads to clipping
136// the lower volume gives more headroom until clipping occurs
137// the selected volume is chosen to be similar to FMod's original volume
138static constexpr auto MaximumMusicVolume = 80;
139
140// higher than MaximumMusicVolume to compensate for lower maximum panning volume
141static constexpr auto MaximumSoundVolume = 100;
142
143C4AudioSystemSdl::C4AudioSystemSdl(const int maxChannels, const bool preferLinearResampling)
144{
145 assert(!instance);
146 instance = this;
147
148 auto logger = Application.LogSystem.CreateLoggerWithDifferentName(config&: Config.Logging.AudioSystem, name: "C4AudioSystem");
149
150 // Check SDL_mixer version
151 SDL_version compile_version;
152 MIX_VERSION(&compile_version);
153 const auto link_version = Mix_Linked_Version();
154 logger->info(fmt: "SDL_mixer runtime version is {}.{}.{} (compiled with {}.{}.{})",
155 args: link_version->major, args: link_version->minor, args: link_version->patch,
156 args&: compile_version.major, args&: compile_version.minor, args&: compile_version.patch);
157
158 // Try to enable linear resampling if requested
159 if (preferLinearResampling)
160 {
161 if (!SDL_SetHint(SDL_HINT_AUDIO_RESAMPLING_MODE, value: "linear"))
162 logger->error(msg: "SDL_SetHint(SDL_HINT_AUDIO_RESAMPLING_MODE, \"linear\") failed");
163 }
164
165 // Initialize SDL_mixer
166 StdSdlSubSystem system{SDL_INIT_AUDIO};
167 ThrowIfFailed(funcName: "Mix_OpenAudioDevice",
168 failed: Mix_OpenAudioDevice(frequency: Frequency, format: Format, channels: NumChannels, chunksize: 1024, device: nullptr, SDL_AUDIO_ALLOW_ANY_CHANGE & ~SDL_AUDIO_ALLOW_CHANNELS_CHANGE) != 0);
169
170 int frequency;
171 Uint16 format;
172 int channels;
173 Mix_QuerySpec(frequency: &frequency, format: &format, channels: &channels);
174
175 logger->debug(fmt: "SDL_mixer device spec: frequency = {} Hz, format = {}, channels = {}", args&: frequency, args&: format, args&: channels);
176
177 Mix_AllocateChannels(numchans: maxChannels);
178 Mix_ChannelFinished(channel_finished: ChannelFinished);
179 this->system.emplace(args: std::move(system));
180 playingChannels.resize(new_size: maxChannels);
181}
182
183C4AudioSystemSdl::~C4AudioSystemSdl() noexcept
184{
185 Mix_CloseAudio();
186 Mix_Quit();
187}
188
189void C4AudioSystemSdl::ThrowIfFailed(const char *const funcName, const bool failed, std::string_view errorMessage)
190{
191 if (failed)
192 {
193 if (errorMessage.empty())
194 {
195 errorMessage = Mix_GetError();
196 }
197
198 throw std::runtime_error{std::format(fmt: "SDL_mixer: {} failed: {}", args: funcName, args&: errorMessage)};
199 }
200}
201
202void C4AudioSystemSdl::FadeOutMusic(const std::int32_t ms)
203{
204 ThrowIfFailed(funcName: "Mix_FadeOutMusic", failed: Mix_FadeOutMusic(ms) != 1);
205}
206
207bool C4AudioSystemSdl::IsMusicPlaying() const
208{
209 return Mix_PlayingMusic() == 1;
210}
211
212void C4AudioSystemSdl::PlayMusic(const C4AudioSystem::MusicFile *const music, const bool loop)
213{
214 ThrowIfFailed(funcName: "Mix_PlayMusic", failed: Mix_PlayMusic(music: static_cast<const MusicFileSdl *const>(music)->sample.get(), loops: (loop ? -1 : 1)) == -1);
215}
216
217void C4AudioSystemSdl::SetMusicVolume(const float volume)
218{
219 Mix_VolumeMusic(volume: std::lrint(x: volume * MaximumMusicVolume));
220}
221
222void C4AudioSystemSdl::StopMusic()
223{
224 Mix_HaltMusic();
225}
226
227void C4AudioSystemSdl::UnpauseMusic() { /* Not supported */ }
228
229template <typename T>
230T *C4AudioSystemSdl::LoadSampleCheckMpegLayer3Header(const SampleLoadFunc<T> loadFunc, const char *const funcName, const void *const buf, const std::size_t size)
231{
232 const auto direct = loadFunc(SDL_RWFromConstMem(mem: buf, size), SDL_TRUE);
233 if (direct)
234 {
235 return direct;
236 }
237 const std::string error{Mix_GetError()};
238
239 // According to http://www.idea2ic.com/File_Formats/MPEG%20Audio%20Frame%20Header.pdf
240 // Maximum possible frame size = 144 * max bit rate / min sample rate + padding
241 // chosen values are limited to layer 3
242 static constexpr std::size_t MaxFrameSize{144 * 320'000 / 8'000 + 1};
243
244 const std::span data{reinterpret_cast<const std::byte *>(buf), size};
245 const std::size_t limit{std::min(a: data.size(), b: MaxFrameSize)};
246
247 for (std::size_t i{0}; i < limit - 4; ++i)
248 {
249 // first 8 of 11 frame sync bits
250 if (data[i] != std::byte{0xFF}) continue;
251
252 const auto byte2 = data[i + 1];
253 // rest of the sync bits + check for Layer 3 (SDL_mixer only accepts layer 3)
254 if ((byte2 & std::byte{0xE6}) != std::byte{0xE2}) continue;
255 // MPEG version bit value 01 is reserved
256 if ((byte2 & std::byte{0x18}) == std::byte{0x08}) continue;
257
258 const auto byte3 = data[i + 2];
259 // bitrate index 1111 is invalid
260 if ((byte3 & std::byte{0xF0}) == std::byte{0xF0}) continue;
261 // sampling rate index 11 is reserved
262 if ((byte3 & std::byte{0x0C}) == std::byte{0x0C}) continue;
263
264 const auto byte4 = data[i + 3];
265 // emphasis bit value 10 is reserved
266 if ((byte4 & std::byte{0x03}) == std::byte{0x02}) continue;
267
268 // at this point there seems to be a valid MPEG frame header
269 const auto sample = loadFunc(SDL_RWFromConstMem(mem: data.data() + i, size: size - i), SDL_TRUE);
270 if (sample)
271 {
272 return sample;
273 }
274 }
275
276 ThrowIfFailed(funcName, failed: true, errorMessage: error);
277 return nullptr;
278}
279
280C4AudioSystemSdl::MusicFileSdl::MusicFileSdl(const void *const buf, const std::size_t size)
281 : sample{LoadSampleCheckMpegLayer3Header(loadFunc: Mix_LoadMUS_RW, funcName: "Mix_LoadMUS_RW", buf, size)}
282{}
283
284C4AudioSystemSdl::SoundFileSdl::SoundFileSdl(const void *const buf, const std::size_t size)
285 : sample{LoadSampleCheckMpegLayer3Header(loadFunc: Mix_LoadWAV_RW, funcName: "Mix_LoadWAV_RW", buf, size)}
286{}
287
288std::uint32_t C4AudioSystemSdl::SoundFileSdl::GetDuration() const
289{
290 return 1000 * sample->alen / BytesPerSecond;
291}
292
293C4AudioSystemSdl::SoundChannelSdl::SoundChannelSdl(const SoundFileSdl *const sound, const bool loop)
294 : channel{Mix_PlayChannel(channel: -1, chunk: sound->sample.get(), loops: (loop ? -1 : 0))}
295{
296 ThrowIfFailed(funcName: "Mix_PlayChannel", failed: GetChannelId() == InvalidChannel);
297}
298
299C4AudioSystemSdl::SoundChannelSdl::~SoundChannelSdl()
300{
301 if (const auto ch = channel.load(m: std::memory_order::acquire); ch != InvalidChannel)
302 {
303 Mix_HaltChannel(channel: ch);
304 }
305}
306
307bool C4AudioSystemSdl::SoundChannelSdl::IsPlaying() const
308{
309 const auto ch = channel.load(m: std::memory_order::acquire);
310 return ch != InvalidChannel && Mix_Playing(channel: ch) == 1;
311}
312
313void C4AudioSystemSdl::SoundChannelSdl::SetPosition(const std::uint32_t ms)
314{
315 // Not supported
316}
317
318void C4AudioSystemSdl::SoundChannelSdl::SetVolumeAndPan(const float volume, const float pan)
319{
320 const auto ch = channel.load(m: std::memory_order::acquire);
321 if (ch == InvalidChannel) return;
322
323 Mix_Volume(channel: ch, volume: std::lrint(x: volume * MaximumSoundVolume));
324 const Uint8
325 left = static_cast<Uint8>(std::clamp(val: std::lrint(x: (1.0f - pan) * 192.0f), lo: 0L, hi: 192L)),
326 right = static_cast<Uint8>(std::clamp(val: std::lrint(x: (1.0f + pan) * 192.0f), lo: 0L, hi: 192L));
327 ThrowIfFailed(funcName: "Mix_SetPanning", failed: Mix_SetPanning(channel: ch, left, right) == 0);
328}
329
330void C4AudioSystemSdl::SoundChannelSdl::Unpause() { /* Not supported */ }
331
332void C4AudioSystemSdl::ChannelFinished(int channel)
333{
334 auto &soundInstance = instance->playingChannels[channel];
335 soundInstance->ClearChannelId();
336 soundInstance = nullptr;
337}
338
339C4AudioSystem *CreateC4AudioSystemSdl(int maxChannels, const bool preferLinearResampling)
340{
341 return new C4AudioSystemSdl{maxChannels, preferLinearResampling};
342}
343