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