Created
August 29, 2023 10:10
-
-
Save DearthDev/cda7c0fe37914cd9604fda792d501746 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Audio library made in a single header library style for my game engine. | |
#pragma once | |
struct Sound { | |
u32 frameCount; | |
i16 *samples; | |
}; | |
enum class AudioBus { | |
Music, | |
Sfx, | |
Voice, | |
}; | |
class AudioPlayer { | |
public: | |
AudioPlayer(Sound *sound, AudioBus audioBus); | |
~AudioPlayer(); | |
AudioBus GetAudioBus() const { return m_audioBus; } | |
void Play(); | |
void PlayOneshot(); | |
void Pause(); | |
bool IsPlaying() const {return m_playing;} | |
bool IsOneshot() const {return m_oneshot;} | |
void SetOneshot(bool oneshot) {m_oneshot = oneshot;} | |
f32 GetAmplitude() const {return m_amplitude;} | |
void SetAmplitude(f32 amplitude) {m_amplitude = amplitude;} | |
f32 GetPitch() const {return m_pitch;} | |
void SetPitch(f32 pitch) {m_pitch = pitch;} | |
bool IsPositional() const {return m_positional;} | |
void SetPositional(bool positional) {m_positional = positional;} | |
v3 GetPosition() const {return m_position;} | |
void SetPosition(const v3 &position) {m_position = position;} | |
f32 GetRadius() const {return m_radius;} | |
void SetRadius(f32 radius) {m_radius = radius;} | |
// Getthe next audio player in the linked list. | |
AudioPlayer *GetNext() const {return m_next;} | |
// Set the next audio player in the linked list. | |
void SetNext(AudioPlayer *next) {m_next = next;} | |
// Get the previous audio player in the linked list. | |
AudioPlayer *GetPrev() const {return m_prev;} | |
// Set the previous audio player in the linked list. | |
void SetPrev(AudioPlayer *prev) {m_prev = prev;} | |
// Write audio samples to the output buffer. | |
void Write(const f32 modulate, const v3 &listenerPosition, const f32 listenerYaw, const u32 frameCount, f32 *output); | |
AudioPlayer(AudioPlayer const &) = delete; | |
void operator=(AudioPlayer const &) = delete; | |
private: | |
// Helper function to check and reset if the audio source is oneshot | |
// and finished. | |
bool CheckOneshotDone(u32 frame); | |
// Helper function to calculate playback amplitude. | |
f32 CalcPlayAmp(const v3 &listenerPosition); | |
// Helper function to calculate the pan of the audio when played. | |
f32 CalcPlayPan(const v3 &listenerPosition, f32 listenerYaw); | |
// Helper function to write unpitched audio samples to the output buffer. | |
void WriteUnpitched(f32 amp, f32 pan, f32 *output, u32 frameCount); | |
// Helper function to write pitched audio samples to the output buffer. | |
void WritePitched(f32 amp, f32 pan, f32 *output, u32 frameCount); | |
Sound *m_sound; | |
AudioBus m_audioBus; | |
bool m_playing = false; | |
bool m_oneshot = false; | |
f32 m_amplitude = 1.0f; | |
f32 m_pitch = 1.0f; | |
bool m_positional = false; | |
v3 m_position = {}; | |
f32 m_radius = 10.0f; | |
AudioPlayer *m_next = 0; | |
AudioPlayer *m_prev = 0; | |
u32 m_currentFrame = 0; | |
}; | |
class AudioManager { | |
public: | |
// Adds an audio player to the manager's linked list. | |
void AddAudioPlayer(AudioPlayer *audioPlayer); | |
// Removes the audio player to the manager's linked list. | |
void RemoveAudioPlayer(AudioPlayer *audioPlayer); | |
f32 GetMasterVolume() const {return m_masterVolume;} | |
void SetMasterVolume(f32 volume) {m_masterVolume = volume;} | |
f32 GetMusicVolume() const {return m_musicVolume;} | |
void SetMusicVolume(f32 volume) {m_musicVolume = volume;} | |
f32 GetSfxVolume() const {return m_sfxVolume;} | |
void SetSfxVolume(f32 volume) {m_sfxVolume = volume;} | |
f32 GetVoiceVolume() const {return m_voiceVolume;} | |
void SetVoiceVolume(f32 volume) {m_voiceVolume = volume;} | |
// Sets the position of the listener for positional audio. | |
void SetListenerPosition(const v3 &position) {m_listenerPosition = position;}; | |
// Sets the yaw (rotation along the y axis) of the listener for positional audio. | |
void SetListenerYaw(f32 yaw) {m_listenerYaw = yaw;} | |
// Has each audio player in the manager's linked list write audio samples | |
// to the output buffer. | |
void Write(const ma_uint32 frameCount, void *pOutput); | |
static AudioManager *Singleton() { | |
static AudioManager instance; | |
return &instance; | |
} | |
AudioManager(AudioManager const &) = delete; | |
void operator=(AudioManager const &) = delete; | |
private: | |
AudioManager() {} | |
f32 m_masterVolume = 1.0f; | |
f32 m_musicVolume = 1.0f; | |
f32 m_sfxVolume = 1.0f; | |
f32 m_voiceVolume = 1.0f; | |
v3 m_listenerPosition = {}; | |
f32 m_listenerYaw = 0.0f; | |
AudioPlayer *m_head = 0; | |
f32 m_audioOutput[MA_DATA_CONVERTER_STACK_BUFFER_SIZE] = {}; | |
}; | |
AudioPlayer::AudioPlayer(Sound *sound, AudioBus audioBus) : m_sound(sound), m_audioBus(audioBus) { | |
AudioManager::Singleton()->AddAudioPlayer(this); | |
} | |
AudioPlayer::~AudioPlayer() { | |
AudioManager::Singleton()->RemoveAudioPlayer(this); | |
} | |
void AudioPlayer::Play() { | |
m_playing = true; | |
} | |
void AudioPlayer::PlayOneshot() { | |
m_playing = true; | |
m_oneshot = true; | |
m_currentFrame = 0; | |
} | |
void AudioPlayer::Pause() { | |
m_playing = false; | |
} | |
void AudioPlayer::Write(const f32 modulate, const v3 &listenerPosition, const f32 listenerYaw, const u32 frameCount, f32 *output) { | |
if (!m_sound || !m_playing) | |
return; | |
// Calculate the amplitude based on the listener position and modulation value. | |
const f32 amp = CalcPlayAmp(listenerPosition) * modulate; | |
// If the amplitude is zero, calculate the next frame index and return | |
// without writing any data. | |
if (amp == 0.0) { | |
u32 frame = static_cast<u32>(m_currentFrame + m_pitch * frameCount); | |
if (CheckOneshotDone(frame)) { | |
return; | |
} | |
m_currentFrame = frame % m_sound->frameCount; | |
return; | |
} | |
// Calculate the panning value based on the listener position and yaw. | |
const f32 pan = CalcPlayPan(listenerPosition, listenerYaw); | |
if (m_pitch == 1.0f) | |
WriteUnpitched(amp, pan, output, frameCount); | |
else | |
WritePitched(amp, pan, output, frameCount); | |
} | |
bool AudioPlayer::CheckOneshotDone(u32 frame) { | |
if (m_oneshot && frame >= m_sound->frameCount) { | |
m_playing = false; | |
m_currentFrame = 0; | |
return true; | |
} | |
return false; | |
} | |
f32 AudioPlayer::CalcPlayAmp(const v3 &listenerPosition) { | |
// If this audio player is not positional, return the amplitude as is. | |
if (!m_positional) | |
return m_amplitude; | |
// obtain the squared distance between the listener and the audio source. | |
const f32 l2 = Length2(m_position - listenerPosition); | |
// If the listener is outside the audio source's radius, return 0. | |
const f32 r2 = m_radius * m_radius; | |
if (l2 > r2) | |
return 0.0; | |
// Calculate the amplitude as a function of distance from the audio source, | |
// using the inverse square. | |
return m_amplitude * (1.0f - l2 / r2); | |
} | |
f32 AudioPlayer::CalcPlayPan(const v3 &listenerPosition, f32 listenerYaw) { | |
// If this audio player is not positional, it should be played with equal | |
// power in both speakers. | |
if (!m_positional) | |
return 0.5f; | |
// Calculate the relative offset from the listener to this player in | |
// the x,z plane. | |
const v2 offset = V2(m_position.x - listenerPosition.x, m_position.z - listenerPosition.z); | |
if (offset == V2(0.0f, 0.0f)) | |
return 0.5f; | |
// Get the dot product of the listener's right unit vector and the offset | |
// vector, then map the [-1, 1] range to [0, 1]. | |
return Dot(Normalize(offset), V2Rotate(Wrap(listenerYaw + PIO2, -PI, PI))) * 0.5f + 0.5f; | |
} | |
void AudioPlayer::WriteUnpitched(f32 amp, f32 pan, f32 *output, u32 frameCount) { | |
for (u32 i = 0; i < frameCount; i++) { | |
m_currentFrame += 1; | |
// If this is a one-shot sound and we've reached the end of the sound | |
// data, stop writing samples. | |
if (CheckOneshotDone(m_currentFrame)) | |
return; | |
m_currentFrame = m_currentFrame % m_sound->frameCount; | |
f32 sample = m_sound->samples[m_currentFrame]; | |
sample *= amp; | |
// Write the sample to the left and right channel based on the panning. | |
output[i * 2] += sample * Cos(pan * PI / 2.0f); | |
output[i * 2 + 1] += sample * Sin(pan * PI / 2.0f); | |
} | |
} | |
void AudioPlayer::WritePitched(f32 amp, f32 pan, f32 *output, u32 frameCount) { | |
// Get the current frame as a float. | |
f32 frame = static_cast<float>(m_currentFrame); | |
for (u32 i = 0; i < frameCount; i++) { | |
// Increase the frame by the pitch which is a non-whole amount. | |
frame += m_pitch; | |
// If this is a one-shot sound and we've reached the end of the sound | |
// data, stop writing samples. | |
if (CheckOneshotDone(static_cast<u32>(Ceil(frame)))) | |
return; | |
// Get the two frames that will be lerped between. | |
const u32 prevFrame = static_cast<u32>(Floor(frame)) % m_sound->frameCount; | |
const u32 nextFrame = static_cast<u32>(Ceil(frame)) % m_sound->frameCount; | |
// Interpolate between the previous and next frames based on how far | |
// between the two frames our current frame is. | |
f32 sample = (Lerp(m_sound->samples[prevFrame], m_sound->samples[nextFrame], frame - Floor(frame))); | |
sample *= amp; | |
// Write the sample to the left and right channel based on the panning. | |
output[i * 2] += sample * Cos(pan * PI / 2.0f); | |
output[i * 2 + 1] += sample * Sin(pan * PI / 2.0f); | |
} | |
// Set the current frame to the closest whole value to the new frame value. | |
m_currentFrame = static_cast<u32>(frame) % m_sound->frameCount; | |
} | |
void AudioManager::AddAudioPlayer(AudioPlayer *audioPlayer) { | |
Assert(audioPlayer); | |
if (m_head) { | |
m_head->SetPrev(audioPlayer); | |
} | |
audioPlayer->SetNext(m_head); | |
m_head = audioPlayer; | |
} | |
void AudioManager::RemoveAudioPlayer(AudioPlayer *audioPlayer) { | |
Assert(audioPlayer); | |
if (audioPlayer == m_head) | |
m_head = audioPlayer->GetNext(); | |
if (audioPlayer->GetPrev()) | |
audioPlayer->GetPrev()->SetNext(audioPlayer->GetNext()); | |
if (audioPlayer->GetNext()) | |
audioPlayer->GetNext()->SetPrev(audioPlayer->GetPrev()); | |
} | |
void AudioManager::Write(const ma_uint32 frameCount, void *pOutput) { | |
i16 *output = static_cast<i16 *>(pOutput); | |
memset(m_audioOutput, 0, sizeof(m_audioOutput)); | |
// Write audio for each audio player. | |
AudioPlayer *audioPlayer = m_head; | |
while (audioPlayer) { | |
// Calculate the volume for this audio player. | |
f32 modulate = m_masterVolume; | |
switch (audioPlayer->GetAudioBus()) { | |
case AudioBus::Music: | |
modulate *= m_musicVolume; | |
break; | |
case AudioBus::Sfx: | |
modulate *= m_sfxVolume; | |
break; | |
case AudioBus::Voice: | |
modulate *= m_voiceVolume; | |
break; | |
} | |
audioPlayer->Write(modulate, m_listenerPosition, m_listenerYaw, frameCount, m_audioOutput); | |
audioPlayer = audioPlayer->GetNext(); | |
} | |
// For both channels, apply limiting and copy the samples to the output buffer. | |
for (u32 i = 0; i < frameCount * 2; i++) { | |
f32 a = m_audioOutput[i]; | |
if (a > 29500.0f) | |
a = Min(32767.0f, (a - 29500.0f) * 0.5f + 29500.0f); | |
else if (a < -29500.0f) | |
a = Max(-32767.0f, (a + 29500.0f) * 0.5f - 29500.0f); | |
output[i] = static_cast<i16>(a); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment