/** ** Supermodel ** A Sega Model 3 Arcade Emulator. ** Copyright 2011 Bart Trzynadlowski, Nik Henson ** ** This file is part of Supermodel. ** ** Supermodel is free software: you can redistribute it and/or modify it under ** the terms of the GNU General Public License as published by the Free ** Software Foundation, either version 3 of the License, or (at your option) ** any later version. ** ** Supermodel is distributed in the hope that it will be useful, but WITHOUT ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ** more details. ** ** You should have received a copy of the GNU General Public License along ** with Supermodel. If not, see . **/ /* * Audio.cpp * * SDL audio playback. Implements the OSD audio interface. * * Buffer sizes and read/write positions must be sample-aligned. A sample is * defined to encompass both channels so for, e.g., 16-bit audio as used here, * a sample is 4 bytes. Static assertions are employed to ensure that the * initial set up of the buffer is correct. * * Model 3 Audio is always 4 channels. SCSP1 is usually for each front * channels (on CN8 connector) and SCSP2 for rear channels (on CN7). * The downmix to 2 channels will be performed here in case supermodel audio * subsystem does not allow such playback. * The default DSB board is supposed to be plug and mixed with the front output * channel. The rear channel is usually plugged to the gullbow speakers that * are present in most model 3 racing cabinets, while front speakers are only * present in Daytona2, Scud Race, Sega Rally 2. * As each cabinet as its own wiring, with different mixing, the games.xml * database will provide which can of mixing should be applied for each game. * * From twin uk cabinet diagrams, at least: * - lemans24: only stereo on rear speakers (gullbox) on front channels SCSP1/CN8. * - scud race: DSB mixed with SCSP2/CN7 for rear (gullbox) speakers, front * connected to SCSP1/CN8. * - daytona2: front on SCSP1/CN8, rear on SCSP2/CN7 * - srally2: SCSP2/CN7 gives left/right, and SCSP1/CN8 is split in 2 channels: * mono front, mono rear. These two channels are them mixed with L/R to give * the quad output. * - oceanhun: SCSP1/CN8 and SCSP2/CN7 are mixed together for stereo output. * Others are unknown so far, but it is expected that most Step 2.x games should * have quad output. */ #include "Audio.h" #include "Supermodel.h" #include "SDLIncludes.h" #include #include // Model3 audio output is 44.1KHz 4-channel sound and frame rate is 60fps #define SAMPLE_RATE_M3 (44100) #define SUPERMODEL_FPS (60.0f) #define MODEL3_FPS (57.53f) #define MAX_SND_FREQ (75) #define MIN_SND_FREQ (45) #define MAX_LATENCY (100) #define NUM_CHANNELS_M3 (4) Game::AudioTypes AudioType; int nbHostAudioChannels = NUM_CHANNELS_M3; // Number of channels on host #define SAMPLES_PER_FRAME_M3 (INT32)(SAMPLE_RATE_M3 / MODEL3_FPS) #define BYTES_PER_SAMPLE_M3 (NUM_CHANNELS_M3 * sizeof(INT16)) #define BYTES_PER_FRAME_M3 (SAMPLES_PER_FRAME_M3 * BYTES_PER_SAMPLE_M3) static int samples_per_frame_host = SAMPLES_PER_FRAME_M3; static int bytes_per_sample_host = BYTES_PER_SAMPLE_M3; static int bytes_per_frame_host = BYTES_PER_FRAME_M3; // Balance percents for mixer float BalanceLeftRight = 0; // 0 mid balance, 100: left only, -100:right only float BalanceFrontRear = 0; // 0 mid balance, 100: front only, -100:right only // Mixer factor (depends on values above) float balanceFactorFrontLeft = 1.0f; float balanceFactorFrontRight = 1.0f; float balanceFactorRearLeft = 1.0f; float balanceFactorRearRight = 1.0f; static bool enabled = true; // True if sound output is enabled static constexpr unsigned latency = 20; // Audio latency to use (ie size of audio buffer) as percentage of max buffer size static constexpr bool underRunLoop = true; // True if should loop back to beginning of buffer on under-run, otherwise sound is just skipped static constexpr unsigned playSamples = 512; // Size (in samples) of callback play buffer static UINT32 audioBufferSize = 0; // Size (in bytes) of audio buffer static INT8* audioBuffer = NULL; // Audio buffer static UINT32 writePos = 0; // Current position at which writing into buffer static UINT32 playPos = 0; // Current position at which playing data in buffer via callback static bool writeWrapped = false; // True if write position has wrapped around at end of buffer but play position has not done so yet static unsigned underRuns = 0; // Number of buffer under-runs that have occured static unsigned overRuns = 0; // Number of buffer over-runs that have occured static AudioCallbackFPtr callback = NULL; // Pointer to audio callback that is called when audio buffer is less than half empty static void* callbackData = NULL; // Pointer to data to be passed to audio callback when it is called static const Util::Config::Node* s_config = 0; void SetAudioCallback(AudioCallbackFPtr newCallback, void* newData) { // Lock audio whilst changing callback pointers SDL_LockAudio(); callback = newCallback; callbackData = newData; SDL_UnlockAudio(); } void SetAudioEnabled(bool newEnabled) { enabled = newEnabled; } /// /// Set game audio mixing type /// /// void SetAudioType(Game::AudioTypes type) { AudioType = type; } static INT16 MixINT16(float x, float y) { INT32 sum = (INT32)((x + y)*0.5f); //!! dither if (sum > INT16_MAX) { sum = INT16_MAX; } if (sum < INT16_MIN) { sum = INT16_MIN; } return (INT16)sum; } static float MixFloat(float x, float y) { return (x + y)*0.5f; } static INT16 ClampINT16(float x) { INT32 xi = (INT32)x; //!! dither if (xi > INT16_MAX) { xi = INT16_MAX; } if (xi < INT16_MIN) { xi = INT16_MIN; } return (INT16)xi; } static void PlayCallback(void* data, Uint8* stream, int len) { //printf("PlayCallback(%d) [writePos = %u, writeWrapped = %s, playPos = %u, audioBufferSize = %u]\n", // len, writePos, (writeWrapped ? "true" : "false"), playPos, audioBufferSize); // Get current write position and adjust it if write has wrapped but play position has not UINT32 adjWritePos = writePos; if (writeWrapped) adjWritePos += audioBufferSize; // Check if play position overlaps write position (ie buffer under-run) if (playPos + len > adjWritePos) { underRuns++; //printf("Audio buffer under-run #%u in PlayCallback(%d) [writePos = %u, writeWrapped = %s, playPos = %u, audioBufferSize = %u]\n", // underRuns, len, writePos, (writeWrapped ? "true" : "false"), playPos, audioBufferSize); // See what action to take on under-run if (underRunLoop) { // If loop, then move play position back to beginning of data in buffer playPos = adjWritePos + bytes_per_frame_host; // Check if play position has moved past end of buffer if (playPos >= audioBufferSize) // If so, wrap it around to beginning again (but keep write wrapped flag as before) playPos -= audioBufferSize; else // Otherwise, set write wrapped flag as will now appear as if write has wrapped but play position has not writeWrapped = true; } else { // Otherwise, just copy silence to audio output stream and exit memset(stream, 0, len); return; } } INT8* src1; INT8* src2; UINT32 len1; UINT32 len2; // Check if play region extends past end of buffer if (playPos + len > audioBufferSize) { // If so, split play region into two src1 = audioBuffer + playPos; src2 = audioBuffer; len1 = audioBufferSize - playPos; len2 = len - len1; } else { // Otherwise, just copy whole region src1 = audioBuffer + playPos; src2 = 0; len1 = len; len2 = 0; } // Check if audio is enabled if (enabled) { // If so, copy play region into audio output stream memcpy(stream, src1, len1); // Also, if not looping on under-runs then blank region out if (!underRunLoop) memset(src1, 0, len1); if (len2) { // If region was split into two, copy second half into audio output stream as well memcpy(stream + len1, src2, len2); // Also, if not looping on under-runs then blank region out if (!underRunLoop) memset(src2, 0, len2); } } else // Otherwise, just copy silence to audio output stream memset(stream, 0, len); // Move play position forward for next time playPos += len; bool bufferFull = adjWritePos + 2 * bytes_per_frame_host > playPos + audioBufferSize; // Check if play position has moved past end of buffer if (playPos >= audioBufferSize) { // If so, wrap it around to beginning again and reset write wrapped flag playPos -= audioBufferSize; writeWrapped = false; } // If buffer is not full then call audio callback if (callback && !bufferFull) callback(callbackData); } static void MixChannels(unsigned numSamples, const float* leftFrontBuffer, const float* rightFrontBuffer, const float* leftRearBuffer, const float* rightRearBuffer, void* dest, bool flipStereo) { INT16* p = (INT16*)dest; if (nbHostAudioChannels == 1) { for (unsigned i = 0; i < numSamples; i++) { INT16 monovalue = MixINT16( MixFloat(leftFrontBuffer[i] * balanceFactorFrontLeft,rightFrontBuffer[i] * balanceFactorFrontRight), MixFloat(leftRearBuffer[i] * balanceFactorRearLeft, rightRearBuffer[i] * balanceFactorRearRight)); *p++ = monovalue; } } else { // Flip again left/right if configured in audio switch (AudioType) { case Game::STEREO_RL: case Game::QUAD_1_FRL_2_RRL: case Game::QUAD_1_RRL_2_FRL: flipStereo = !flipStereo; break; } // Now order channels according to audio type if (nbHostAudioChannels == 2) { for (unsigned i = 0; i < numSamples; i++) { INT16 leftvalue = MixINT16(leftFrontBuffer[i] * balanceFactorFrontLeft, leftRearBuffer[i] * balanceFactorRearLeft); INT16 rightvalue = MixINT16(rightFrontBuffer[i]*balanceFactorFrontRight, rightRearBuffer[i]*balanceFactorRearRight); if (flipStereo) // swap left and right channels { *p++ = rightvalue; *p++ = leftvalue; } else { *p++ = leftvalue; *p++ = rightvalue; } } } else if (nbHostAudioChannels == 4) { for (unsigned i = 0; i < numSamples; i++) { float frontLeftValue = leftFrontBuffer[i]*balanceFactorFrontLeft; float frontRightValue = rightFrontBuffer[i]*balanceFactorFrontRight; float rearLeftValue = leftRearBuffer[i]*balanceFactorRearLeft; float rearRightValue = rightRearBuffer[i]*balanceFactorRearRight; // Check game audio type switch (AudioType) { case Game::MONO: { INT16 monovalue = MixINT16(MixFloat(frontLeftValue, frontRightValue), MixFloat(rearLeftValue, rearRightValue)); *p++ = monovalue; *p++ = monovalue; *p++ = monovalue; *p++ = monovalue; } break; case Game::STEREO_LR: case Game::STEREO_RL: { INT16 leftvalue = MixINT16(frontLeftValue, frontRightValue); INT16 rightvalue = MixINT16(rearLeftValue, rearRightValue); if (flipStereo) // swap left and right channels { *p++ = rightvalue; *p++ = leftvalue; *p++ = rightvalue; *p++ = leftvalue; } else { *p++ = leftvalue; *p++ = rightvalue; *p++ = leftvalue; *p++ = rightvalue; } } break; case Game::QUAD_1_FLR_2_RLR: case Game::QUAD_1_FRL_2_RRL: { // Normal channels Front Left/Right then Rear Left/Right if (flipStereo) // swap left and right channels { *p++ = ClampINT16(frontRightValue); *p++ = ClampINT16(frontLeftValue); *p++ = ClampINT16(rearRightValue); *p++ = ClampINT16(rearLeftValue); } else { *p++ = ClampINT16(frontLeftValue); *p++ = ClampINT16(frontRightValue); *p++ = ClampINT16(rearLeftValue); *p++ = ClampINT16(rearRightValue); } } break; case Game::QUAD_1_RLR_2_FLR: case Game::QUAD_1_RRL_2_FRL: // Reversed channels Front/Rear Left then Front/Rear Right if (flipStereo) // swap left and right channels { *p++ = ClampINT16(rearRightValue); *p++ = ClampINT16(rearLeftValue); *p++ = ClampINT16(frontRightValue); *p++ = ClampINT16(frontLeftValue); } else { *p++ = ClampINT16(rearLeftValue); *p++ = ClampINT16(rearRightValue); *p++ = ClampINT16(frontLeftValue); *p++ = ClampINT16(frontRightValue); } break; case Game::QUAD_1_LR_2_FR_MIX: // Split mix: one goes to left/right, other front/rear (mono) // =>Remix all! INT16 newfrontLeftValue = MixINT16(frontLeftValue, rearLeftValue); INT16 newfrontRightValue = MixINT16(frontLeftValue, rearRightValue); INT16 newrearLeftValue = MixINT16(frontRightValue, rearLeftValue); INT16 newrearRightValue = MixINT16(frontRightValue, rearRightValue); if (flipStereo) // swap left and right channels { *p++ = newfrontRightValue; *p++ = newfrontLeftValue; *p++ = newrearRightValue; *p++ = newrearLeftValue; } else { *p++ = newfrontLeftValue; *p++ = newfrontRightValue; *p++ = newrearLeftValue; *p++ = newrearRightValue; } break; } } } } } /* static void LogAudioInfo(SDL_AudioSpec *fmt) { InfoLog("Audio device information:"); InfoLog(" Frequency: %d", fmt->freq); InfoLog(" Channels: %d", fmt->channels); InfoLog("Sample Format: %d", fmt->format); InfoLog(""); } */ /// /// Prepare audio subsystem on host. /// The requested channels is deduced, and SDL will make sure it is compatible with this. /// /// /// bool OpenAudio(const Util::Config::Node& config) { s_config = &config; // Initialize SDL audio sub-system if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) return ErrorLog("Unable to initialize SDL audio sub-system: %s\n", SDL_GetError()); // Number of channels requested in config (default is 4) nbHostAudioChannels = (int)s_config->Get("NbSoundChannels").ValueAs(); // If game is only stereo or mono, enforce host to reduce number of channels switch (AudioType) { case Game::MONO: nbHostAudioChannels = std::min(nbHostAudioChannels, 1); break; case Game::STEREO_LR: case Game::STEREO_RL: nbHostAudioChannels = std::min(nbHostAudioChannels, 2); break; } // Mixer Balance float balancelr = std::max(-100.f, std::min(100.f, s_config->Get("BalanceLeftRight").ValueAs())); balancelr *= 0.01f; BalanceLeftRight = balancelr; float balancefr = std::max(-100.f, std::min(100.f, s_config->Get("BalanceFrontRear").ValueAs())); balancefr *= 0.01f; BalanceFrontRear = balancefr; balanceFactorFrontLeft = (BalanceLeftRight < 0.f ? 1.f + BalanceLeftRight : 1.f) * (BalanceFrontRear < 0 ? 1.f + BalanceFrontRear : 1.f); balanceFactorFrontRight = (BalanceLeftRight > 0.f ? 1.f - BalanceLeftRight : 1.f) * (BalanceFrontRear < 0 ? 1.f + BalanceFrontRear : 1.f); balanceFactorRearLeft = (BalanceLeftRight < 0.f ? 1.f + BalanceLeftRight : 1.f) * (BalanceFrontRear > 0 ? 1.f - BalanceFrontRear : 1.f); balanceFactorRearRight = (BalanceLeftRight > 0.f ? 1.f - BalanceLeftRight : 1.f) * (BalanceFrontRear > 0 ? 1.f - BalanceFrontRear : 1.f); // Set up audio specification SDL_AudioSpec desired{}; desired.freq = SAMPLE_RATE_M3; // Number of host channels to use (choice limited to 1,2,4) desired.channels = nbHostAudioChannels; desired.format = AUDIO_S16SYS; desired.samples = playSamples; desired.callback = PlayCallback; // Now force SDL to use the format we requested (nullptr); it will convert if necessary if (SDL_OpenAudio(&desired, nullptr) < 0) { if (desired.channels==2) { return ErrorLog("Unable to open 44.1KHz 2-channel audio with SDL: %s\n", SDL_GetError()); } else if (desired.channels==4) { return ErrorLog("Unable to open 44.1KHz 4-channel audio with SDL: %s\n", SDL_GetError()); } else { return ErrorLog("Unable to open 44.1KHz channel audio with SDL: %s\n", SDL_GetError()); } } float soundFreq_Hz = (float)s_config->Get("SoundFreq").ValueAs(); if (soundFreq_Hz>MAX_SND_FREQ) soundFreq_Hz = MAX_SND_FREQ; if (soundFreq_Hz(minBufferSize, audioBufferSize); audioBuffer = new(std::nothrow) INT8[audioBufferSize]; if (audioBuffer == NULL) { float audioBufMB = (float)audioBufferSize / (float)0x100000; return ErrorLog("Insufficient memory for audio latency buffer (need %1.1f MB).", audioBufMB); } memset(audioBuffer, 0, sizeof(INT8) * audioBufferSize); // Set initial play position to be beginning of buffer and initial write position to be half-way into buffer playPos = 0; uint32_t endOfBuffer = bufferSize - bytes_per_frame_host; uint32_t midpointAfterFirstFrameUnaligned = bytes_per_frame_host + (bufferSize - bytes_per_frame_host) / 2; uint32_t extraPaddingNeeded = (bytes_per_frame_host - midpointAfterFirstFrameUnaligned % bytes_per_frame_host) % bytes_per_frame_host; uint32_t midpointAfterFirstFrame = midpointAfterFirstFrameUnaligned + extraPaddingNeeded; if (!((endOfBuffer % (nbHostAudioChannels*sizeof(INT16))) == 0)) { return ErrorLog("must be an integer multiple of the sample size\n"); } if (!((midpointAfterFirstFrame % nbHostAudioChannels*sizeof(INT16)) == 0)) { return ErrorLog("must be an integer multiple of the sample size\n"); } writePos = std::min(endOfBuffer, midpointAfterFirstFrame); writeWrapped = false; // Reset counters underRuns = 0; overRuns = 0; // Start audio playing SDL_PauseAudio(0); return OKAY; } bool OutputAudio(unsigned numSamples, const float* leftFrontBuffer, const float* rightFrontBuffer, const float* leftRearBuffer, const float* rightRearBuffer, bool flipStereo) { //printf("OutputAudio(%u) [writePos = %u, writeWrapped = %s, playPos = %u, audioBufferSize = %u]\n", // numSamples, writePos, (writeWrapped ? "true" : "false"), playPos, audioBufferSize); UINT32 bytesRemaining; UINT32 bytesToCopy; INT16* src; // Number of samples should never be more than max number of samples per frame if (numSamples > (unsigned)samples_per_frame_host) numSamples = samples_per_frame_host; // Mix together left and right channels into single chunk of data INT16 mixBuffer[NUM_CHANNELS_M3 * (SAMPLE_RATE_M3 / MIN_SND_FREQ)]; MixChannels(numSamples, leftFrontBuffer, rightFrontBuffer, leftRearBuffer, rightRearBuffer, mixBuffer, flipStereo); // Lock SDL audio callback so that it doesn't interfere with following code SDL_LockAudio(); // Calculate number of bytes for current sound chunk UINT32 numBytes = numSamples * bytes_per_sample_host; // Get end of current play region (writing must occur past this point) UINT32 playEndPos = playPos + bytes_per_frame_host; // Undo any wrap-around of the write position that may have occurred to create following ordering: playPos < playEndPos < writePos if (playEndPos > writePos && writeWrapped) writePos += audioBufferSize; // Check if play region has caught up with write position and now overlaps it (ie buffer under-run) if (playEndPos > writePos) { underRuns++; //printf("Audio buffer under-run #%u in OutputAudio(%u) [writePos = %u, writeWrapped = %s, playPos = %u, audioBufferSize = %u, numBytes = %u]\n", // underRuns, numSamples, writePos, (writeWrapped ? "true" : "false"), playPos, audioBufferSize, numBytes); // See what action to take on under-run if (underRunLoop) { // If loop, then move play position back to beginning of data in buffer playPos = writePos + numBytes + bytes_per_frame_host; // Check if play position has moved past end of buffer if (playPos >= audioBufferSize) // If so, wrap it around to beginning again (but keep write wrapped flag as before) playPos -= audioBufferSize; else { // Otherwise, set write wrapped flag as will now appear as if write has wrapped but play position has not writeWrapped = true; writePos += audioBufferSize; } } else { // Otherwise, bump write position forward in chunks until it is past end of play region do { writePos += numBytes; } while (playEndPos > writePos); } } // Check if write position has caught up with play region and now overlaps it (ie buffer over-run) bool overRun = writePos + numBytes > playPos + audioBufferSize; bool bufferFull = writePos + 2 * bytes_per_frame_host > playPos + audioBufferSize; // Move write position back to within buffer if (writePos >= audioBufferSize) writePos -= audioBufferSize; // Handle buffer over-run if (overRun) { overRuns++; //printf("Audio buffer over-run #%u in OutputAudio(%u) [writePos = %u, writeWrapped = %s, playPos = %u, audioBufferSize = %u, numBytes = %u]\n", // overRuns, numSamples, writePos, (writeWrapped ? "true" : "false"), playPos, audioBufferSize, numBytes); bufferFull = true; // Discard current chunk of data } else { src = mixBuffer; INT8* dst1; INT8* dst2; UINT32 len1; UINT32 len2; // Check if write region extends past end of buffer if (writePos + numBytes > audioBufferSize) { // If so, split write region into two dst1 = audioBuffer + writePos; dst2 = audioBuffer; len1 = audioBufferSize - writePos; len2 = numBytes - len1; } else { // Otherwise, just copy whole region dst1 = audioBuffer + writePos; dst2 = NULL; len1 = numBytes; len2 = NULL; } // Copy chunk to write position in buffer bytesRemaining = numBytes; bytesToCopy = (bytesRemaining > len1 ? len1 : bytesRemaining); memcpy(dst1, src, bytesToCopy); // Adjust for number of bytes copied bytesRemaining -= bytesToCopy; src = (INT16*)((UINT8*)src + bytesToCopy); if (bytesRemaining) { // If write region was split into two, copy second half of chunk into buffer as well bytesToCopy = (bytesRemaining > len2 ? len2 : bytesRemaining); memcpy(dst2, src, bytesToCopy); } // Move write position forward for next time writePos += numBytes; // Check if write position has moved past end of buffer if (writePos >= audioBufferSize) { // If so, wrap it around to beginning again and set write wrapped flag writePos -= audioBufferSize; writeWrapped = true; } } // Unlock SDL audio callback SDL_UnlockAudio(); // Return whether buffer is half full return bufferFull; } void CloseAudio() { // Close SDL audio output SDL_CloseAudio(); // Delete audio buffer if (audioBuffer != NULL) { delete[] audioBuffer; audioBuffer = NULL; } }