| 1 | //============================================================================ |
| 2 | // |
| 3 | // SSSS tt lll lll |
| 4 | // SS SS tt ll ll |
| 5 | // SS tttttt eeee ll ll aaaa |
| 6 | // SSSS tt ee ee ll ll aa |
| 7 | // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" |
| 8 | // SS SS tt ee ll ll aa aa |
| 9 | // SSSS ttt eeeee llll llll aaaaa |
| 10 | // |
| 11 | // Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony |
| 12 | // and the Stella Team |
| 13 | // |
| 14 | // See the file "License.txt" for information on usage and redistribution of |
| 15 | // this file, and for a DISCLAIMER OF ALL WARRANTIES. |
| 16 | //============================================================================ |
| 17 | |
| 18 | #ifdef SOUND_SUPPORT |
| 19 | |
| 20 | #include <sstream> |
| 21 | #include <cassert> |
| 22 | #include <cmath> |
| 23 | |
| 24 | #include "SDL_lib.hxx" |
| 25 | #include "Logger.hxx" |
| 26 | #include "FrameBuffer.hxx" |
| 27 | #include "Settings.hxx" |
| 28 | #include "System.hxx" |
| 29 | #include "OSystem.hxx" |
| 30 | #include "Console.hxx" |
| 31 | #include "SoundSDL2.hxx" |
| 32 | #include "AudioQueue.hxx" |
| 33 | #include "EmulationTiming.hxx" |
| 34 | #include "AudioSettings.hxx" |
| 35 | #include "audio/SimpleResampler.hxx" |
| 36 | #include "audio/LanczosResampler.hxx" |
| 37 | #include "StaggeredLogger.hxx" |
| 38 | |
| 39 | #include "ThreadDebugging.hxx" |
| 40 | |
| 41 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 42 | SoundSDL2::SoundSDL2(OSystem& osystem, AudioSettings& audioSettings) |
| 43 | : Sound(osystem), |
| 44 | myIsInitializedFlag(false), |
| 45 | myVolume(100), |
| 46 | myVolumeFactor(0xffff), |
| 47 | myDevice(0), |
| 48 | myEmulationTiming(nullptr), |
| 49 | myCurrentFragment(nullptr), |
| 50 | myUnderrun(false), |
| 51 | myAudioSettings(audioSettings) |
| 52 | { |
| 53 | ASSERT_MAIN_THREAD; |
| 54 | |
| 55 | Logger::debug("SoundSDL2::SoundSDL2 started ..." ); |
| 56 | |
| 57 | if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { |
| 58 | ostringstream buf; |
| 59 | |
| 60 | buf << "WARNING: Failed to initialize SDL audio system! " << endl |
| 61 | << " " << SDL_GetError() << endl; |
| 62 | Logger::error(buf.str()); |
| 63 | return; |
| 64 | } |
| 65 | |
| 66 | SDL_zero(myHardwareSpec); |
| 67 | if(!openDevice()) |
| 68 | return; |
| 69 | |
| 70 | mute(true); |
| 71 | |
| 72 | Logger::debug("SoundSDL2::SoundSDL2 initialized" ); |
| 73 | } |
| 74 | |
| 75 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 76 | SoundSDL2::~SoundSDL2() |
| 77 | { |
| 78 | ASSERT_MAIN_THREAD; |
| 79 | |
| 80 | if (!myIsInitializedFlag) return; |
| 81 | |
| 82 | SDL_CloseAudioDevice(myDevice); |
| 83 | SDL_QuitSubSystem(SDL_INIT_AUDIO); |
| 84 | } |
| 85 | |
| 86 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 87 | bool SoundSDL2::openDevice() |
| 88 | { |
| 89 | ASSERT_MAIN_THREAD; |
| 90 | |
| 91 | SDL_AudioSpec desired; |
| 92 | desired.freq = myAudioSettings.sampleRate(); |
| 93 | desired.format = AUDIO_F32SYS; |
| 94 | desired.channels = 2; |
| 95 | desired.samples = static_cast<Uint16>(myAudioSettings.fragmentSize()); |
| 96 | desired.callback = callback; |
| 97 | desired.userdata = static_cast<void*>(this); |
| 98 | |
| 99 | if(myIsInitializedFlag) |
| 100 | SDL_CloseAudioDevice(myDevice); |
| 101 | myDevice = SDL_OpenAudioDevice(nullptr, 0, &desired, &myHardwareSpec, |
| 102 | SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); |
| 103 | |
| 104 | if(myDevice == 0) |
| 105 | { |
| 106 | ostringstream buf; |
| 107 | |
| 108 | buf << "WARNING: Couldn't open SDL audio device! " << endl |
| 109 | << " " << SDL_GetError() << endl; |
| 110 | Logger::error(buf.str()); |
| 111 | |
| 112 | return myIsInitializedFlag = false; |
| 113 | } |
| 114 | return myIsInitializedFlag = true; |
| 115 | } |
| 116 | |
| 117 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 118 | void SoundSDL2::setEnabled(bool state) |
| 119 | { |
| 120 | myAudioSettings.setEnabled(state); |
| 121 | if (myAudioQueue) myAudioQueue->ignoreOverflows(!state); |
| 122 | |
| 123 | Logger::debug(state ? "SoundSDL2::setEnabled(true)" : |
| 124 | "SoundSDL2::setEnabled(false)" ); |
| 125 | } |
| 126 | |
| 127 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 128 | void SoundSDL2::open(shared_ptr<AudioQueue> audioQueue, |
| 129 | EmulationTiming* emulationTiming) |
| 130 | { |
| 131 | string pre_about = myAboutString; |
| 132 | |
| 133 | // Do we need to re-open the sound device? |
| 134 | // Only do this when absolutely necessary |
| 135 | if(myAudioSettings.sampleRate() != uInt32(myHardwareSpec.freq) || |
| 136 | myAudioSettings.fragmentSize() != uInt32(myHardwareSpec.samples)) |
| 137 | openDevice(); |
| 138 | |
| 139 | myEmulationTiming = emulationTiming; |
| 140 | |
| 141 | Logger::debug("SoundSDL2::open started ..." ); |
| 142 | mute(true); |
| 143 | |
| 144 | audioQueue->ignoreOverflows(!myAudioSettings.enabled()); |
| 145 | if(!myAudioSettings.enabled()) |
| 146 | { |
| 147 | Logger::info("Sound disabled\n" ); |
| 148 | return; |
| 149 | } |
| 150 | |
| 151 | myAudioQueue = audioQueue; |
| 152 | myUnderrun = true; |
| 153 | myCurrentFragment = nullptr; |
| 154 | |
| 155 | // Adjust volume to that defined in settings |
| 156 | setVolume(myAudioSettings.volume()); |
| 157 | |
| 158 | initResampler(); |
| 159 | |
| 160 | // Show some info |
| 161 | myAboutString = about(); |
| 162 | if(myAboutString != pre_about) |
| 163 | Logger::info(myAboutString); |
| 164 | |
| 165 | // And start the SDL sound subsystem ... |
| 166 | mute(false); |
| 167 | |
| 168 | Logger::debug("SoundSDL2::open finished" ); |
| 169 | } |
| 170 | |
| 171 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 172 | void SoundSDL2::close() |
| 173 | { |
| 174 | if(!myIsInitializedFlag) return; |
| 175 | |
| 176 | mute(true); |
| 177 | |
| 178 | if (myAudioQueue) myAudioQueue->closeSink(myCurrentFragment); |
| 179 | myAudioQueue.reset(); |
| 180 | myCurrentFragment = nullptr; |
| 181 | } |
| 182 | |
| 183 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 184 | bool SoundSDL2::mute(bool state) |
| 185 | { |
| 186 | bool oldstate = SDL_GetAudioDeviceStatus(myDevice) == SDL_AUDIO_PAUSED; |
| 187 | if(myIsInitializedFlag) |
| 188 | SDL_PauseAudioDevice(myDevice, state ? 1 : 0); |
| 189 | |
| 190 | return oldstate; |
| 191 | } |
| 192 | |
| 193 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 194 | bool SoundSDL2::toggleMute() |
| 195 | { |
| 196 | bool enabled = myAudioSettings.enabled(); |
| 197 | |
| 198 | setEnabled(!enabled); |
| 199 | myOSystem.console().initializeAudio(); |
| 200 | |
| 201 | string message = "Sound " ; |
| 202 | message += !enabled ? "unmuted" : "muted" ; |
| 203 | |
| 204 | myOSystem.frameBuffer().showMessage(message); |
| 205 | |
| 206 | return enabled; |
| 207 | } |
| 208 | |
| 209 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 210 | void SoundSDL2::setVolume(uInt32 percent) |
| 211 | { |
| 212 | if(myIsInitializedFlag && (percent <= 100)) |
| 213 | { |
| 214 | myAudioSettings.setVolume(percent); |
| 215 | myVolume = percent; |
| 216 | |
| 217 | SDL_LockAudioDevice(myDevice); |
| 218 | myVolumeFactor = static_cast<float>(percent) / 100.f; |
| 219 | SDL_UnlockAudioDevice(myDevice); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 224 | void SoundSDL2::adjustVolume(Int8 direction) |
| 225 | { |
| 226 | ostringstream strval; |
| 227 | string message; |
| 228 | |
| 229 | Int32 percent = myVolume; |
| 230 | |
| 231 | if(direction == -1) |
| 232 | percent -= 2; |
| 233 | else if(direction == 1) |
| 234 | percent += 2; |
| 235 | |
| 236 | if((percent < 0) || (percent > 100)) |
| 237 | return; |
| 238 | |
| 239 | setVolume(percent); |
| 240 | |
| 241 | // enabled audio if it is currently disabled |
| 242 | bool enabled = myAudioSettings.enabled(); |
| 243 | |
| 244 | if (percent > 0 && !enabled) |
| 245 | { |
| 246 | setEnabled(!enabled); |
| 247 | myOSystem.console().initializeAudio(); |
| 248 | } |
| 249 | |
| 250 | // Now show an onscreen message |
| 251 | strval << percent; |
| 252 | message = "Volume set to " ; |
| 253 | message += strval.str(); |
| 254 | |
| 255 | myOSystem.frameBuffer().showMessage(message); |
| 256 | } |
| 257 | |
| 258 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 259 | string SoundSDL2::about() const |
| 260 | { |
| 261 | ostringstream buf; |
| 262 | buf << "Sound enabled:" << endl |
| 263 | << " Volume: " << myVolume << "%" << endl |
| 264 | << " Channels: " << uInt32(myHardwareSpec.channels) |
| 265 | << (myAudioQueue->isStereo() ? " (Stereo)" : " (Mono)" ) << endl |
| 266 | << " Preset: " ; |
| 267 | switch (myAudioSettings.preset()) { |
| 268 | case AudioSettings::Preset::custom: |
| 269 | buf << "Custom" << endl; |
| 270 | break; |
| 271 | case AudioSettings::Preset::lowQualityMediumLag: |
| 272 | buf << "Low quality, medium lag" << endl; |
| 273 | break; |
| 274 | case AudioSettings::Preset::highQualityMediumLag: |
| 275 | buf << "High quality, medium lag" << endl; |
| 276 | break; |
| 277 | case AudioSettings::Preset::highQualityLowLag: |
| 278 | buf << "High quality, low lag" << endl; |
| 279 | break; |
| 280 | case AudioSettings::Preset::ultraQualityMinimalLag: |
| 281 | buf << "Ultra quality, minimal lag" << endl; |
| 282 | break; |
| 283 | } |
| 284 | buf << " Fragment size: " << uInt32(myHardwareSpec.samples) << " bytes" << endl |
| 285 | << " Sample rate: " << uInt32(myHardwareSpec.freq) << " Hz" << endl; |
| 286 | buf << " Resampling: " ; |
| 287 | switch(myAudioSettings.resamplingQuality()) |
| 288 | { |
| 289 | case AudioSettings::ResamplingQuality::nearestNeightbour: |
| 290 | buf << "Quality 1, nearest neighbor" << endl; |
| 291 | break; |
| 292 | case AudioSettings::ResamplingQuality::lanczos_2: |
| 293 | buf << "Quality 2, Lanczos (a = 2)" << endl; |
| 294 | break; |
| 295 | case AudioSettings::ResamplingQuality::lanczos_3: |
| 296 | buf << "Quality 3, Lanczos (a = 3)" << endl; |
| 297 | break; |
| 298 | } |
| 299 | buf << " Headroom: " << std::fixed << std::setprecision(1) |
| 300 | << (0.5 * myAudioSettings.headroom()) << " frames" << endl |
| 301 | << " Buffer size: " << std::fixed << std::setprecision(1) |
| 302 | << (0.5 * myAudioSettings.bufferSize()) << " frames" << endl; |
| 303 | return buf.str(); |
| 304 | } |
| 305 | |
| 306 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 307 | void SoundSDL2::processFragment(float* stream, uInt32 length) |
| 308 | { |
| 309 | myResampler->fillFragment(stream, length); |
| 310 | |
| 311 | for (uInt32 i = 0; i < length; i++) stream[i] = stream[i] * myVolumeFactor; |
| 312 | } |
| 313 | |
| 314 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 315 | void SoundSDL2::initResampler() |
| 316 | { |
| 317 | Resampler::NextFragmentCallback nextFragmentCallback = [this] () -> Int16* { |
| 318 | Int16* nextFragment = nullptr; |
| 319 | |
| 320 | if (myUnderrun) |
| 321 | nextFragment = myAudioQueue->size() >= myEmulationTiming->prebufferFragmentCount() ? |
| 322 | myAudioQueue->dequeue(myCurrentFragment) : nullptr; |
| 323 | else |
| 324 | nextFragment = myAudioQueue->dequeue(myCurrentFragment); |
| 325 | |
| 326 | myUnderrun = nextFragment == nullptr; |
| 327 | if (nextFragment) myCurrentFragment = nextFragment; |
| 328 | |
| 329 | return nextFragment; |
| 330 | }; |
| 331 | |
| 332 | Resampler::Format formatFrom = |
| 333 | Resampler::Format(myEmulationTiming->audioSampleRate(), myAudioQueue->fragmentSize(), myAudioQueue->isStereo()); |
| 334 | Resampler::Format formatTo = |
| 335 | Resampler::Format(myHardwareSpec.freq, myHardwareSpec.samples, myHardwareSpec.channels > 1); |
| 336 | |
| 337 | switch (myAudioSettings.resamplingQuality()) { |
| 338 | case AudioSettings::ResamplingQuality::nearestNeightbour: |
| 339 | myResampler = make_unique<SimpleResampler>(formatFrom, formatTo, nextFragmentCallback); |
| 340 | break; |
| 341 | |
| 342 | case AudioSettings::ResamplingQuality::lanczos_2: |
| 343 | myResampler = make_unique<LanczosResampler>(formatFrom, formatTo, nextFragmentCallback, 2); |
| 344 | break; |
| 345 | |
| 346 | case AudioSettings::ResamplingQuality::lanczos_3: |
| 347 | myResampler = make_unique<LanczosResampler>(formatFrom, formatTo, nextFragmentCallback, 3); |
| 348 | break; |
| 349 | |
| 350 | default: |
| 351 | throw runtime_error("invalid resampling quality" ); |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 356 | void SoundSDL2::callback(void* udata, uInt8* stream, int len) |
| 357 | { |
| 358 | SoundSDL2* self = static_cast<SoundSDL2*>(udata); |
| 359 | |
| 360 | if (self->myAudioQueue) |
| 361 | self->processFragment(reinterpret_cast<float*>(stream), len >> 2); |
| 362 | else |
| 363 | SDL_memset(stream, 0, len); |
| 364 | } |
| 365 | |
| 366 | #endif // SOUND_SUPPORT |
| 367 | |