2011-09-14 19:08:43 +00:00
/**
* * 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
2020-07-27 10:28:48 +00:00
* * the terms of the GNU General Public License as published by the Free
2011-09-14 19:08:43 +00:00
* * 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 < http : //www.gnu.org/licenses/>.
* */
2020-07-27 10:28:48 +00:00
2011-09-14 19:08:43 +00:00
/*
* Audio . cpp
2020-07-27 10:28:48 +00:00
*
2011-09-14 19:08:43 +00:00
* SDL audio playback . Implements the OSD audio interface .
2020-09-14 06:59:38 +00:00
*
* 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 .
2011-09-14 19:08:43 +00:00
*/
2021-11-22 17:15:06 +00:00
# include "Audio.h"
2011-08-01 19:12:44 +00:00
# include "Supermodel.h"
2020-07-27 10:28:48 +00:00
# include "SDLIncludes.h"
2011-07-20 21:14:00 +00:00
2011-09-14 19:08:43 +00:00
# include <cmath>
2016-03-22 11:34:32 +00:00
# include <algorithm>
2011-08-01 19:12:44 +00:00
// Model3 audio output is 44.1KHz 2-channel sound and frame rate is 60fps
# define SAMPLE_RATE 44100
# define NUM_CHANNELS 2
# define SUPERMODEL_FPS 60
# define BYTES_PER_SAMPLE (NUM_CHANNELS * sizeof(INT16))
# define SAMPLES_PER_FRAME (SAMPLE_RATE / SUPERMODEL_FPS)
2020-07-27 10:28:48 +00:00
# define BYTES_PER_FRAME (SAMPLES_PER_FRAME * BYTES_PER_SAMPLE)
2011-07-20 21:14:00 +00:00
2011-08-01 19:12:44 +00:00
# define MAX_LATENCY 100
2011-07-20 21:14:00 +00:00
static bool enabled = true ; // True if sound output is enabled
2020-09-14 06:59:38 +00:00
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
2011-07-20 21:14:00 +00:00
2020-09-14 06:59:38 +00:00
static constexpr unsigned playSamples = 512 ; // Size (in samples) of callback play buffer
2011-07-20 21:14:00 +00:00
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
2011-09-12 21:03:16 +00:00
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
2011-09-12 05:43:37 +00:00
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 ;
}
2011-07-20 21:14:00 +00:00
static void PlayCallback ( void * data , Uint8 * stream , int len )
{
2020-07-27 10:28:48 +00:00
//printf("PlayCallback(%d) [writePos = %u, writeWrapped = %s, playPos = %u, audioBufferSize = %u]\n",
2011-09-01 21:58:10 +00:00
// len, writePos, (writeWrapped ? "true" : "false"), playPos, audioBufferSize);
2020-07-27 10:28:48 +00:00
2011-08-07 23:09:18 +00:00
// Get current write position and adjust it if write has wrapped but play position has not
2011-07-20 21:14:00 +00:00
UINT32 adjWritePos = writePos ;
if ( writeWrapped )
adjWritePos + = audioBufferSize ;
// Check if play position overlaps write position (ie buffer under-run)
2011-08-07 23:09:18 +00:00
if ( playPos + len > adjWritePos )
2011-07-20 21:14:00 +00:00
{
underRuns + + ;
2011-08-07 23:09:18 +00:00
2011-09-01 21:58:10 +00:00
//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);
2020-07-27 10:28:48 +00:00
2011-08-07 23:09:18 +00:00
// 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 ;
// Check if play position has moved past end of buffer
if ( playPos > = audioBufferSize )
2011-08-31 22:30:45 +00:00
// If so, wrap it around to beginning again (but keep write wrapped flag as before)
2011-08-07 23:09:18 +00:00
playPos - = audioBufferSize ;
2020-07-27 10:28:48 +00:00
else
2011-08-07 23:09:18 +00:00
// 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 ;
}
2011-07-20 21:14:00 +00:00
}
2020-07-27 10:28:48 +00:00
2011-07-20 21:14:00 +00:00
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 )
{
2011-08-07 23:09:18 +00:00
// If so, copy play region into audio output stream
2011-07-20 21:14:00 +00:00
memcpy ( stream , src1 , len1 ) ;
2020-07-27 10:28:48 +00:00
2011-08-07 23:09:18 +00:00
// Also, if not looping on under-runs then blank region out
if ( ! underRunLoop )
memset ( src1 , 0 , len1 ) ;
2011-07-20 21:14:00 +00:00
if ( len2 )
{
2011-08-07 23:09:18 +00:00
// If region was split into two, copy second half into audio output stream as well
2011-07-20 21:14:00 +00:00
memcpy ( stream + len1 , src2 , len2 ) ;
2011-08-07 23:09:18 +00:00
// Also, if not looping on under-runs then blank region out
if ( ! underRunLoop )
memset ( src2 , 0 , len2 ) ;
2011-07-20 21:14:00 +00:00
}
}
else
// Otherwise, just copy silence to audio output stream
memset ( stream , 0 , len ) ;
// Move play position forward for next time
playPos + = len ;
2011-09-12 21:03:16 +00:00
bool bufferFull = adjWritePos + 2 * BYTES_PER_FRAME > playPos + audioBufferSize ;
2011-07-20 21:14:00 +00:00
// Check if play position has moved past end of buffer
if ( playPos > = audioBufferSize )
{
2011-08-07 23:09:18 +00:00
// If so, wrap it around to beginning again and reset write wrapped flag
2011-07-20 21:14:00 +00:00
playPos - = audioBufferSize ;
writeWrapped = false ;
}
2011-09-12 05:43:37 +00:00
2011-09-12 21:03:16 +00:00
// If buffer is not full then call audio callback
if ( callback & & ! bufferFull )
2011-09-12 05:43:37 +00:00
callback ( callbackData ) ;
2011-07-20 21:14:00 +00:00
}
2017-03-27 03:19:15 +00:00
static void MixChannels ( unsigned numSamples , INT16 * leftBuffer , INT16 * rightBuffer , void * dest , bool flipStereo )
2011-07-20 21:14:00 +00:00
{
INT16 * p = ( INT16 * ) dest ;
2020-07-27 10:28:48 +00:00
2011-07-20 21:14:00 +00:00
# if (NUM_CHANNELS == 1)
2011-09-01 06:43:01 +00:00
for ( unsigned i = 0 ; i < numSamples ; i + + )
2020-07-27 10:28:48 +00:00
* p + + = leftBuffer [ i ] + rightBuffer [ i ] ; // TODO: these should probably be clipped!
2011-07-20 21:14:00 +00:00
# else
2017-03-27 03:19:15 +00:00
if ( flipStereo ) // swap left and right channels
2011-09-01 06:43:01 +00:00
{
for ( unsigned i = 0 ; i < numSamples ; i + + )
{
2012-02-09 17:12:13 +00:00
* p + + = rightBuffer [ i ] ;
2020-09-14 06:59:38 +00:00
* p + + = leftBuffer [ i ] ;
2011-09-01 06:43:01 +00:00
}
}
2020-09-14 06:59:38 +00:00
else // correct stereo
2011-09-01 06:43:01 +00:00
{
for ( unsigned i = 0 ; i < numSamples ; i + + )
{
2012-02-09 17:12:13 +00:00
* p + + = leftBuffer [ i ] ;
2020-09-14 06:59:38 +00:00
* p + + = rightBuffer [ i ] ;
2011-09-01 06:43:01 +00:00
}
2011-07-20 21:14:00 +00:00
}
2011-09-01 06:43:01 +00:00
# endif // NUM_CHANNELS
}
2020-04-19 08:34:58 +00:00
/*
2011-09-01 06:43:01 +00:00
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 ( " " ) ;
2011-07-20 21:14:00 +00:00
}
2020-04-19 08:34:58 +00:00
*/
2011-07-20 21:14:00 +00:00
2011-09-08 06:34:18 +00:00
bool OpenAudio ( )
2011-07-20 21:14:00 +00:00
{
// Initialize SDL audio sub-system
if ( SDL_InitSubSystem ( SDL_INIT_AUDIO ) ! = 0 )
2011-08-07 23:09:18 +00:00
return ErrorLog ( " Unable to initialize SDL audio sub-system: %s \n " , SDL_GetError ( ) ) ;
2011-07-20 21:14:00 +00:00
// Set up audio specification
SDL_AudioSpec fmt ;
memset ( & fmt , 0 , sizeof ( SDL_AudioSpec ) ) ;
fmt . freq = SAMPLE_RATE ;
fmt . channels = NUM_CHANNELS ;
fmt . format = AUDIO_S16SYS ;
fmt . samples = playSamples ;
fmt . callback = PlayCallback ;
2020-07-27 10:28:48 +00:00
2020-04-19 08:34:58 +00:00
// Force SDL to use the format we requested; it will convert if necessary
if ( SDL_OpenAudio ( & fmt , nullptr ) < 0 )
2011-08-07 23:09:18 +00:00
return ErrorLog ( " Unable to open 44.1KHz 2-channel audio with SDL: %s \n " , SDL_GetError ( ) ) ;
2011-07-20 21:14:00 +00:00
// Create audio buffer
2020-09-15 08:51:23 +00:00
constexpr uint32_t bufferSize = SAMPLE_RATE * BYTES_PER_SAMPLE * latency / MAX_LATENCY ;
static_assert ( bufferSize % BYTES_PER_SAMPLE = = 0 , " must be an integer multiple of the sample size " ) ;
2020-09-14 06:59:38 +00:00
audioBufferSize = bufferSize ;
2011-09-12 21:03:16 +00:00
int minBufferSize = 3 * BYTES_PER_FRAME ;
2016-03-22 11:34:32 +00:00
audioBufferSize = std : : max < int > ( minBufferSize , audioBufferSize ) ;
2011-08-07 23:09:18 +00:00
audioBuffer = new ( std : : nothrow ) INT8 [ audioBufferSize ] ;
if ( audioBuffer = = NULL )
{
float audioBufMB = ( float ) audioBufferSize / ( float ) 0x100000 ;
2020-07-27 10:28:48 +00:00
return ErrorLog ( " Insufficient memory for audio latency buffer (need %1.1f MB) . " , audioBufMB) ;
2011-08-07 23:09:18 +00:00
}
2011-07-20 21:14:00 +00:00
memset ( audioBuffer , 0 , sizeof ( INT8 ) * audioBufferSize ) ;
2020-07-27 10:28:48 +00:00
2011-07-20 21:14:00 +00:00
// Set initial play position to be beginning of buffer and initial write position to be half-way into buffer
playPos = 0 ;
2020-09-15 08:51:23 +00:00
constexpr uint32_t endOfBuffer = bufferSize - BYTES_PER_FRAME ;
constexpr uint32_t midpointAfterFirstFrameUnaligned = BYTES_PER_FRAME + ( bufferSize - BYTES_PER_FRAME ) / 2 ;
constexpr uint32_t extraPaddingNeeded = ( BYTES_PER_SAMPLE - midpointAfterFirstFrameUnaligned % BYTES_PER_SAMPLE ) % BYTES_PER_SAMPLE ;
constexpr uint32_t midpointAfterFirstFrame = midpointAfterFirstFrameUnaligned + extraPaddingNeeded ;
static_assert ( endOfBuffer % BYTES_PER_SAMPLE = = 0 , " make sure we are aligned to a sample boundary otherwise underrun/overrun adjustment will end up shifting playback by one channel causing stereo to flip " ) ;
static_assert ( midpointAfterFirstFrame % BYTES_PER_SAMPLE = = 0 , " error " ) ;
2020-09-14 06:59:38 +00:00
writePos = std : : min < int > ( endOfBuffer , midpointAfterFirstFrame ) ;
2011-07-20 21:14:00 +00:00
writeWrapped = false ;
// Reset counters
underRuns = 0 ;
overRuns = 0 ;
// Start audio playing
SDL_PauseAudio ( 0 ) ;
return OKAY ;
}
2017-03-27 03:19:15 +00:00
bool OutputAudio ( unsigned numSamples , INT16 * leftBuffer , INT16 * rightBuffer , bool flipStereo )
2011-07-20 21:14:00 +00:00
{
2011-09-01 21:58:10 +00:00
//printf("OutputAudio(%u) [writePos = %u, writeWrapped = %s, playPos = %u, audioBufferSize = %u]\n",
// numSamples, writePos, (writeWrapped ? "true" : "false"), playPos, audioBufferSize);
2011-07-20 21:14:00 +00:00
2011-08-01 19:12:44 +00:00
UINT32 bytesRemaining ;
UINT32 bytesToCopy ;
INT16 * src ;
2011-07-20 21:14:00 +00:00
// Number of samples should never be more than max number of samples per frame
if ( numSamples > SAMPLES_PER_FRAME )
numSamples = SAMPLES_PER_FRAME ;
// Mix together left and right channels into single chunk of data
INT16 mixBuffer [ NUM_CHANNELS * SAMPLES_PER_FRAME ] ;
2017-03-27 03:19:15 +00:00
MixChannels ( numSamples , leftBuffer , rightBuffer , mixBuffer , flipStereo ) ;
2020-07-27 10:28:48 +00:00
2011-07-20 21:14:00 +00:00
// Lock SDL audio callback so that it doesn't interfere with following code
SDL_LockAudio ( ) ;
2020-07-27 10:28:48 +00:00
2011-07-20 21:14:00 +00:00
// Calculate number of bytes for current sound chunk
UINT32 numBytes = numSamples * BYTES_PER_SAMPLE ;
2020-07-27 10:28:48 +00:00
2011-07-20 21:14:00 +00:00
// Get end of current play region (writing must occur past this point)
UINT32 playEndPos = playPos + BYTES_PER_FRAME ;
2020-07-27 10:28:48 +00:00
2011-07-20 21:14:00 +00:00
// Undo any wrap-around of the write position that may have occured to create following ordering: playPos < playEndPos < writePos
2011-08-07 23:09:18 +00:00
if ( playEndPos > writePos & & writeWrapped )
2011-07-20 21:14:00 +00:00
writePos + = audioBufferSize ;
2011-08-07 23:09:18 +00:00
// Check if play region has caught up with write position and now overlaps it (ie buffer under-run)
if ( playEndPos > writePos )
2011-07-20 21:14:00 +00:00
{
underRuns + + ;
2011-08-07 23:09:18 +00:00
2011-09-01 21:58:10 +00:00
//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);
2020-07-27 10:28:48 +00:00
2011-08-07 23:09:18 +00:00
// 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 ;
2020-07-27 10:28:48 +00:00
2011-08-07 23:09:18 +00:00
// Check if play position has moved past end of buffer
if ( playPos > = audioBufferSize )
2011-08-31 22:30:45 +00:00
// If so, wrap it around to beginning again (but keep write wrapped flag as before)
2011-08-07 23:09:18 +00:00
playPos - = audioBufferSize ;
2020-07-27 10:28:48 +00:00
else
2011-08-07 23:09:18 +00:00
{
// 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 ) ;
}
2011-07-20 21:14:00 +00:00
}
2011-08-07 23:09:18 +00:00
// Check if write position has caught up with play region and now overlaps it (ie buffer over-run)
2011-09-01 21:58:10 +00:00
bool overRun = writePos + numBytes > playPos + audioBufferSize ;
2020-07-27 10:28:48 +00:00
2011-09-12 21:03:16 +00:00
bool bufferFull = writePos + 2 * BYTES_PER_FRAME > playPos + audioBufferSize ;
2011-09-12 05:43:37 +00:00
2011-09-01 21:58:10 +00:00
// Move write position back to within buffer
if ( writePos > = audioBufferSize )
writePos - = audioBufferSize ;
// Handle buffer over-run
if ( overRun )
2011-07-20 21:14:00 +00:00
{
overRuns + + ;
2011-09-01 21:58:10 +00:00
//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);
2020-07-27 10:28:48 +00:00
2011-09-12 21:03:16 +00:00
bufferFull = true ;
2011-09-12 05:43:37 +00:00
2011-09-01 21:58:10 +00:00
// Discard current chunk of data
2011-07-20 21:14:00 +00:00
goto Finish ;
}
2011-08-01 19:12:44 +00:00
src = mixBuffer ;
2011-07-20 21:14:00 +00:00
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 = 0 ;
len1 = numBytes ;
len2 = 0 ;
}
// Copy chunk to write position in buffer
2011-08-01 19:12:44 +00:00
bytesRemaining = numBytes ;
bytesToCopy = ( bytesRemaining > len1 ? len1 : bytesRemaining ) ;
2011-07-20 21:14:00 +00:00
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 )
{
2011-08-07 23:09:18 +00:00
// If so, wrap it around to beginning again and set write wrapped flag
2011-07-20 21:14:00 +00:00
writePos - = audioBufferSize ;
writeWrapped = true ;
}
Finish :
// Unlock SDL audio callback
SDL_UnlockAudio ( ) ;
2011-09-12 05:43:37 +00:00
// Return whether buffer is half full
2011-09-12 21:03:16 +00:00
return bufferFull ;
2011-07-20 21:14:00 +00:00
}
void CloseAudio ( )
{
// Close SDL audio output
SDL_CloseAudio ( ) ;
// Delete audio buffer
if ( audioBuffer ! = NULL )
{
delete [ ] audioBuffer ;
audioBuffer = NULL ;
}
}