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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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)
|
2011-08-01 19:12:44 +00:00
|
|
|
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
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;
|
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
|
2022-06-09 21:10:39 +00:00
|
|
|
static INT8* audioBuffer = NULL; // Audio buffer
|
2011-07-20 21:14:00 +00:00
|
|
|
|
|
|
|
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
|
2022-06-09 21:10:39 +00:00
|
|
|
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;
|
|
|
|
|
2011-09-12 05:43:37 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
void SetAudioCallback(AudioCallbackFPtr newCallback, void* newData)
|
2011-09-12 05:43:37 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// Lock audio whilst changing callback pointers
|
|
|
|
SDL_LockAudio();
|
2011-09-12 05:43:37 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
callback = newCallback;
|
|
|
|
callbackData = newData;
|
2011-09-12 05:43:37 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
SDL_UnlockAudio();
|
2011-09-12 05:43:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void SetAudioEnabled(bool newEnabled)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
enabled = newEnabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Set game audio mixing type
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="type"></param>
|
|
|
|
void SetAudioType(Game::AudioTypes type)
|
|
|
|
{
|
|
|
|
AudioType = type;
|
2011-09-12 05:43:37 +00:00
|
|
|
}
|
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
static INT16 AddAndClampINT16(INT32 x, INT32 y)
|
2011-07-20 21:14:00 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
INT32 sum = x + y;
|
|
|
|
if (sum > INT16_MAX) {
|
|
|
|
sum = INT16_MAX;
|
|
|
|
}
|
|
|
|
if (sum < INT16_MIN) {
|
|
|
|
sum = INT16_MIN;
|
|
|
|
}
|
|
|
|
return (INT16)sum;
|
|
|
|
}
|
|
|
|
|
2022-06-19 16:15:54 +00:00
|
|
|
static INT16 MixINT16(INT32 x, INT32 y)
|
|
|
|
{
|
|
|
|
INT32 sum = (x + y)>>1;
|
|
|
|
if (sum > INT16_MAX) {
|
|
|
|
sum = INT16_MAX;
|
|
|
|
}
|
|
|
|
if (sum < INT16_MIN) {
|
|
|
|
sum = INT16_MIN;
|
|
|
|
}
|
|
|
|
return (INT16)sum;
|
|
|
|
}
|
|
|
|
|
2022-11-03 10:49:35 +00:00
|
|
|
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;
|
|
|
|
if (xi > INT16_MAX) {
|
|
|
|
xi = INT16_MAX;
|
|
|
|
}
|
|
|
|
if (xi < INT16_MIN) {
|
|
|
|
xi = INT16_MIN;
|
|
|
|
}
|
|
|
|
return (INT16)xi;
|
|
|
|
}
|
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
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);
|
2020-07-27 10:28:48 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Get current write position and adjust it if write has wrapped but play position has not
|
|
|
|
UINT32 adjWritePos = writePos;
|
|
|
|
if (writeWrapped)
|
|
|
|
adjWritePos += audioBufferSize;
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
underRuns++;
|
2011-08-07 23:09:18 +00:00
|
|
|
|
2022-06-09 21:10:39 +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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// See what action to take on under-run
|
2011-08-07 23:09:18 +00:00
|
|
|
if (underRunLoop)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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;
|
2011-08-07 23:09:18 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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
|
2011-07-20 21:14:00 +00:00
|
|
|
if (playPos + len > audioBufferSize)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// If so, split play region into two
|
|
|
|
src1 = audioBuffer + playPos;
|
|
|
|
src2 = audioBuffer;
|
|
|
|
len1 = audioBufferSize - playPos;
|
|
|
|
len2 = len - len1;
|
2011-07-20 21:14:00 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// Otherwise, just copy whole region
|
|
|
|
src1 = audioBuffer + playPos;
|
|
|
|
src2 = 0;
|
|
|
|
len1 = len;
|
|
|
|
len2 = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if audio is enabled
|
2011-07-20 21:14:00 +00:00
|
|
|
if (enabled)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// If so, copy play region into audio output stream
|
|
|
|
memcpy(stream, src1, len1);
|
2020-07-27 10:28:48 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Also, if not looping on under-runs then blank region out
|
|
|
|
if (!underRunLoop)
|
|
|
|
memset(src1, 0, len1);
|
2011-08-07 23:09:18 +00:00
|
|
|
|
2011-07-20 21:14:00 +00:00
|
|
|
if (len2)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// If region was split into two, copy second half into audio output stream as well
|
|
|
|
memcpy(stream + len1, src2, len2);
|
2011-08-07 23:09:18 +00:00
|
|
|
|
2022-06-09 21:10:39 +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
|
2022-06-09 21:10:39 +00:00
|
|
|
// Otherwise, just copy silence to audio output stream
|
|
|
|
memset(stream, 0, len);
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Move play position forward for next time
|
|
|
|
playPos += len;
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
bool bufferFull = adjWritePos + 2 * bytes_per_frame_host > playPos + audioBufferSize;
|
2011-09-12 21:03:16 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Check if play position has moved past end of buffer
|
2011-07-20 21:14:00 +00:00
|
|
|
if (playPos >= audioBufferSize)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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);
|
2011-07-20 21:14:00 +00:00
|
|
|
}
|
|
|
|
|
2022-11-03 10:49:35 +00:00
|
|
|
static void MixChannels(unsigned numSamples, const float* leftFrontBuffer, const float* rightFrontBuffer, const float* leftRearBuffer, const float* rightRearBuffer, void* dest, bool flipStereo)
|
2011-07-20 21:14:00 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
INT16* p = (INT16*)dest;
|
|
|
|
|
|
|
|
if (nbHostAudioChannels == 1) {
|
|
|
|
for (unsigned i = 0; i < numSamples; i++) {
|
2022-06-19 16:15:54 +00:00
|
|
|
INT16 monovalue = MixINT16(
|
2022-11-03 10:49:35 +00:00
|
|
|
MixFloat(leftFrontBuffer[i] * balanceFactorFrontLeft,rightFrontBuffer[i] * balanceFactorFrontRight),
|
|
|
|
MixFloat(leftRearBuffer[i] * balanceFactorRearLeft, rightRearBuffer[i] * balanceFactorRearRight));
|
2022-06-09 21:10:39 +00:00
|
|
|
*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++) {
|
2022-11-03 10:49:35 +00:00
|
|
|
INT16 leftvalue = MixINT16(leftFrontBuffer[i] * balanceFactorFrontLeft, leftRearBuffer[i] * balanceFactorRearLeft);
|
|
|
|
INT16 rightvalue = MixINT16(rightFrontBuffer[i]*balanceFactorFrontRight, rightRearBuffer[i]*balanceFactorRearRight);
|
2022-06-09 21:10:39 +00:00
|
|
|
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++) {
|
2022-11-03 10:49:35 +00:00
|
|
|
float frontLeftValue = leftFrontBuffer[i]*balanceFactorFrontLeft;
|
|
|
|
float frontRightValue = rightFrontBuffer[i]*balanceFactorFrontRight;
|
|
|
|
float rearLeftValue = leftRearBuffer[i]*balanceFactorRearLeft;
|
|
|
|
float rearRightValue = rightRearBuffer[i]*balanceFactorRearRight;
|
2022-06-09 21:10:39 +00:00
|
|
|
|
|
|
|
// Check game audio type
|
|
|
|
switch (AudioType) {
|
|
|
|
case Game::MONO: {
|
2022-11-03 10:49:35 +00:00
|
|
|
INT16 monovalue = MixINT16(MixFloat(frontLeftValue, frontRightValue), MixFloat(rearLeftValue, rearRightValue));
|
2022-06-09 21:10:39 +00:00
|
|
|
*p++ = monovalue;
|
|
|
|
*p++ = monovalue;
|
|
|
|
*p++ = monovalue;
|
|
|
|
*p++ = monovalue;
|
|
|
|
} break;
|
|
|
|
|
|
|
|
case Game::STEREO_LR:
|
|
|
|
case Game::STEREO_RL: {
|
2022-06-19 16:15:54 +00:00
|
|
|
INT16 leftvalue = MixINT16(frontLeftValue, frontRightValue);
|
|
|
|
INT16 rightvalue = MixINT16(rearLeftValue, rearRightValue);
|
2022-06-09 21:10:39 +00:00
|
|
|
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
|
|
|
|
{
|
2022-11-03 10:49:35 +00:00
|
|
|
*p++ = ClampINT16(frontRightValue);
|
|
|
|
*p++ = ClampINT16(frontLeftValue);
|
|
|
|
*p++ = ClampINT16(rearRightValue);
|
|
|
|
*p++ = ClampINT16(rearLeftValue);
|
2022-06-09 21:10:39 +00:00
|
|
|
} else {
|
2022-11-03 10:49:35 +00:00
|
|
|
*p++ = ClampINT16(frontLeftValue);
|
|
|
|
*p++ = ClampINT16(frontRightValue);
|
|
|
|
*p++ = ClampINT16(rearLeftValue);
|
|
|
|
*p++ = ClampINT16(rearRightValue);
|
2022-06-09 21:10:39 +00:00
|
|
|
}
|
|
|
|
} 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
|
|
|
|
{
|
2022-11-03 10:49:35 +00:00
|
|
|
*p++ = ClampINT16(rearRightValue);
|
|
|
|
*p++ = ClampINT16(rearLeftValue);
|
|
|
|
*p++ = ClampINT16(frontRightValue);
|
|
|
|
*p++ = ClampINT16(frontLeftValue);
|
2022-06-09 21:10:39 +00:00
|
|
|
} else {
|
2022-11-03 10:49:35 +00:00
|
|
|
*p++ = ClampINT16(rearLeftValue);
|
|
|
|
*p++ = ClampINT16(rearRightValue);
|
|
|
|
*p++ = ClampINT16(frontLeftValue);
|
|
|
|
*p++ = ClampINT16(frontRightValue);
|
2022-06-09 21:10:39 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Game::QUAD_1_LR_2_FR_MIX:
|
|
|
|
// Split mix: one goes to left/right, other front/rear (mono)
|
|
|
|
// =>Remix all!
|
2022-06-19 16:15:54 +00:00
|
|
|
INT16 newfrontLeftValue = MixINT16(frontLeftValue, rearLeftValue);
|
|
|
|
INT16 newfrontRightValue = MixINT16(frontLeftValue, rearRightValue);
|
|
|
|
INT16 newrearLeftValue = MixINT16(frontRightValue, rearLeftValue);
|
|
|
|
INT16 newrearRightValue = MixINT16(frontRightValue, rearRightValue);
|
2022-06-09 21:10:39 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-09-01 06:43:01 +00:00
|
|
|
}
|
|
|
|
|
2020-04-19 08:34:58 +00:00
|
|
|
/*
|
2011-09-01 06:43:01 +00:00
|
|
|
static void LogAudioInfo(SDL_AudioSpec *fmt)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Prepare audio subsystem on host.
|
|
|
|
/// The requested channels is deduced, and SDL will make sure it is compatible with this.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="config"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
bool OpenAudio(const Util::Config::Node& config)
|
2011-07-20 21:14:00 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
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<int>();
|
|
|
|
|
|
|
|
// 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
|
2022-11-03 10:49:35 +00:00
|
|
|
float balancelr = std::max(-100.f, std::min(100.f, s_config->Get("BalanceLeftRight").ValueAs<float>()));
|
2022-06-09 21:10:39 +00:00
|
|
|
balancelr *= 0.01f;
|
|
|
|
BalanceLeftRight = balancelr;
|
|
|
|
|
2022-11-03 10:49:35 +00:00
|
|
|
float balancefr = std::max(-100.f, std::min(100.f, s_config->Get("BalanceFrontRear").ValueAs<float>()));
|
2022-06-09 21:10:39 +00:00
|
|
|
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
|
2022-11-03 10:49:35 +00:00
|
|
|
SDL_AudioSpec desired{};
|
2022-06-09 21:10:39 +00:00
|
|
|
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<float>();
|
|
|
|
if (soundFreq_Hz>MAX_SND_FREQ)
|
|
|
|
soundFreq_Hz = MAX_SND_FREQ;
|
|
|
|
if (soundFreq_Hz<MIN_SND_FREQ)
|
|
|
|
soundFreq_Hz = MIN_SND_FREQ;
|
|
|
|
samples_per_frame_host = (INT32)(SAMPLE_RATE_M3 / soundFreq_Hz);
|
|
|
|
bytes_per_sample_host = (nbHostAudioChannels * sizeof(INT16));
|
|
|
|
bytes_per_frame_host = (samples_per_frame_host * bytes_per_sample_host);
|
|
|
|
|
|
|
|
|
|
|
|
// Create audio buffer
|
|
|
|
uint32_t bufferSize = ((SAMPLE_RATE_M3 * latency) / MAX_LATENCY) * bytes_per_sample_host;
|
|
|
|
if (!(bufferSize % bytes_per_sample_host == 0)) {
|
|
|
|
return ErrorLog("must be an integer multiple of the sample size\n");
|
|
|
|
}
|
|
|
|
audioBufferSize = bufferSize;
|
|
|
|
|
|
|
|
int minBufferSize = 3 * bytes_per_frame_host;
|
|
|
|
audioBufferSize = std::max<int>(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<int>(endOfBuffer, midpointAfterFirstFrame);
|
|
|
|
writeWrapped = false;
|
|
|
|
|
|
|
|
// Reset counters
|
|
|
|
underRuns = 0;
|
|
|
|
overRuns = 0;
|
|
|
|
|
|
|
|
// Start audio playing
|
|
|
|
SDL_PauseAudio(0);
|
|
|
|
return OKAY;
|
2011-07-20 21:14:00 +00:00
|
|
|
}
|
|
|
|
|
2022-11-03 10:49:35 +00:00
|
|
|
bool OutputAudio(unsigned numSamples, const float* leftFrontBuffer, const float* rightFrontBuffer, const float* leftRearBuffer, const float* rightRearBuffer, bool flipStereo)
|
2011-07-20 21:14:00 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
UINT32 bytesRemaining;
|
|
|
|
UINT32 bytesToCopy;
|
|
|
|
INT16* src;
|
2011-08-01 19:12:44 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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;
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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);
|
2020-07-27 10:28:48 +00:00
|
|
|
|
2022-06-09 21:10:39 +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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Calculate number of bytes for current sound chunk
|
|
|
|
UINT32 numBytes = numSamples * bytes_per_sample_host;
|
2020-07-27 10:28:48 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Get end of current play region (writing must occur past this point)
|
|
|
|
UINT32 playEndPos = playPos + bytes_per_frame_host;
|
2020-07-27 10:28:48 +00:00
|
|
|
|
2022-11-03 10:49:35 +00:00
|
|
|
// Undo any wrap-around of the write position that may have occurred to create following ordering: playPos < playEndPos < writePos
|
2022-06-09 21:10:39 +00:00
|
|
|
if (playEndPos > writePos && writeWrapped)
|
|
|
|
writePos += audioBufferSize;
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Check if play region has caught up with write position and now overlaps it (ie buffer under-run)
|
2011-08-07 23:09:18 +00:00
|
|
|
if (playEndPos > writePos)
|
2011-07-20 21:14:00 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
underRuns++;
|
2011-08-07 23:09:18 +00:00
|
|
|
|
2022-06-09 21:10:39 +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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// See what action to take on under-run
|
2011-08-07 23:09:18 +00:00
|
|
|
if (underRunLoop)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// If loop, then move play position back to beginning of data in buffer
|
|
|
|
playPos = writePos + numBytes + bytes_per_frame_host;
|
2020-07-27 10:28:48 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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;
|
2020-07-27 10:28:48 +00:00
|
|
|
else
|
2011-08-07 23:09:18 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +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;
|
|
|
|
}
|
2011-08-07 23:09:18 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// Otherwise, bump write position forward in chunks until it is past end of play region
|
2011-08-07 23:09:18 +00:00
|
|
|
do
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
writePos += numBytes;
|
2011-08-07 23:09:18 +00:00
|
|
|
}
|
|
|
|
while (playEndPos > writePos);
|
2022-06-09 21:10:39 +00:00
|
|
|
}
|
|
|
|
}
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Check if write position has caught up with play region and now overlaps it (ie buffer over-run)
|
|
|
|
bool overRun = writePos + numBytes > playPos + audioBufferSize;
|
2020-07-27 10:28:48 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
bool bufferFull = writePos + 2 * bytes_per_frame_host > playPos + audioBufferSize;
|
2011-09-12 05:43:37 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Move write position back to within buffer
|
|
|
|
if (writePos >= audioBufferSize)
|
|
|
|
writePos -= audioBufferSize;
|
2011-09-01 21:58:10 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Handle buffer over-run
|
2011-09-01 21:58:10 +00:00
|
|
|
if (overRun)
|
2011-07-20 21:14:00 +00:00
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
overRuns++;
|
2011-09-01 21:58:10 +00:00
|
|
|
|
2022-06-09 21:10:39 +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
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
bufferFull = true;
|
2011-09-12 05:43:37 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Discard current chunk of data
|
|
|
|
}
|
2022-11-03 10:49:35 +00:00
|
|
|
else
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
src = mixBuffer;
|
|
|
|
INT8* dst1;
|
|
|
|
INT8* dst2;
|
|
|
|
UINT32 len1;
|
|
|
|
UINT32 len2;
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Check if write region extends past end of buffer
|
2011-07-20 21:14:00 +00:00
|
|
|
if (writePos + numBytes > audioBufferSize)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// If so, split write region into two
|
|
|
|
dst1 = audioBuffer + writePos;
|
|
|
|
dst2 = audioBuffer;
|
|
|
|
len1 = audioBufferSize - writePos;
|
|
|
|
len2 = numBytes - len1;
|
2011-07-20 21:14:00 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// Otherwise, just copy whole region
|
|
|
|
dst1 = audioBuffer + writePos;
|
2022-11-03 10:49:35 +00:00
|
|
|
dst2 = NULL;
|
2022-06-09 21:10:39 +00:00
|
|
|
len1 = numBytes;
|
2022-11-03 10:49:35 +00:00
|
|
|
len2 = NULL;
|
2022-06-09 21:10:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
2011-07-20 21:14:00 +00:00
|
|
|
|
|
|
|
if (bytesRemaining)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// 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);
|
|
|
|
}
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Move write position forward for next time
|
|
|
|
writePos += numBytes;
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Check if write position has moved past end of buffer
|
2011-07-20 21:14:00 +00:00
|
|
|
if (writePos >= audioBufferSize)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// If so, wrap it around to beginning again and set write wrapped flag
|
|
|
|
writePos -= audioBufferSize;
|
|
|
|
writeWrapped = true;
|
|
|
|
}
|
2022-11-03 10:49:35 +00:00
|
|
|
}
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Unlock SDL audio callback
|
|
|
|
SDL_UnlockAudio();
|
2011-09-12 05:43:37 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Return whether buffer is half full
|
|
|
|
return bufferFull;
|
2011-07-20 21:14:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CloseAudio()
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
// Close SDL audio output
|
|
|
|
SDL_CloseAudio();
|
2011-07-20 21:14:00 +00:00
|
|
|
|
2022-06-09 21:10:39 +00:00
|
|
|
// Delete audio buffer
|
2011-07-20 21:14:00 +00:00
|
|
|
if (audioBuffer != NULL)
|
|
|
|
{
|
2022-06-09 21:10:39 +00:00
|
|
|
delete[] audioBuffer;
|
|
|
|
audioBuffer = NULL;
|
|
|
|
}
|
2011-07-20 21:14:00 +00:00
|
|
|
}
|