Skip to content

Commit

Permalink
Merge pull request #599 from novelrt/feature/audio-system
Browse files Browse the repository at this point in the history
Platform-specific Audio
  • Loading branch information
RubyNova authored Jul 29, 2024
2 parents 7bd1b57 + fa7b475 commit 5ad1ba6
Show file tree
Hide file tree
Showing 37 changed files with 1,173 additions and 1,294 deletions.
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@ if(NOT DEFINED NOVELRT_TARGET)
set(NOVELRT_TARGET "Win32" CACHE STRING "")
elseif(APPLE)
set(NOVELRT_TARGET "macOS" CACHE STRING "")
find_library(AVFOUNDATION_LIB AVFoundation)
find_library(FOUNDATION_LIB Foundation)
find_library(OBJC_LIB ObjC)
elseif(UNIX)
set(NOVELRT_TARGET "Linux" CACHE STRING "")
else()
set(NOVELRT_TARGET "Unknown" CACHE STRING "")
endif()
endif()
message("Using OS: ${NOVELRT_TARGET}")

#
# Prepend so that our FindVulkan gets picked up first when needed
Expand All @@ -64,10 +68,11 @@ option(NOVELRT_BUILD_DEPS_WITH_MAX_CPU "Use all available CPU processing power w
#
set(NOVELRT_DOXYGEN_VERSION "1.8.17" CACHE STRING "Doxygen version")
set(NOVELRT_FLAC_VERSION "1.3.4" CACHE STRING "FLAC version")
set(NOVELRT_FMT_VERSION "10.2.1" CACHE STRING "FMT version")
set(NOVELRT_GLFW_VERSION "3.3.7" CACHE STRING "GLFW3 version")
set(NOVELRT_GSL_VERSION "4.0.0" CACHE STRING "Microsoft.GSL version")
set(NOVELRT_ONETBB_VERSION "2021.5.0" CACHE STRING "OneTBB version")
set(NOVELRT_OPENAL_VERSION "1.21.1" CACHE STRING "OpenAL version")
set(NOVELRT_OPENAL_VERSION "1.23.1" CACHE STRING "OpenAL version")
set(NOVELRT_OGG_VERSION "1.3.5" CACHE STRING "Ogg version")
set(NOVELRT_OPUS_VERSION "1.3.1" CACHE STRING "Opus version")
set(NOVELRT_PNG_VERSION "1.6.35" CACHE STRING "PNG version")
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ The dependencies that are handled by CMake that do not need to be manually insta
- GLFW 3.3.7
- glm 0.9.9.9
- gtest/gmock 1.11.0
- fmt 10.2.1
- libpng 1.6.35
- libsndfile 1.1.0
- Microsoft GSL 4.0.0
- OneTBB 2021.5.0
- OpenAL 1.21.1
- OpenAL 1.23.1
- spdlog 1.13.0

### Build instructions
Expand Down Expand Up @@ -144,7 +145,7 @@ cmake .. -DCMAKE_APPLE_SILICON_PROCESSOR="arm64"

If Vulkan SDK is not installed in a system path and the `setup-env.sh` file did not properly add the required environment variables, you can specify the `VULKAN_SDK` environment variable to your local Vulkan SDK location as such:
```
VULKAN_SDK=/Users/youruser/Vulkan SDK/1.3.231.1/macOS cmake ..
VULKAN_SDK=/Users/youruser/Vulkan SDK/1.3.231.1/macOS cmake ..
```
Please ensure that the path includes the macOS folder, otherwise finding the proper libraries will fail.

Expand Down
200 changes: 200 additions & 0 deletions audio/AVAudioEngine/AVAudioEngineAudioProvider.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright © Matt Jones and Contributors. Licensed under the MIT Licence (MIT). See LICENCE.md in the repository root
// for more information.
#include <NovelRT/Exceptions/Exceptions.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#import <NovelRT/Audio/AVAudioEngine/AVAudioEngineAudioProvider.hpp>
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <AVFAudio/AVAudioEngine.h>

namespace NovelRT::Audio::AVAudioEngine
{
AVAudioEngineAudioProvider::AVAudioEngineAudioProvider():
_buffers(std::map<uint32_t, ::AVAudioPCMBuffer*>()),
_sources(std::map<uint32_t, ::AVAudioPlayerNode*>()),
_sourceEQUnits(std::map<uint32_t, ::AVAudioUnitEQ*>()),
_sourceStates(std::map<uint32_t, AudioSourceState>()),
_sourceContexts(std::map<uint32_t, AudioSourceContext>()),
_logger(spdlog::stdout_color_mt("AVAudioEngine"))
{
//Logger init
_logger->set_level(spdlog::level::debug);

//Device and Context Init
@try
{
::NSError* err;
_logger->debug("calling alloc and init");
_impl = [::AVAudioEngine new];
}
@catch(::NSException* ex)
{
std::string err = std::string([ex.reason UTF8String]);
_logger->error(err);
throw new Exceptions::InitialisationFailureException("Failed to initialise AVAudioEngine!", err);
}
}

AVAudioEngineAudioProvider::~AVAudioEngineAudioProvider()
{
Dispose();
[(::AVAudioEngine*)_impl stop];
[(::AVAudioEngine*)_impl release];
}

void AVAudioEngineAudioProvider::Dispose()
{
for(auto [id, source] : _sources)
{
[(::AVAudioPlayerNode*)source stop];
[(::AVAudioPlayerNode*)source release];
}
_sources.clear();

for(auto [id, source] : _sourceEQUnits)
{
[(::AVAudioUnitEQ*)source release];
}
_sourceEQUnits.clear();

for(auto [id, buffer] : _buffers)
{
[(::AVAudioPCMBuffer*)buffer release];
}
_buffers.clear();

_sourceContexts.clear();
}

uint32_t AVAudioEngineAudioProvider::OpenSource(AudioSourceContext& context)
{
unused(context);
return _sourceCounter;
}

uint32_t AVAudioEngineAudioProvider::OpenSourceInternal(AudioSourceContext& context, ::AVAudioPCMBuffer* buffer, ::AVAudioFormat* format)
{
uint32_t nextSource = ++_sourceCounter;
::AVAudioPlayerNode* node = [::AVAudioPlayerNode new];
[_impl attachNode:node];
_sources.emplace(nextSource, node);
_sourceStates.emplace(nextSource, AudioSourceState::SOURCE_STOPPED);
_sourceContexts.emplace(nextSource, context);

::AVAudioUnitEQ* eq = [[::AVAudioUnitEQ alloc] initWithNumberOfBands: 1];
[_impl attachNode:eq];
_sourceEQUnits.emplace(nextSource, eq);

_logger->debug("Connecting source {0} to EQ", nextSource);
[(::AVAudioEngine*)_impl connect:node to:eq format:nil];
_logger->debug("Connecting source {0} EQ to mixer", nextSource);
[(::AVAudioEngine*)_impl connect:eq to:_impl.mainMixerNode format:nil];
_buffers.emplace(nextSource, buffer);

return nextSource;
}

void AVAudioEngineAudioProvider::PlaySource(uint32_t sourceId)
{
if(!_impl.running)
{
_logger->debug("filling up gas tank...");
[(::AVAudioEngine*)_impl prepare];
::NSError* err;
_logger->debug("starting engine.... VRROOOOOOOMMMMM....");
[_impl startAndReturnError: &err];
if(!_impl.running)
{
_logger->error("Could not start engine: {0}", std::string([err.localizedDescription UTF8String]));
}
}

::AVAudioPlayerNode* node = _sources.at(sourceId);

if(!node.playing)
{
auto lambda = [this, sourceId = sourceId]()
{
this->UpdateSourceState(sourceId, AudioSourceState::SOURCE_STOPPED);
};
if(_sourceContexts.at(sourceId).Loop)
{
_logger->debug("Looping source ID {0}", sourceId);
[node scheduleBuffer: _buffers.at(sourceId) atTime: nil options: AVAudioPlayerNodeBufferLoops completionHandler: nil];
}
else
{
[node scheduleBuffer: _buffers.at(sourceId) completionHandler: lambda];
}




[node play];
}

_sourceStates[sourceId] = AudioSourceState::SOURCE_PLAYING;
}

void AVAudioEngineAudioProvider::UpdateSourceState(uint32_t sourceId, AudioSourceState state)
{
_sourceStates[sourceId] = state;
}

void AVAudioEngineAudioProvider::StopSource(uint32_t sourceId)
{
::AVAudioPlayerNode* node = _sources.at(sourceId);
if(node.playing)
{
[(::AVAudioPlayerNode*)node stop];
}
_sourceStates[sourceId] = AudioSourceState::SOURCE_STOPPED;
}

void AVAudioEngineAudioProvider::PauseSource(uint32_t sourceId)
{
::AVAudioPlayerNode* node = _sources.at(sourceId);
if(node.playing)
{
[(::AVAudioPlayerNode*)node pause];
}
_sourceStates[sourceId] = AudioSourceState::SOURCE_PAUSED;
}

uint32_t AVAudioEngineAudioProvider::SubmitAudioBuffer(const NovelRT::Utilities::Misc::Span<float> buffer, AudioSourceContext& context)
{
_logger->debug("Loading audio buffer - SampleRate: {0}, Channels: {1}", context.SampleRate, context.Channels);

uint32_t frameCap = buffer.size() * sizeof(float);
::AVAudioFormat* format = [[::AVAudioFormat alloc] initWithCommonFormat:AVAudioCommonFormat::AVAudioPCMFormatFloat32 sampleRate:44100 channels:context.Channels interleaved:true];
::AVAudioFormat* deformat = [[::AVAudioFormat alloc] initWithCommonFormat:AVAudioCommonFormat::AVAudioPCMFormatFloat32 sampleRate:44100 channels:context.Channels interleaved:false];
AudioBufferList abl;
abl.mNumberBuffers = 1;
abl.mBuffers[0].mData = ((void *)new Byte[frameCap]);
abl.mBuffers[0].mNumberChannels = context.Channels;
abl.mBuffers[0].mDataByteSize = frameCap;

std::memcpy((void*)abl.mBuffers[0].mData, reinterpret_cast<void*>(buffer.data()), frameCap);

::AVAudioPCMBuffer* pcmBuffer = [[::AVAudioPCMBuffer alloc] initWithPCMFormat:format bufferListNoCopy:&abl deallocator:NULL];

::AVAudioConverter* convert = [[::AVAudioConverter alloc] initFromFormat:format toFormat:deformat];
::AVAudioPCMBuffer* deinterleavedBuffer = [[::AVAudioPCMBuffer alloc] initWithPCMFormat:deformat frameCapacity:pcmBuffer.frameCapacity];
[convert convertToBuffer:deinterleavedBuffer fromBuffer:pcmBuffer error:nil];

return OpenSourceInternal(context, deinterleavedBuffer, format);
}

void AVAudioEngineAudioProvider::SetSourceProperties(uint32_t sourceId, AudioSourceContext& context)
{
_sourceContexts[sourceId] = context;

::AVAudioUnitEQ* eq = _sourceEQUnits.at(sourceId);
eq.globalGain = context.Volume;
}

AudioSourceState AVAudioEngineAudioProvider::GetSourceState(uint32_t sourceId)
{
return _sourceStates.at(sourceId);
}
}
99 changes: 99 additions & 0 deletions audio/AudioMixer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright © Matt Jones and Contributors. Licensed under the MIT Licence (MIT). See LICENCE.md in the repository root
// for more information.
#include <NovelRT/Audio/AudioMixer.hpp>

//Conditional
#if defined(_WIN32)
#include <NovelRT/Audio/XAudio2/XAudio2AudioProvider.hpp>
#elif __APPLE__
#include <NovelRT/Audio/AVAudioEngine/AVAudioEngineAudioProvider.hpp>
#else
#include <NovelRT/Audio/OpenAL/OpenALAudioProvider.hpp>
#endif
namespace NovelRT::Audio
{
void AudioMixer::Initialise()
{
_sourceContextCache = std::map<uint32_t, AudioSourceContext>();
#if defined(_WIN32)
_audioProvider = std::make_unique<XAudio2::XAudio2AudioProvider>();
#elif defined(__APPLE__)
_audioProvider = std::make_unique<AVAudioEngine::AVAudioEngineAudioProvider>();
#else
_audioProvider = std::make_unique<OpenAL::OpenALAudioProvider>();
#endif
}

uint32_t AudioMixer::SubmitAudioBuffer(const NovelRT::Utilities::Misc::Span<float> buffer, int32_t channelCount, int32_t originalSampleRate)
{
auto newContext = AudioSourceContext{};
newContext.Channels = channelCount;
newContext.SampleRate = originalSampleRate;
uint32_t sourceId = _audioProvider->SubmitAudioBuffer(buffer, newContext);
_sourceContextCache.emplace(sourceId, newContext);
return sourceId;
}

AudioSourceState AudioMixer::GetSourceState(uint32_t id)
{
return _audioProvider->GetSourceState(id);
}

void AudioMixer::PlaySource(uint32_t id)
{
_audioProvider->PlaySource(id);
}

void AudioMixer::StopSource(uint32_t id)
{
_audioProvider->StopSource(id);
}

void AudioMixer::PauseSource(uint32_t id)
{
_audioProvider->PauseSource(id);
}

AudioSourceContext& AudioMixer::GetSourceContext(uint32_t id)
{
return _sourceContextCache.at(id);
}

void AudioMixer::SetSourceContext(uint32_t id, AudioSourceContext& context)
{
_sourceContextCache.erase(id);
_sourceContextCache.emplace(id, context);
_audioProvider->SetSourceProperties(id, context);
}

void AudioMixer::SetSourceVolume(uint32_t id, float volume)
{
auto& context = _sourceContextCache.at(id);
context.Volume = volume;
_audioProvider->SetSourceProperties(id, context);
}

void AudioMixer::SetSourcePitch(uint32_t id, float pitch)
{
auto& context = _sourceContextCache.at(id);
context.Pitch = pitch;
_audioProvider->SetSourceProperties(id, context);
}

void AudioMixer::SetSourceLoop(uint32_t id, bool isLooping)
{
auto& context = _sourceContextCache.at(id);
context.Loop = isLooping;
_audioProvider->SetSourceProperties(id, context);
}

void AudioMixer::TearDown()
{
_audioProvider.reset();
}

AudioMixer::~AudioMixer()
{
TearDown();
}
}
Loading

0 comments on commit 5ad1ba6

Please sign in to comment.