Conversation
Provides a more backend-independent audio system Also fixes some sounds being played at half speed (chapter introductions)
src/audio/AudioWorld.cpp
Outdated
| alGenBuffers(RE_NUM_MUSIC_BUFFERS, m_musicBuffers); | ||
| alGenSources(1, &m_musicSource); | ||
| m_MusicSource = m_Engine.getAudioEngine().createSound( | ||
| [&](std::int16_t* buf, std::size_t len) -> int { |
There was a problem hiding this comment.
nit: as types of buf and len aren't directly needed in lambda, you can use auto.
src/audio/AudioWorld.cpp
Outdated
| return Handle::SfxHandle::makeInvalidHandle(); | ||
| } | ||
| return Handle::SfxHandle::makeInvalidHandle(); | ||
| const std::int16_t* dataPtr = (std::int16_t*)(wav.getData()); |
There was a problem hiding this comment.
I'd try to avoid old style cast.
why?
There was a problem hiding this comment.
You don't really know what is happening there. And it's quite hard to refactor.
src/audio/AudioEngine.h
Outdated
| virtual void stop() = 0; | ||
| }; | ||
|
|
||
| using SoundRef = std::shared_ptr<Sound>; |
src/audio/OpenALAudioEngine.cpp
Outdated
| , m_engine(engine) | ||
| {} | ||
| public: | ||
| OpenALSound(OpenALAudioEngine* engine, const std::int16_t* buf, std::size_t len, Format fmt, std::size_t samplingFreq) |
src/audio/OpenALAudioEngine.cpp
Outdated
| captureContext(); | ||
| // Remove all expired sounds | ||
| std::remove_if(std::begin(m_bufferedSounds), std::end(m_bufferedSounds), | ||
| [](const std::shared_ptr<OpenALSound>& sound) { return sound.use_count() == 1; }); |
src/audio/OpenALAudioEngine.cpp
Outdated
| std::remove_if(std::begin(m_bufferedSounds), std::end(m_bufferedSounds), | ||
| [](const std::shared_ptr<OpenALSound>& sound) { return sound.use_count() == 1; }); | ||
| std::remove_if(std::begin(m_streamingSounds), std::end(m_streamingSounds), | ||
| [](const std::shared_ptr<OpenALSound>& sound) { return sound.use_count() == 1; }); |
| { | ||
| std::array<std::int16_t, BUFFER_SIZE> dataBuffer{}; | ||
|
|
||
| for(auto buf : m_buffers) |
There was a problem hiding this comment.
If you are referring to auto buf, m_buffers is a vector of integers, it should not make any difference here
src/audio/AudioWorld.h
Outdated
| Daedalus::GEngineClasses::C_SFX sfx; | ||
| std::vector<Handle::SfxHandle> variants; // Instances ending with "_Ax" | ||
| unsigned m_Handle = 0; | ||
| Audio::SoundRef sound = nullptr; |
src/audio/OpenALAudioEngine.cpp
Outdated
|
|
||
| OpenALAudioEngine::~OpenALAudioEngine() | ||
| { | ||
| m_bufferedSounds.erase(std::begin(m_bufferedSounds), std::end(m_bufferedSounds)); |
There was a problem hiding this comment.
m_bufferedSounds.clear(); will do the same // I'd add comment why you do it
|
Tested your build. The chapter music works fine, but it seems that musiczones got somehow messed up. When you enter Khorinis, for example, music doesn't switch until you reach the place in front of Vatras. Also, I had the impression that just by turning the Hero, it had an impact on the music (directly in front of Lobart, there was a weird switching in between the lobart and the regular world theme). |
|
I'm marking this as WIP, I'd like to make some more updates like making the AudioWorld live in the BaseEngine instead of the World (there are no ties between it and the AudioWorld), to allow stuff like playing music and sound effects before a game is started |
src/audio/OpenALAudioEngine.cpp
Outdated
|
|
||
| using namespace Audio; | ||
|
|
||
| static std::vector<std::string> enumeratedDevices; |
There was a problem hiding this comment.
maybe instead of global static use anonymous namespace?
There was a problem hiding this comment.
I don't really see the need, it's just going to be used in this file only, I think. Actually, it could even be declared as static inside the function that uses it 🤔
There was a problem hiding this comment.
Even better, we could also stick to the YAGNI principle and remove the functionality outright...!
| // can be accessed in a thread-safe manner | ||
| static std::mutex mtx_enumeratedDevices; | ||
|
|
||
| constexpr std::size_t OPENAL_SOURCES_POOL = 256; |
There was a problem hiding this comment.
Don't know regoth naming convection, but it looks like macro name.
There was a problem hiding this comment.
Yeah the rationale is that that variable is essentially to be considered a macro, but let the compiler treat it as a variable. If that makes sense anyway.
src/audio/OpenALAudioEngine.cpp
Outdated
| } | ||
| else | ||
| { | ||
| enumeratedDevices.push_back(""); |
|
|
||
| OpenALAudioEngine::~OpenALAudioEngine() | ||
| { | ||
| m_bufferedSounds.clear(); |
There was a problem hiding this comment.
IIRC buffers of openAL should be removed earlier than "closing device". I guess comment explaining why it is here will be helpful.
src/audio/OpenALAudioEngine.cpp
Outdated
| { | ||
| protected: | ||
| OpenALSound(OpenALAudioEngine* engine) | ||
| : m_buffer(0) |
There was a problem hiding this comment.
You can use default member initializer and make ctors simpler.
src/engine/BaseEngine.h
Outdated
| EngineArgs m_Args; | ||
|
|
||
| Audio::AudioEngine* m_AudioEngine = nullptr; | ||
| std::shared_ptr<Audio::AudioEngine> m_AudioEngine; |
|
|
||
| #include <string> | ||
|
|
||
| typedef struct ALCdevice_struct ALCdevice; |
There was a problem hiding this comment.
It's going to interfere with #include <al/al.h> when it's included in OpenALAudioEngine
|
@shfil119 Done addressing the new reviews. During testing Gothic 1 I noticed that some sounds are played even though they should not be heard, like In Extremo (which is now playing correctly 👍 ) that starts right as the player is spawned. Maybe it's got something to do with the |
|
Last commit should fix the issue, marking as ready for review & merge |
| { | ||
| protected: | ||
| OpenALSound(OpenALAudioEngine* engine) | ||
| : m_hasBuffer(false) |
There was a problem hiding this comment.
Also can used default member initializer.
| , m_stream(stream) | ||
| , m_format(fmt == Format::Mono ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16) | ||
| , m_sampleRate(sampleRate) | ||
| , m_stop(false) |
There was a problem hiding this comment.
Also can used default member initializer.
… large music zones
src/logic/MusicController.cpp
Outdated
| "_NGT_STD", | ||
| "_NGT_THR", | ||
| "_NGT_FGT", | ||
| { |
There was a problem hiding this comment.
Yep, this one is unwanted 😨
src/audio/OpenALAudioEngine.cpp
Outdated
| auto lambda = [](const auto& sound) { return sound.use_count() == 1; }; | ||
|
|
||
| // Remove all expired sounds | ||
| std::remove_if(std::begin(m_bufferedSounds), std::end(m_bufferedSounds), lambda); |
There was a problem hiding this comment.
Btw Is this needed? Actually it just moves objects in vector.
There was a problem hiding this comment.
What do you mean if it is needed? The shared_ptrs are never going to get deleted if they stay inside the array, this allows them to be freed
There was a problem hiding this comment.
I think about missing erase, https://cpppatterns.com/patterns/remove-elements-from-container.html
There was a problem hiding this comment.
Doh! 🤦♂️ Yeah my C++-fu is still not very strong
src/audio/OpenALAudioEngine.cpp
Outdated
|
|
||
| if(!renderBuffer(dataBuffer)) return; | ||
|
|
||
| while(true) |
There was a problem hiding this comment.
Without further reading it's hard to determine what's the purpose of loop. Some boolean with name explaining what's going on will be useful. ;)
src/audio/OpenALAudioEngine.cpp
Outdated
| ALint processedBuffers; | ||
| alGetSourcei(m_source, AL_BUFFERS_PROCESSED, &processedBuffers); | ||
| if(processedBuffers <= 0) continue; | ||
| while(processedBuffers--) |
There was a problem hiding this comment.
Also readability. Can be used range loop?
There was a problem hiding this comment.
I think at most a standard for loop could be used
| }; | ||
| Daedalus::DaedalusVM *m_SoundVM = nullptr, *m_MusicVM = nullptr; | ||
|
|
||
| struct Sound : public Handle::HandleTypeDescriptor<Handle::SfxHandle> |
There was a problem hiding this comment.
I haven't seen virtual dtor of HandleTypeDescriptor, isn't there problem with leaking memory? (by vector and string)
There was a problem hiding this comment.
I don't know the details of the memory management inside the engine, this is something @ataulien or @markusobi know better than me
| : public Sound | ||
| { | ||
| public: | ||
| void pitch(float p) override {} |
There was a problem hiding this comment.
As some parts of interface aren't implemented you can think about splitting interface into smaller ones. Idea from SOLID. ;)
There was a problem hiding this comment.
I'm not usre I understand what do you mean? The interface is fully implemented, it just doesn't do anything :)
There was a problem hiding this comment.
Well im not cpp guy but he means that having methods from contract which are not doing anything in class which implements this contract(like no body at all or throwing exeption) is overall a bad concept and you should rethink your interfaces. It should be separated interface then which this class just shouldn't implement.
It's pretty much Liskov Subtitution Principle and also ISP.
There was a problem hiding this comment.
The current way this is implemented is to allow compiling for platforms that don't support audio, or for running the game without any audio, without having to change anything about the frontend code. The NullAudioEngine provides a way to let the game engine use an AudioEngine without actually doing anything.
This is the best I could come up with, suggestions are welcome!
src/logic/MusicController.cpp
Outdated
| MusicController::MusicController(World::WorldInstance& world, Handle::EntityHandle entity) | ||
| : Controller(world, entity) | ||
| , m_isPlaying(false) {} | ||
| , m_isPlaying(false) |
src/logic/MusicController.cpp
Outdated
| // threatened or fighting to play the correct music. | ||
| if ((!m_isPlaying && isInBoundingBox()) | ||
| || (m_isPlaying && m_currentTime != time)) | ||
| if (((!m_isPlaying || currentTheme.find(m_instancePrefix) != 0) && isInBoundingBox()) || (m_isPlaying && m_currentTime != time)) |
There was a problem hiding this comment.
Too long, I'd try to reformat all your code with clang-format. ;)
|
The game crashes when you load a savegame after you started a new game. |
|
Can you confirm this is not present in master? I cannot reproduce this on my machine Also this looks like it's got something to do with bgfx/3d rendering, I'm not sure what could have introduced it from this PR |
|
@frabert Oh, I'm sorry... This crash is indeed present at the latest master-build. Just stumbled over it because I was testing your branch. |
|
That's a bug on its own right, anyway. I remember seeing something similar some time ago but could never pinpoint it to something precise, it happens sometimes when sometimes calls |
| virtual void stop() = 0; | ||
| }; | ||
|
|
||
| using SoundPtr = std::shared_ptr<Sound>; |
There was a problem hiding this comment.
I wasn't able to do it with a unique_ptr because OpenGLAudioEngine keeps reference of the sounds that have been created
There was a problem hiding this comment.
Is the destruction order of Sound and OpenGLAudioEngine unspecified?
There was a problem hiding this comment.
Considering that OpenGLAudioEngine is basically guaranteed to never be destroyed (it should be destroyed when the application closes), Sounds are always going to die before the engine. The issue is that the Engine gives out Sounds to its users, and these objects must stay alive even if they go out of scope, otherwise the sounds would stop playing. So the Engine keeps reference of all created sounds and destroys them when they are not tracked anymore by anyone else and they are not playing
| { | ||
| if (!m_hasSource) return State::Stopped; | ||
| ALint s; | ||
| alGetSourcei(m_source, AL_SOURCE_STATE, &s); |
There was a problem hiding this comment.
nit: as it returns number, you can assign these numbers in enum and cast value of s to State.
There was a problem hiding this comment.
I'm not sure I particularly like this idea, because it ties it specifically to the states that OpenAL returns, which is the reason I wrote a wrapper around it...
| std::size_t m_sampleRate; | ||
| std::atomic_bool m_stop; | ||
| std::array<ALuint, BUFFER_NUM> m_buffers{}; | ||
| std::thread m_thread; |
There was a problem hiding this comment.
nit: Probably one thread for sounds is enough.
// But probably (also) it's not convenient to implement this.
There was a problem hiding this comment.
Indeed it is a bit wasteful to have a separate thread for each streaming sound, it would have be nice to have a single thread that manages all streaming sounds at once, but it complicates the design as you said.
However, there should be very few streaming sounds going on generally (one for music, one for video sound playback when it will get implemented), so I don't think it is too bad of a perf hit.
Provides a more backend-independent audio system
Also fixes some sounds being played at half speed (chapter introductions)