Perfected Audio

This commit is contained in:
2023-01-17 19:34:47 -08:00
parent a4ad5a3ac4
commit 821074dfc4
18 changed files with 459 additions and 308 deletions

View File

@ -7,7 +7,6 @@
target_link_libraries(${DAWN_TARGET_NAME}
PUBLIC
OpenAL
AudioFile
)
# Includes

View File

@ -1,82 +0,0 @@
// Copyright (c) 2023 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "AudioData.hpp"
using namespace Dawn;
AudioData::AudioData() {
}
void AudioData::init() {
alGenBuffers((ALsizei)1, &this->buffer);
// Test
AudioFile<double> audioFile;
audioFile.load ("C:\\sample.wav");
audioFile.printSummary();
ALenum format;
switch(audioFile.getBitDepth()) {
case 16:
switch(audioFile.getNumChannels()) {
case 2:
format = AL_FORMAT_STEREO16;
break;
case 1:
format = AL_FORMAT_MONO16;
break;
default:
assertUnreachable();
}
break;
case 8:
switch(audioFile.getNumChannels()) {
case 2:
format = AL_FORMAT_STEREO8;
break;
case 1:
format = AL_FORMAT_MONO8;
break;
default:
assertUnreachable();
}
break;
default:
assertUnreachable();
}
format = AL_FORMAT_MONO16;
std::vector<uint8_t> data;
for (int i = 0; i < audioFile.getNumSamplesPerChannel(); i++) {
// for(int y = 0; y < audioFile.getNumChannels(); y++) {
for(int y = 0; y < 1; y++) {
double sample = audioFile.samples[y][i];
sample = mathClamp(sample, -1., 1.);
auto q = static_cast<int16_t> (sample * 32767.);
uint8_t bytes[2];
bytes[0] = (q >> 8) & 0xFF;
bytes[1] = q & 0xFF;
data.push_back(bytes[1]);
data.push_back(bytes[0]);
}
}
alBufferData(buffer, format, &data[0], data.size(), audioFile.getSampleRate());
this->ready = true;
}
AudioData::~AudioData() {
if(this->ready) alDeleteBuffers((ALsizei)1, &this->buffer);
}

View File

@ -1,31 +0,0 @@
// Copyright (c) 2023 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "dawnlibs.hpp"
#include "dawnopenal.hpp"
#include "assert/assert.hpp"
#include "util/mathutils.hpp"
namespace Dawn {
class AudioSource;
class AudioData {
private:
ALuint buffer;
bool_t ready = false;
ALenum format;
public:
AudioData();
void init();
~AudioData();
friend class AudioSource;
};
}

View File

@ -6,6 +6,5 @@
# Sources
target_sources(${DAWN_TARGET_NAME}
PRIVATE
AudioData.cpp
AudioManager.cpp
)

View File

@ -5,5 +5,4 @@
#pragma once
#include <AL/al.h>
#include <AL/alc.h>
#include "AudioFile.h"
#include <AL/alc.h>

View File

