VEX Engine
Retro looking game engine made in vulkan mostly because i wanted to understand it better.
Loading...
Searching...
No Matches
AudioSystem.cpp
2#include "components/ErrorUtils.hpp"
3#include "../../../thirdparty/stb/stb_vorbis.c"
4
5namespace vex {
6
7 AudioClip::AudioClip(const std::string& path, vex::VirtualFileSystem* vfs) {
8 if (!vfs) return;
9
10 auto fileData = vfs->load_file(path);
11 if (!fileData) {
12 vex::log(vex::LogLevel::ERROR, "AudioClip: VFS failed to load path: %s", path.c_str());
13 return;
14 }
15
16 bool isOgg = (path.length() >= 4 && path.substr(path.length() - 4) == ".ogg");
17
18 if (isOgg) {
19 int channels, sampleRate;
20 short* decodedData;
21
22 int samplesPerChannel = stb_vorbis_decode_memory(
23 reinterpret_cast<const unsigned char*>(fileData->data.data()),
24 fileData->size,
25 &channels,
26 &sampleRate,
27 &decodedData
28 );
29
30 if (samplesPerChannel >= 0) {
31 SDL_zero(spec);
32 spec.freq = sampleRate;
33 spec.format = SDL_AUDIO_S16;
34 spec.channels = channels;
35
36 buffer = reinterpret_cast<Uint8*>(decodedData);
37 length = samplesPerChannel * channels * sizeof(short);
38
39 valid = true;
40 isVorbisAllocated = true;
41 } else {
42 vex::log(vex::LogLevel::ERROR, "AudioClip: stb_vorbis failed to decode: %s", path.c_str());
43 }
44 }
45 else {
46 SDL_IOStream* io = SDL_IOFromConstMem(fileData->data.data(), fileData->size);
47 if (SDL_LoadWAV_IO(io, true, &spec, &buffer, &length)) {
48 valid = true;
49 } else {
50 vex::log(vex::LogLevel::ERROR, "AudioClip: SDL_LoadWAV failed for %s. SDL Error: %s", path.c_str(), SDL_GetError());
51 }
52 }
53 }
54
56}
57
59 this->vfs = vfs;
60
61 registry.on_destroy<AudioSourceComponent>([this](vex::Entity entity, AudioSourceComponent&) {
62 OnAudioComponentDestroyed(registry, entity);
63 });
64
65 if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
66 throw_error("Failed to initialize audio subsystem");
67 }
68
69 deviceID = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
70}
71
72void AudioSystem::Update(vex::Entity cameraEntity) {
73 glm::vec3 listenerPos = glm::vec3(0.0f);
74 if (registry.has<TransformComponent>(cameraEntity)) {
75 listenerPos = registry.get<TransformComponent>(cameraEntity).getWorldPosition();
76 }else{
77 log(LogLevel::ERROR, "No valid camera entity found");
78 }
79
80 vex::View<AudioSourceComponent>(registry).each([&, this, listenerPos](vex::Entity entity, AudioSourceComponent& comp) {
81 if (comp.autoPlay) {
82 comp.Play();
83 comp.autoPlay = false;
84 }
85
86 if (comp.getAudioClip() == nullptr && !comp.audioFilePath.empty()) {
87 if (clipCache.find(comp.audioFilePath) == clipCache.end()) {
88 auto newClip = std::make_unique<AudioClip>(comp.audioFilePath, vfs);
89 if (newClip->valid) {
90 clipCache[comp.audioFilePath] = std::move(newClip);
91 } else {
92 comp.Stop();
93 comp.stateDirty = false;
94 return;
95 }
96 }
97 comp.setAudioClip(clipCache[comp.audioFilePath].get());
98 }
99
100 if (comp.stateDirty) {
101 SDL_AudioStream* stream = streamMap[entity];
102
103 if (comp.getState() == AudioState::PLAYING) {
104 if (comp.getAudioClip() && comp.getAudioClip()->valid) {
105 if (stream) SDL_DestroyAudioStream(stream);
106
107 stream = SDL_CreateAudioStream(&comp.getAudioClip()->spec, NULL);
108 SDL_BindAudioStream(deviceID, stream);
109 SDL_PutAudioStreamData(stream, comp.getAudioClip()->buffer, comp.getAudioClip()->length);
110
111 streamMap[entity] = stream;
112 }
113 }
114 else if (comp.getState() == AudioState::STOPPED) {
115 if (stream) {
116 SDL_DestroyAudioStream(stream);
117 streamMap.erase(entity);
118 stream = nullptr;
119 }
120 }
121 else if (comp.getState() == AudioState::PAUSED) {
122 if (stream) SDL_UnbindAudioStream(stream);
123 }
124
125 comp.stateDirty = false;
126 }
127
128 if (streamMap.find(entity) == streamMap.end()) {
129 return;
130 }
131 SDL_AudioStream* stream = streamMap[entity];
132
133 if (!stream) {
134 return;
135 }
136
137 if (comp.loop && SDL_GetAudioStreamAvailable(stream) < comp.getAudioClip()->length / 2) {
138 SDL_PutAudioStreamData(stream, comp.getAudioClip()->buffer, comp.getAudioClip()->length);
139 }
140
141 if (!comp.loop && SDL_GetAudioStreamAvailable(stream) == 0) {
142 SDL_DestroyAudioStream(stream);
143 streamMap.erase(entity);
144 comp.Stop();
145 comp.stateDirty = false;
146 return;
147 }
148
149 float finalVolume = comp.volume;
150 if (comp.is3D) {
151 if (registry.has<TransformComponent>(entity)) {
152 auto& transform = registry.get<TransformComponent>(entity);
153 float dist = glm::distance(listenerPos, transform.getWorldPosition());
154
155 if (dist > comp.distance) {
156 finalVolume = 0.0f;
157 } else {
158 finalVolume = comp.volume * (1.0f - (dist / comp.distance));
159 }
160 }
161 }
162
163 SDL_SetAudioStreamGain(stream, finalVolume);
164 SDL_SetAudioStreamFrequencyRatio(stream, comp.pitch);
165 });
166
167 for (auto it = standaloneStreams.begin(); it != standaloneStreams.end(); ) {
168 SDL_AudioStream* stream = it->stream;
169
170 if (it->loop && SDL_GetAudioStreamAvailable(stream) < it->clip->length / 2) {
171 SDL_PutAudioStreamData(stream, it->clip->buffer, it->clip->length);
172 ++it;
173 }
174 else if (!it->loop && SDL_GetAudioStreamAvailable(stream) == 0) {
175 SDL_DestroyAudioStream(stream);
176 it = standaloneStreams.erase(it);
177 }
178 else {
179 ++it;
180 }
181 }
182}
183
185 for (auto& sa : standaloneStreams) {
186 SDL_DestroyAudioStream(sa.stream);
187 }
188 standaloneStreams.clear();
189
190 for (auto& [entity, stream] : streamMap) {
191 SDL_DestroyAudioStream(stream);
192 }
193 streamMap.clear();
194 clipCache.clear();
195 SDL_CloseAudioDevice(deviceID);
196}
197
199 auto it = streamMap.find(entity);
200 if (it != streamMap.end()) {
201 SDL_DestroyAudioStream(it->second);
202 streamMap.erase(it);
203 }
204}
205
206void AudioSystem::PlaySound2D(const std::string& filePath, float volume, float pitch, bool loop) {
207 try{
208 if (clipCache.find(filePath) == clipCache.end()) {
209 auto newClip = std::make_unique<AudioClip>(filePath, vfs);
210 if (newClip->valid) {
211 clipCache[filePath] = std::move(newClip);
212 } else {
213 log(LogLevel::ERROR, "PlaySound2D: Failed to load audio clip - %s", filePath.c_str());
214 return;
215 }
216 }
217
218 AudioClip* clip = clipCache[filePath].get();
219
220 SDL_AudioStream* stream = SDL_CreateAudioStream(&clip->spec, NULL);
221 if (!stream) return;
222
223 SDL_SetAudioStreamGain(stream, volume);
224 SDL_SetAudioStreamFrequencyRatio(stream, pitch);
225
226 SDL_BindAudioStream(deviceID, stream);
227 SDL_PutAudioStreamData(stream, clip->buffer, clip->length);
228
229 standaloneStreams.push_back({stream, clip, loop});
230 } catch (const std::exception& e) {
231 log(LogLevel::ERROR, "PlaySound2D: Failed to play sound - %s", e.what());
232 }
233}
234
235}
Audio system for managing audio sources, playback, and 3D spatialization.
AudioSystem(vex::Registry &reg)
Constructor for AudioSystem.
std::unordered_map< vex::Entity, SDL_AudioStream * > streamMap
Map linking entities to their active SDL audio streams.
void PlaySound2D(const std::string &filePath, float volume=1.0f, float pitch=1.0f, bool loop=false)
Quickly plays a 2D sound without requiring an entity or AudioSourceComponent.
void Shutdown()
Shuts down the audio system.
void Init(vex::VirtualFileSystem *vfs)
Initializes the audio subsystem and binding to the VFS.
std::unordered_map< std::string, std::unique_ptr< AudioClip > > clipCache
Cache of loaded audio clips to prevent reloading the same file multiple times. Key is the file path.
void Update(vex::Entity cameraEntity)
Main update loop for the audio system.
void OnAudioComponentDestroyed(vex::Registry &registry, vex::Entity entity)
Callback triggered when an AudioSourceComponent is destroyed.
Central registry for managing entities and their components in the ECS system.
Definition Registry.hpp:29
Provides iteration over entities that have all specified component types.
Definition View.hpp:17
void each(Func func)
Iterates over all entities with the specified component types.
Definition View.hpp:40
This class provides abstraction of file system needed for loading packed and unpacked assets.
std::unique_ptr< FileData > load_file(const std::string &virtual_path)
Loads a file into memory from the specified path.
uint32_t Entity
Type alias representing a unique entity identifier in the ECS system.
Definition Types.hpp:11
void VEX_EXPORT throw_error(const std::string &msg)
Throws an error or terminates the application.
void VEX_EXPORT log(const char *fmt,...)
Logs a formatted message.
Struct containing transform data and methods.