From 8abfdf1aeef6a20ad1051220cd59e63faf18edeb Mon Sep 17 00:00:00 2001 From: Iliyas Jorio Date: Sun, 8 Jan 2023 21:13:07 +0100 Subject: [PATCH] More pleasant audio mix + cmixer stability update --- src/music.cpp | 8 +-- src/soundfx.cpp | 35 +++++---- src/soundfx.h | 3 - src/support/cmixer.cpp | 157 +++++++++++++++++++++++++++++++++-------- src/support/cmixer.h | 28 ++++++-- 5 files changed, 174 insertions(+), 57 deletions(-) diff --git a/src/music.cpp b/src/music.cpp index 58734bd..ca40dcb 100644 --- a/src/music.cpp +++ b/src/music.cpp @@ -80,11 +80,8 @@ static std::vector LoadFile(char const* filename) void ChooseMusic( short which ) { - if (s_musicChannel != NULL) - { - delete s_musicChannel; - s_musicChannel = NULL; - } + // Kill existing song first, if any + ShutdownMusic(); musicSelection = -1; @@ -115,6 +112,7 @@ void ShutdownMusic() { if (s_musicChannel) { + s_musicChannel->RemoveFromMixer(); delete s_musicChannel; s_musicChannel = NULL; } diff --git a/src/soundfx.cpp b/src/soundfx.cpp index 00e24fc..9b73b3d 100644 --- a/src/soundfx.cpp +++ b/src/soundfx.cpp @@ -7,9 +7,11 @@ #include "support/cmixer.h" #include -static std::vector soundBank; -MBoolean soundOn = true; -float playerStereoSeparation = 1.0; +MBoolean soundOn = true; + +static std::vector s_soundBank; +static constexpr float k_playerStereoSeparation = 0.5f; +static constexpr float k_soundEffectGain = 0.7f; void InitSound() { @@ -23,14 +25,18 @@ void InitSound() Error(path); } - soundBank.emplace_back(); - soundBank.back().InitFromWAVFile(path) ; + s_soundBank.emplace_back(cmixer::LoadWAVFromFile(path)); + s_soundBank.back().SetInterpolation(true); } } void ShutdownSound() { - soundBank.clear(); + for (auto& wavStream : s_soundBank) + { + wavStream.RemoveFromMixer(); + } + s_soundBank.clear(); cmixer::ShutdownWithSDL(); } @@ -48,19 +54,22 @@ void PlayStereoFrequency( short player, short which, short freq ) { if (soundOn) { - auto& effect = soundBank.at(which); - + auto& effect = s_soundBank.at(which); + double pan; - switch (player) { - case 0: pan = -playerStereoSeparation; break; - case 1: pan = +playerStereoSeparation; break; + switch (player) + { + case 0: pan = -k_playerStereoSeparation; break; + case 1: pan = +k_playerStereoSeparation; break; default: pan = 0.0; break; } - + + effect.Stop(); + effect.SetGain(k_soundEffectGain); effect.SetPan(pan); effect.SetPitch(1.0 + freq/16.0); effect.Play(); - + UpdateSound(); } } diff --git a/src/soundfx.h b/src/soundfx.h index 3fb489e..73751ce 100644 --- a/src/soundfx.h +++ b/src/soundfx.h @@ -30,7 +30,4 @@ enum kNumSounds }; -namespace FMOD { class System; } - extern MBoolean soundOn; -extern FMOD::System *g_fmod; diff --git a/src/support/cmixer.cpp b/src/support/cmixer.cpp index fe051b6..a015224 100644 --- a/src/support/cmixer.cpp +++ b/src/support/cmixer.cpp @@ -44,6 +44,30 @@ using namespace cmixer; #define BUFFER_MASK (BUFFER_SIZE - 1) +static inline int16_t UnpackI16BE(const void* data) +{ +#if __BIG_ENDIAN__ + // no-op on big-endian systems + return *(const uint16_t*) data; +#else + const uint8_t* p = (uint8_t*) data; + return ( p[0] << 8 ) + | ( p[1] ); +#endif +} + +static inline int16_t UnpackI16LE(const void* data) +{ +#if __BIG_ENDIAN__ + const uint8_t* p = (uint8_t*) data; + return ( p[0] ) + | ( p[1] << 8 ); +#else + // no-op on little-endian systems + return *(const uint16_t*) data; +#endif +} + //----------------------------------------------------------------------------- // Global mixer @@ -86,11 +110,12 @@ void cmixer::InitWithSDL() // Init SDL audio SDL_AudioSpec fmt = {}; fmt.freq = 44100; - fmt.format = AUDIO_S16; + fmt.format = AUDIO_S16SYS; fmt.channels = 2; fmt.samples = 1024; fmt.callback = [](void* udata, Uint8* stream, int size) { + (void) udata; gMixer.Process((int16_t*) stream, size / 2); }; @@ -161,7 +186,7 @@ void Mixer::SetMasterGain(double newGain) { if (newGain < 0) newGain = 0; - gain = FX_FROM_FLOAT(newGain); + gain = (int) FX_FROM_FLOAT(newGain); } void Mixer::Process(int16_t* dst, int len) @@ -245,6 +270,7 @@ void Source::Init(int theSampleRate, int theLength) { this->samplerate = theSampleRate; this->length = theLength; + this->sustainOffset = 0; SetGain(1); SetPan(0); SetPitch(1); @@ -252,16 +278,29 @@ void Source::Init(int theSampleRate, int theLength) Stop(); } -Source::~Source() +void Source::RemoveFromMixer() { gMixer.Lock(); if (active) { gMixer.sources.remove(this); + active = false; } gMixer.Unlock(); } +Source::~Source() +{ + if (active) + { + // You MUST call RemoveFromMixer before destroying a source. If you get here, your program is incorrect. + fprintf(stderr, "Source wasn't removed from mixer prior to destruction!\n"); +#if _DEBUG + std::terminate(); +#endif + } +} + void Source::Rewind() { RewindImplementation(); @@ -395,8 +434,8 @@ void Source::RecalcGains() { double l = this->gain * (pan <= 0. ? 1. : 1. - pan); double r = this->gain * (pan >= 0. ? 1. : 1. + pan); - this->lgain = FX_FROM_FLOAT(l); - this->rgain = FX_FROM_FLOAT(r); + this->lgain = (int) FX_FROM_FLOAT(l); + this->rgain = (int) FX_FROM_FLOAT(r); } void Source::SetGain(double newGain) @@ -422,7 +461,7 @@ void Source::SetPitch(double newPitch) { newRate = 0.001; } - rate = FX_FROM_FLOAT(newRate); + rate = (int) FX_FROM_FLOAT(newRate); } void Source::SetLoop(bool newLoop) @@ -437,6 +476,13 @@ void Source::SetInterpolation(bool newInterpolation) void Source::Play() { + if (length == 0) + { + // Don't attempt to play an empty source as this would result + // in instant starvation when filling mixer buffer + return; + } + gMixer.Lock(); state = CM_STATE_PLAYING; if (!active) @@ -488,36 +534,88 @@ void WavStream::ClearImplementation() bitdepth = 0; channels = 0; idx = 0; + +#if __BIG_ENDIAN__ // default to native endianness + bigEndian = true; +#else + bigEndian = false; +#endif + userBuffer.clear(); } +void WavStream::Init( + int theSampleRate, + int theBitDepth, + int theNChannels, + bool theBigEndian, + std::span theSpan) +{ + Clear(); + Source::Init(theSampleRate, int((theSpan.size() / (theBitDepth / 8)) / theNChannels)); + this->bitdepth = theBitDepth; + this->channels = theNChannels; + this->idx = 0; + this->span = theSpan; + this->bigEndian = theBigEndian; +} + +std::span WavStream::GetBuffer(int nBytesOut) +{ + userBuffer.clear(); + userBuffer.reserve(nBytesOut); + return std::span(userBuffer.data(), nBytesOut); +} + +std::span WavStream::SetBuffer(std::vector&& data) +{ + userBuffer = std::move(data); + return std::span(userBuffer.data(), userBuffer.size()); +} + void WavStream::RewindImplementation() { idx = 0; } -void WavStream::FillBuffer(int16_t* dst, int len) +void WavStream::FillBuffer(int16_t* dst, int fillLength) { int x, n; - len /= 2; + fillLength /= 2; - while (len > 0) + while (fillLength > 0) { - n = MIN(len, length - idx); - len -= n; - if (bitdepth == 16 && channels == 1) + n = MIN(fillLength, length - idx); + + fillLength -= n; + + if (bigEndian && bitdepth == 16 && channels == 1) + { + WAV_PROCESS_LOOP({ + dst[0] = dst[1] = UnpackI16BE(&data16()[idx]); + }); + } + else if (bigEndian && bitdepth == 16 && channels == 2) { WAV_PROCESS_LOOP({ - dst[0] = dst[1] = data16()[idx]; + x = idx * 2; + dst[0] = UnpackI16BE(&data16()[x]); + dst[1] = UnpackI16BE(&data16()[x + 1]); + }); + } + else if (bitdepth == 16 && channels == 1) + { + WAV_PROCESS_LOOP({ + dst[0] = dst[1] = UnpackI16LE(&data16()[idx]); }); } else if (bitdepth == 16 && channels == 2) { WAV_PROCESS_LOOP({ x = idx * 2; - dst[0] = data16()[x]; - dst[1] = data16()[x + 1]; + dst[0] = UnpackI16LE(&data16()[x]); + dst[1] = UnpackI16LE(&data16()[x + 1]); }); } else if (bitdepth == 8 && channels == 1) @@ -535,15 +633,15 @@ void WavStream::FillBuffer(int16_t* dst, int len) }); } // Loop back and continue filling buffer if we didn't fill the buffer - if (len > 0) + if (fillLength > 0) { - idx = 0; + idx = sustainOffset; } } } //----------------------------------------------------------------------------- -// LoadWAVFromFile for testing +// LoadWAVFromFile static std::vector LoadFile(char const* filename) { @@ -570,7 +668,7 @@ static const char* FindChunk(const char* data, int len, const char* id, int* siz return p + 8; } -void cmixer::WavStream::InitFromWAVFile(const char* path) +WavStream cmixer::LoadWAVFromFile(const char* path) { int sz; auto filebuf = LoadFile(path); @@ -581,7 +679,7 @@ void cmixer::WavStream::InitFromWAVFile(const char* path) // Check header if (memcmp(p, "RIFF", 4) || memcmp(p + 8, "WAVE", 4)) throw std::invalid_argument("bad wav header"); - + // Find fmt subchunk p = FindChunk(data, len, "fmt ", &sz); if (!p) @@ -602,15 +700,12 @@ void cmixer::WavStream::InitFromWAVFile(const char* path) if (!p) throw std::invalid_argument("no data subchunk"); - Clear(); - Init(samplerate, sz / (channels * bitdepth / 8)); - - this->bitdepth = bitdepth; - this->channels = channels; - this->idx = 0; - this->interpolate = true; - - userBuffer.reserve(sz); - for (int i = 0; i < sz; i++) - userBuffer.push_back(p[i]); + WavStream wavStream; + wavStream.Init( + samplerate, + bitdepth, + channels, + false, + wavStream.SetBuffer(std::vector(p, p + sz))); + return wavStream; } diff --git a/src/support/cmixer.h b/src/support/cmixer.h index 8fbc7de..69b97cb 100644 --- a/src/support/cmixer.h +++ b/src/support/cmixer.h @@ -27,6 +27,7 @@ #include #include #include +#include #define BUFFER_SIZE (512) @@ -45,6 +46,7 @@ namespace cmixer int16_t pcmbuf[BUFFER_SIZE]; // Internal buffer with raw stereo PCM int samplerate; // Stream's native samplerate int length; // Stream's length in frames + int sustainOffset; // Offset of the sustain loop in frames int end; // End index for the current play-through int state; // Current state (playing|paused|stopped) int64_t position; // Current playhead position (fixed point) @@ -75,6 +77,8 @@ namespace cmixer public: virtual ~Source(); + void RemoveFromMixer(); + void Clear(); void Rewind(); @@ -114,7 +118,9 @@ namespace cmixer { int bitdepth; int channels; + bool bigEndian; int idx; + std::span span; std::vector userBuffer; void ClearImplementation() override; @@ -123,16 +129,28 @@ namespace cmixer void FillBuffer(int16_t* buffer, int length) override; - inline const uint8_t* data8() const - { return reinterpret_cast(userBuffer.data()); } + inline uint8_t* data8() const + { return reinterpret_cast(span.data()); } - inline const int16_t* data16() const - { return reinterpret_cast(userBuffer.data()); } + inline int16_t* data16() const + { return reinterpret_cast(span.data()); } public: WavStream(); - void InitFromWAVFile(const char* path); + WavStream(WavStream&&) = default; // move constructor ensures span stays in sync with userBuffer! + + void Init( + int theSampleRate, + int theBitDepth, + int nChannels, + bool bigEndian, + std::span data + ); + + std::span GetBuffer(int nBytesOut); + + std::span SetBuffer(std::vector&& data); };