@ -12,146 +12,120 @@ AudioSource::AudioSource(SceneItem *i) : SceneItemComponent(i) {
}
void AudioSource::updateAudioSource() {
if(!this->ready || data == nullptr || !data->ready) return;
void AudioSource::fillBuffer(ALuint buffer) {
// Read in some data
uint8_t *temporaryBuffer = (uint8_t*)memoryAllocate(AUDIO_SOURCE_BUFFER_SIZE);
/*
OK A big writeup here because things get complicated and I am sure that I
will need to revist this.
First thing to note is that OpenAL requires that the data buffered is a
multiple of the frame size. Frame Size from what I can tell is;
dataSize * channelCount
e.g. sizeof(int16_t) * stereo = 4
switch(this->requestedAudioState) {
// When the engine wants the sound to stop
case AUDIO_SOURCE_STATE_STOPPED:
if(this->currentAudioState == AUDIO_SOURCE_STATE_STOPPED) return;
this->currentAudioState = AUDIO_SOURCE_STATE_STOPPED;
alSourceStop(this->source);
this->eventStopped.invoke();
EDIT 23017 - Something about how OpenAL handles things means the following
statement is probably false, I'm leaving it here but DON'T READ IT PLEASE
Additionally, we need to handle cases where the buffer "wraps around". So
let's say we are at 8179/8192 bytes, we go to read "128 bytes", we need to
first read the remaining 13 bytes and then the remaining (128-13)bytes.
After that we need to do our frame size division, and make sure we only
advance our buffer by that many bytes, so that the next buffer can pick up
exactly where this buffer left off.
*/
// Try to read the entire buffer
this->data->loader.setPosition(this->data->bufferStart + bufferPosition);
size_t readLength = this->data->loader.read(
temporaryBuffer, AUDIO_SOURCE_BUFFER_SIZE
);
size_t bufferLength = (readLength / this->data->frameSize) * this->data->frameSize;
// Did we run out of data?
size_t newPosition;
if(readLength < AUDIO_SOURCE_BUFFER_SIZE) {
newPosition = 0;
} else {
newPosition = bufferPosition + bufferLength;
}
// Determine format, may do this somewhere else for efficiency sake.
ALenum format;
switch(this->data->channelCount) {
case 2:
format = AL_FORMAT_STEREO16;
break;
// When the engine wants the sound to pause
case AUDIO_SOURCE_STATE_PAUSED:
if(this->currentAudioState != AUDIO_SOURCE_STATE_PLAYING) return;
this->currentAudioState = AUDIO_SOURCE_STATE_PAUSED;
alSourcePause(this->source);
this->eventPaused.invoke();
case 1:
format = AL_FORMAT_MONO16;
break;
// When the engine wants the sound to play...
case AUDIO_SOURCE_STATE_PLAYING:
switch(this->playMode) {
// This should be playing, right now.
case AUDIO_PLAY_MODE_ALWAYS:
if(this->currentAudioState == AUDIO_SOURCE_STATE_PLAYING) return;
this->currentAudioState = AUDIO_SOURCE_STATE_PLAYING;
alSourcePlay(this->source);
this->eventPlaying.invoke();
break;
case AUDIO_PLAY_MODE_UNPAUSED:
if(this->getGame()->timeManager.isPaused) {
if(this->currentAudioState != AUDIO_SOURCE_STATE_PLAYING) return;
this->currentAudioState = AUDIO_SOURCE_STATE_PAUSED;
alSourcePause(this->source);
this->eventPaused.invoke();
} else {
if(this->currentAudioState == AUDIO_SOURCE_STATE_PLAYING) return;
this->currentAudioState = AUDIO_SOURCE_STATE_PLAYING;
alSourcePlay(this->source);
this->eventPlaying.invoke();
}
break;
default:
assertUnreachable();
}
break;
default:
assertUnreachable();
}
// Buffer data
alBufferData(
buffer, format, temporaryBuffer, bufferLength, (ALsizei)this->data->sampleRate
);
// Cleanup
memoryFree(temporaryBuffer);
bufferPosition = newPosition;
}
void AudioSource::detatchBuffers() {
ALint buffersQueued;
ALuint buffersUnqueued[AUDIO_SOURCE_BUFFER_COUNT];
alGetSourcei(this->source, AL_BUFFERS_QUEUED, &buffersQueued);
if(buffersQueued > 0) {
alSourceUnqueueBuffers(this->source, buffersQueued, buffersUnqueued);
}
}
void AudioSource::attachBuffers() {
alSourceQueueBuffers(this->source, AUDIO_SOURCE_BUFFER_COUNT, this->buffers);
}
void AudioSource::onStart() {
// Create source and buffers
alGenSources((ALuint)1, &this->source);
alGenBuffers((ALuint)AUDIO_SOURCE_BUFFER_COUNT, this->buffers);
// Set up the source positions
glm::vec3 position = this->transform->getLocalPosition();
alSource3f(this->source, AL_POSITION, position.x, position.y, position.z);
alSource3f(this->source, AL_VELOCITY, 0, 0, 0);
// In future these will probably be tied to the layer
alSourcef(this->source, AL_PITCH, 1);
alSourcef(this->source, AL_GAIN, 1);
glm::vec3 position = this->transform->getLocalPosition();
alSource3f(this->source, AL_POSITION, position.x, position.y, position.z);
// Velocity is always zero for now
alSource3f(this->source, AL_VELOCITY, 0, 0, 0);
// Looping
alSourcei(this->source, AL_LOOPING, this->loop);
// Source
if(this->data != nullptr && this->data->ready) {
alSourcei(source, AL_BUFFER, this->data->buffer);
}
// Ready.
// Mark as ready.
this->ready = true;
// Update the source now we are ready.
this->updateAudioSource();
// Listen for events
this->transform->eventTransformUpdated.addListener(this, &AudioSource::onTransformUpdate);
this->getGame()->timeManager.eventTimePaused.addListener(this, &AudioSource::onGamePause);
this->getGame()->timeManager.eventTimeResumed.addListener(this, &AudioSource::onGameUnpause);
}
bool_t AudioSource::isLooping() {
return this->loop;
}
void AudioSource::setLoop(bool_t loop) {
this->loop = loop;
if(this->ready) alSourcei(this->source, AL_LOOPING, loop);
}
AudioSourceState AudioSource::getPlayingState() {
return this->requestedAudioState;
}
void AudioSource::setPlayingState(enum AudioSourceState state) {
this->requestedAudioState = state;
this->updateAudioSource();
this->getScene()->eventSceneUpdate.addListener(this, &AudioSource::onSceneUpdate);
}
AudioSourceState AudioSource::getRealState() {
return this->currentAudioState;
return this->internalState;
}
void AudioSource::play() {
this->setPlayingState(AUDIO_SOURCE_STATE_PLAYING);
void AudioSource::rewind() {
this->bufferPosition = 0;
}
void AudioSource::pause() {
this->setPlayingState(AUDIO_SOURCE_STATE_PAUSED);
}
void AudioSource::stop() {
this->setPlayingState(AUDIO_SOURCE_STATE_STOPPED);
}
// void AudioSource::rewind() {
// if(this->ready && data != nullptr && data->ready) {
// alSourceRewind(this->source);
// }
// }
AudioData * AudioSource::getAudioData() {
AudioAsset * AudioSource::getAudioData() {
return this->data;
}
void AudioSource::setAudioData(AudioData *data) {
// Set up source.
if(this->ready && data != nullptr && data->ready) {
alSourcei(source, AL_BUFFER, data->buffer);
}
void AudioSource::setAudioData(AudioAsset *data) {
this->data = data;
this->updateAudioSource();
this->rewind();
}
enum AudioSourcePlayMode AudioSource::getPlayMode() {
@ -160,27 +134,116 @@ enum AudioSourcePlayMode AudioSource::getPlayMode() {
void AudioSource::setPlayMode(enum AudioSourcePlayMode playMode) {
this->playMode = playMode;
this->updateAudioSource();
}
void AudioSource::onDispose() {
this->transform->eventTransformUpdated.removeListener(this, &AudioSource::onTransformUpdate);
this->getGame()->timeManager.eventTimePaused.removeListener(this, &AudioSource::onGamePause);
this->getGame()->timeManager.eventTimeResumed.removeListener(this, &AudioSource::onGameUnpause);
assertTrue(this->ready);
this->ready = false;
this->getScene()->eventSceneUpdate.removeListener(this, &AudioSource::onSceneUpdate);
this->transform->eventTransformUpdated.removeListener(this, &AudioSource::onTransformUpdate);
alDeleteSources((ALuint)1, &this->source);
alDeleteBuffers((ALuint)AUDIO_SOURCE_BUFFER_COUNT, this->buffers);
}
void AudioSource::onSceneUpdate() {
ALuint bufferId;
ALint buffersProcessed;
assertTrue(this->ready);
assertTrue(this->data != nullptr);
assertTrue(this->data->loaded);
// What is the user trying to do?
if(this->state == AUDIO_SOURCE_STATE_PLAYING) {
// Handle the special game-paused music-paused state.
if(this->playMode == AUDIO_PLAY_MODE_UNPAUSED && this->getGame()->timeManager.isPaused) {
if(this->internalState == AUDIO_SOURCE_STATE_PLAYING) {
// Functionally, this is the same as pausing
alSourcePause(this->source);
this->internalState = AUDIO_SOURCE_STATE_PAUSED;
}
return;// Do nothing else, at all.
}
// They are trying to play. We need to check if we are already playing
if(this->internalState == AUDIO_SOURCE_STATE_PLAYING) {
// We are good to continue buffering audio data.
buffersProcessed = 0;
alGetSourcei(this->source, AL_BUFFERS_PROCESSED, &buffersProcessed);
while(buffersProcessed > 0) {
alSourceUnqueueBuffers(this->source, 1, &bufferId);
this->fillBuffer(bufferId);
alSourceQueueBuffers(this->source, 1, &bufferId);
buffersProcessed--;
// Now, before we go to the next buffer let's see if we need to
// update the internal state
if(this->bufferPosition != 0) continue;
// We reached the end of the buffer whilst filling. What to do now?
if(this->loop) {
// We're looping so it's fine
this->eventLooped.invoke();
continue;
}
// We are NOT looping, so we need to finish here.
this->state = AUDIO_SOURCE_STATE_STOPPED;
this->eventFinished.invoke();
this->bufferPosition = 0;
buffersProcessed = 0;
}
} else if(this->internalState == AUDIO_SOURCE_STATE_STOPPED) {
// Not yet playing. First thing we need to do is fill the buffers.
for(int32_t i = 0; i < AUDIO_SOURCE_BUFFER_COUNT; i++) {
this->fillBuffer(this->buffers[i]);
}
// Now attach the buffers
this->attachBuffers();
// And begin playing
alSourcePlay(this->source);
this->internalState = AUDIO_SOURCE_STATE_PLAYING;
this->eventPlaying.invoke();
} else if(AUDIO_SOURCE_STATE_PAUSED) {
// Request to resume
alSourcePlay(this->source);
this->eventResumed.invoke();
this->internalState = AUDIO_SOURCE_STATE_PLAYING;
} else {
assertUnreachable();
}
} else if(this->state == AUDIO_SOURCE_STATE_PAUSED) {
// Pause has been requested.
if(this->internalState != AUDIO_SOURCE_STATE_PLAYING) return;
// We are playing something, pause it
alSourcePause(this->source);
this->internalState = AUDIO_SOURCE_STATE_PAUSED;
this->eventPaused.invoke();
} else if(this->state == AUDIO_SOURCE_STATE_STOPPED) {
if(this->internalState == AUDIO_SOURCE_STATE_STOPPED) return;
// Release the buffers
alSourceStop(this->source);
this->detatchBuffers();
this->internalState = AUDIO_SOURCE_STATE_STOPPED;
this->eventStopped.invoke();
this->bufferPosition = 0;
} else {
assertUnreachable();
}
}
void AudioSource::onTransformUpdate() {
glm::vec3 position = this->transform->getWorldPosition();
alSource3f(this->source, AL_POSITION, position.x, position.y, position.z);
}
void AudioSource::onGamePause() {
this->updateAudioSource();
}
void AudioSource::onGameUnpause() {
this->updateAudioSource();
}

View File

@ -6,7 +6,10 @@
#pragma once
#include "dawnopenal.hpp"
#include "scene/SceneItemComponent.hpp"
#include "audio/AudioData.hpp"
#include "asset/assets/AudioAsset.hpp"
#define AUDIO_SOURCE_BUFFER_COUNT 3
#define AUDIO_SOURCE_BUFFER_SIZE 8192
namespace Dawn {
enum AudioSourceState {
@ -24,13 +27,13 @@ namespace Dawn {
private:
ALuint source;
bool_t ready = false;
ALuint buffers[AUDIO_SOURCE_BUFFER_COUNT];
size_t bufferPosition = 0;
// Settings
bool_t loop = false;
enum AudioSourceState requestedAudioState = AUDIO_SOURCE_STATE_STOPPED;
enum AudioSourceState currentAudioState = AUDIO_SOURCE_STATE_STOPPED;
enum AudioSourceState internalState = AUDIO_SOURCE_STATE_STOPPED;
int32_t layer = 0;
AudioData *data = nullptr;
AudioAsset *data = nullptr;
AudioSourcePlayMode playMode = AUDIO_PLAY_MODE_UNPAUSED;
/**
@ -38,15 +41,24 @@ namespace Dawn {
*/
void updateAudioSource();
void fillBuffer(ALuint buffer);
void detatchBuffers();
void attachBuffers();
// Events
void onSceneUpdate();
void onTransformUpdate();
void onGamePause();
void onGameUnpause();
public:
bool_t loop = false;
enum AudioSourceState state = AUDIO_SOURCE_STATE_STOPPED;
Event<> eventPlaying;
Event<> eventResumed;
Event<> eventPaused;
Event<> eventStopped;
Event<> eventFinished;
Event<> eventLooped;
/**
* Creates an Audio Source item component.
@ -55,38 +67,6 @@ namespace Dawn {
*/
AudioSource(SceneItem *item);
/**
* Returns whether or not the audio source is set to loop or not.
*
* @return True if the source is looping otherwise false.
*/
bool_t isLooping();
/**
* Sets whether the audio source should loop or not.
*
* @param loop Loop or not.
*/
void setLoop(bool_t loop);
/**
* Returns the requested playing state of the source. Due to some weird
* limitations of hardware, as well as some nice controls to decide how
* sources should play, this is basically "What you would like the sound
* to do" mode, so you really should be controlling this value where
* possible.
*
* @return The requested playing state.
*/
AudioSourceState getPlayingState();
/**
* Sets the ideal playing state you want of this audio source.
*
* @param state State to set.
*/
void setPlayingState(enum AudioSourceState state);
/**
* Returns the real current state of the sound. Refer to getPlayingState
* but basically the actually requested play state and what the hardware
@ -97,33 +77,18 @@ namespace Dawn {
*/
AudioSourceState getRealState();
/**
* Plays the audio source.
*/
void play();
/**
* Pauses the audio source.
*/
void pause();
/**
* Stops the audio source.
*/
void stop();
/**
* Rewinds the audio source. At the time of writing this comment (23017) I
* am unsure of the behaviour when rewinding whilst the source is active.
*/
// void rewind();
void rewind();
/**
* Returns the current audio data for this source.
*
* @return Audio data that is atached to this source.
*/
AudioData * getAudioData();
AudioAsset * getAudioData();
/**
* Sets the audio data for this source. Currently switching between the
@ -131,7 +96,7 @@ namespace Dawn {
*
* @param data Data to set.
*/
void setAudioData(AudioData *data);
void setAudioData(AudioAsset *data);
/**
* Returns the play mode for the source.