1 /* libClunk - cross-platform 3D audio API built on top SDL library
2 * Copyright (C) 2007-2008 Netive Media Group
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19
20 #include <SDL.h>
21 #include <SDL_audio.h>
22 #include "context.h"
23 #include <string.h>
24 #include "sdl_ex.h"
25 #include "logger.h"
26 #include "source.h"
27 #include <assert.h>
28 #define _USE_MATH_DEFINES
29 #include <math.h>
30 #include <map>
31 #include <algorithm>
32 #include <vector>
33 #include "locker.h"
34 #include "stream.h"
35 #include "object.h"
36
37 using namespace clunk;
38
Context()39 Context::Context() : period_size(0), listener(NULL), max_sources(8), fx_volume(1), distance_model(DistanceModel::Inverse, true, 128), fdump(NULL) {
40 }
41
callback(void * userdata,Uint8 * bstream,int len)42 void Context::callback(void *userdata, Uint8 *bstream, int len) {
43 Context *self = (Context *)userdata;
44 assert(self != NULL);
45 Sint16 *stream = (Sint16*)bstream;
46 TRY {
47 self->process(stream, len);
48 } CATCH("callback", {})
49 }
50
51 namespace clunk {
52 struct source_t {
53 Source *source;
54
55 v3<float> s_pos;
56 v3<float> s_vel;
57 v3<float> s_dir;
58 v3<float> l_vel;
59
source_tclunk::source_t60 inline source_t(Source *source, const v3<float> &s_pos, const v3<float> &s_vel, const v3<float>& s_dir, const v3<float>& l_vel) :
61 source(source), s_pos(s_pos), s_vel(s_vel), s_dir(s_dir), l_vel(l_vel) {}
62 };
63 }
64
process(Sint16 * stream,int size)65 void Context::process(Sint16 *stream, int size) {
66 //TIMESPY(("total"));
67
68 v3<float> l_pos, l_vel;
69 if (listener != NULL) {
70 l_pos = listener->position;
71 l_vel = listener->velocity;
72 }
73 {
74 //TIMESPY(("sorting objects"));
75 std::sort(objects.begin(), objects.end(), Object::DistanceOrder(l_pos));
76 }
77 //LOG_DEBUG(("sorted %u objects", (unsigned)objects.size()));
78
79 std::vector<source_t> lsources;
80 int n = size / 2 / spec.channels;
81 typedef std::map<const std::string, unsigned> stats_type;
82 stats_type sources_stats;
83
84 for(objects_type::iterator i = objects.begin(); i != objects.end(); ) {
85 Object *o = *i;
86 Object::Sources & sset = o->sources;
87 if (sset.empty() && o->dead) {
88 //autodeleted object
89 delete o;
90 i = objects.erase(i);
91 continue;
92 }
93 for(Object::Sources::iterator j = sset.begin(); j != sset.end(); ) {
94 const std::string &name = j->first;
95 Source *s = j->second;
96 if (!s->playing()) {
97 //LOG_DEBUG(("purging inactive source %s", j->first.c_str()));
98 delete j->second;
99 sset.erase(j++);
100 continue;
101 }
102
103 stats_type::iterator s_i = sources_stats.find(name);
104 unsigned same_sounds_n = (s_i != sources_stats.end())? s_i->second: 0;
105
106 if (lsources.size() < max_sources && same_sounds_n < distance_model.same_sounds_limit) {
107 lsources.push_back(source_t(s, o->position + s->delta_position - l_pos, o->velocity, o->direction, l_vel));
108 if (same_sounds_n == 0) {
109 sources_stats.insert(stats_type::value_type(name, 1));
110 } else {
111 ++s_i->second;
112 }
113 //LOG_DEBUG(("%u: source: %s", (unsigned)lsources.size(), name.c_str()));
114 } else {
115 s->update_position(n);
116 }
117 ++j;
118 }
119 ++i;
120 }
121
122 memset(stream, 0, size);
123
124 for(streams_type::iterator i = streams.begin(); i != streams.end();) {
125 //LOG_DEBUG(("processing stream %d", i->first));
126 stream_info &stream_info = i->second;
127 while ((int)stream_info.buffer.get_size() < size) {
128 clunk::Buffer data;
129 bool eos = !stream_info.stream->read(data, size);
130 if (!data.empty() && stream_info.stream->sample_rate != spec.freq) {
131 //LOG_DEBUG(("converting audio data from %u to %u", stream_info.stream->sample_rate, spec.freq));
132 convert(data, data, stream_info.stream->sample_rate, stream_info.stream->format, stream_info.stream->channels);
133 }
134 stream_info.buffer.append(data);
135 //LOG_DEBUG(("read %u bytes", (unsigned)data.get_size()));
136 if (eos) {
137 if (stream_info.loop) {
138 stream_info.stream->rewind();
139 } else {
140 break;
141 }
142 }
143 }
144 int buf_size = (int)stream_info.buffer.get_size();
145 //LOG_DEBUG(("buffered %d bytes", buf_size));
146 if (buf_size == 0) {
147 //all data buffered. continue;
148 LOG_DEBUG(("stream %d finished. dropping.", i->first));
149 TRY {
150 delete stream_info.stream;
151 } CATCH("mixing stream", {});
152 streams.erase(i++);
153 continue;
154 }
155
156 if (buf_size >= size)
157 buf_size = size;
158
159 int sdl_v = (int)floor(SDL_MIX_MAXVOLUME * stream_info.gain + 0.5f);
160 SDL_MixAudio((Uint8 *)stream, (Uint8 *)stream_info.buffer.get_ptr(), buf_size, sdl_v);
161
162 if ((int)stream_info.buffer.get_size() > size) {
163 memmove(stream_info.buffer.get_ptr(), ((Uint8 *)stream_info.buffer.get_ptr()) + size, stream_info.buffer.get_size() - size);
164 stream_info.buffer.set_size(stream_info.buffer.get_size() - size);
165 } else {
166 stream_info.buffer.free();
167 }
168
169 ++i;
170 }
171
172 clunk::Buffer buf;
173 buf.set_size(size);
174
175 //TIMESPY(("mixing sources"));
176 //LOG_DEBUG(("mixing %u sources", (unsigned)lsources.size()));
177 for(unsigned i = 0; i < lsources.size(); ++i ) {
178 const source_t& source_info = lsources[i];
179 Source * source = source_info.source;
180
181 float dpitch = 1.0f;
182 if (distance_model.doppler_factor > 0) {
183 dpitch = distance_model.doppler_pitch(-source_info.s_pos, source_info.s_vel, source_info.l_vel);
184 }
185
186 float volume = fx_volume * distance_model.gain(source_info.s_pos.length());
187 int sdl_v = (int)floor(SDL_MIX_MAXVOLUME * volume + 0.5f);
188 if (sdl_v <= 0)
189 continue;
190 //check for 0
191 volume = source->process(buf, spec.channels, source_info.s_pos, source_info.s_dir, volume, dpitch);
192 sdl_v = (int)floor(SDL_MIX_MAXVOLUME * volume + 0.5f);
193 //LOG_DEBUG(("%u: mixing source with volume %g (%d)", i, volume, sdl_v));
194 if (sdl_v <= 0)
195 continue;
196 if (sdl_v > SDL_MIX_MAXVOLUME)
197 sdl_v = SDL_MIX_MAXVOLUME;
198
199 SDL_MixAudio((Uint8 *)stream, (Uint8 *)buf.get_ptr(), size, sdl_v);
200 }
201
202 if (fdump != NULL) {
203 if (fwrite(stream, size, 1, fdump) != 1) {
204 fclose(fdump);
205 fdump = NULL;
206 }
207 }
208 }
209
210
create_object()211 Object *Context::create_object() {
212 AudioLocker l;
213 Object *o = new Object(this);
214 objects.push_back(o);
215 return o;
216 }
217
create_sample()218 Sample *Context::create_sample() {
219 AudioLocker l;
220 return new Sample(this);
221 }
222
save(const std::string & file)223 void Context::save(const std::string &file) {
224 AudioLocker l;
225 if (fdump != NULL) {
226 fclose(fdump);
227 fdump = NULL;
228 }
229 if (file.empty())
230 return;
231
232 fdump = fopen(file.c_str(), "wb");
233 }
234
init(const int sample_rate,const Uint8 channels,int period_size)235 void Context::init(const int sample_rate, const Uint8 channels, int period_size) {
236 if (!SDL_WasInit(SDL_INIT_AUDIO)) {
237 if (SDL_InitSubSystem(SDL_INIT_AUDIO) == -1)
238 throw_sdl(("SDL_InitSubSystem"));
239 }
240
241 SDL_AudioSpec src;
242 memset(&src, 0, sizeof(src));
243 src.freq = sample_rate;
244 src.channels = channels;
245 src.format = AUDIO_S16SYS;
246 src.samples = period_size;
247 src.callback = &Context::callback;
248 src.userdata = (void *) this;
249
250 this->period_size = period_size;
251
252 if ( SDL_OpenAudio(&src, &spec) < 0 )
253 throw_sdl(("SDL_OpenAudio(%d, %u, %d)", sample_rate, channels, period_size));
254 if (spec.format != AUDIO_S16SYS)
255 throw_ex(("SDL_OpenAudio(%d, %u, %d) returned format %d", sample_rate, channels, period_size, spec.format));
256 if (spec.channels < 2)
257 LOG_ERROR(("Could not operate on %d channels", spec.channels));
258
259 LOG_DEBUG(("opened audio device, sample rate: %d, period: %d, channels: %d", spec.freq, spec.samples, spec.channels));
260 SDL_PauseAudio(0);
261
262 AudioLocker l;
263 listener = create_object();
264 }
265
delete_object(Object * o)266 void Context::delete_object(Object *o) {
267 AudioLocker l;
268 objects_type::iterator i = std::find(objects.begin(), objects.end(), o);
269 while(i != objects.end() && *i == o)
270 i = objects.erase(i); //just for fun
271 }
272
deinit()273 void Context::deinit() {
274 //cleanup objects here too.
275 if (!SDL_WasInit(SDL_INIT_AUDIO))
276 return;
277
278 AudioLocker l;
279 delete listener;
280 listener = NULL;
281 SDL_CloseAudio();
282
283 if (fdump != NULL) {
284 fclose(fdump);
285 fdump = NULL;
286 }
287
288 SDL_QuitSubSystem(SDL_INIT_AUDIO);
289 }
290
~Context()291 Context::~Context() {
292 deinit();
293 }
294
295
296 //MUSIC MIXER:
297
play(const int id,Stream * stream,bool loop)298 void Context::play(const int id, Stream *stream, bool loop) {
299 LOG_DEBUG(("play(%d, %p, %s)", id, (const void *)stream, loop?"'loop'":"'once'"));
300 AudioLocker l;
301 stream_info & stream_info = streams[id];
302 delete stream_info.stream;
303 stream_info.stream = stream;
304 stream_info.loop = loop;
305 stream_info.paused = false;
306 stream_info.gain = 1.0f;
307 }
308
playing(const int id) const309 bool Context::playing(const int id) const {
310 AudioLocker l;
311 return streams.find(id) != streams.end();
312 }
313
pause(const int id)314 void Context::pause(const int id) {
315 AudioLocker l;
316 streams_type::iterator i = streams.find(id);
317 if (i == streams.end())
318 return;
319
320 i->second.paused = !i->second.paused;
321 }
322
stop(const int id)323 void Context::stop(const int id) {
324 AudioLocker l;
325 streams_type::iterator i = streams.find(id);
326 if (i == streams.end())
327 return;
328
329 TRY {
330 delete i->second.stream;
331 } CATCH(clunk::format_string("stop(%d)", id).c_str(), {
332 streams.erase(i);
333 throw;
334 })
335 streams.erase(i);
336 }
337
set_volume(const int id,float volume)338 void Context::set_volume(const int id, float volume) {
339 if (volume < 0)
340 volume = 0;
341 if (volume > 1)
342 volume = 1;
343
344 streams_type::iterator i = streams.find(id);
345 if (i == streams.end())
346 return;
347 i->second.gain = volume;
348 }
349
set_fx_volume(float volume)350 void Context::set_fx_volume(float volume) {
351 //LOG_WARN(("ignoring set_fx_volume(%g)", volume));
352 if (volume < 0)
353 fx_volume = 0;
354 else if (volume > 1)
355 fx_volume = 1;
356 else
357 fx_volume = volume;
358 }
359
stop_all()360 void Context::stop_all() {
361 AudioLocker l;
362 for(streams_type::iterator i = streams.begin(); i != streams.end(); ++i) {
363 delete i->second.stream;
364 }
365 streams.clear();
366 }
367
set_max_sources(int sources)368 void Context::set_max_sources(int sources) {
369 AudioLocker l;
370 max_sources = sources;
371 }
372
convert(clunk::Buffer & dst,const clunk::Buffer & src,int rate,const Uint16 format,const Uint8 channels)373 void Context::convert(clunk::Buffer &dst, const clunk::Buffer &src, int rate, const Uint16 format, const Uint8 channels) {
374 SDL_AudioCVT cvt;
375 memset(&cvt, 0, sizeof(cvt));
376 if (SDL_BuildAudioCVT(&cvt, format, channels, rate, spec.format, channels, spec.freq) == -1) {
377 throw_sdl(("DL_BuildAudioCVT(%d, %04x, %u)", rate, format, channels));
378 }
379 size_t buf_size = (size_t)(src.get_size() * cvt.len_mult);
380 cvt.buf = (Uint8 *)malloc(buf_size);
381 cvt.len = (int)src.get_size();
382
383 assert(buf_size >= src.get_size());
384 memcpy(cvt.buf, src.get_ptr(), src.get_size());
385
386 if (SDL_ConvertAudio(&cvt) == -1)
387 throw_sdl(("SDL_ConvertAudio"));
388
389 dst.set_data(cvt.buf, (size_t)(cvt.len * cvt.len_ratio), true);
390 }
391
392 /*!
393 \mainpage Tutorial
394 \section overview Overview
395 Hello there!
396 Here's quick explanation of the clunk library concepts and usage scenarios.
397 \section scenario Typical scenario
398
399 First of all, initialize SDL in your code:
400 \code
401 SDL_Init(SDL_INIT_AUDIO) or SDL_InitSubSystem(SDL_INIT_AUDIO);
402 \endcode
403
404 Let's initialize context with typical values: 22kHz sample rate, 2 channels and 1024 bytes period:
405 \code
406 Context context;
407 context.init(22050, 2, 1024);
408 //main code
409 context.deinit();
410 \endcode
411 If you choose greater sample rate such as 44kHz or even 48kHz, you will need more CPU power to mix sounds and it could hurt overall game performance.
412 You could raise period value to avoid clicks, but you get more latency for that.
413 Latency could be calculated with the following formula:
414 \code latency (in seconds) = period_size / channels / byte per sample (2 for 16 bit sound) / sample_rate \endcode
415 in this example latency is only 12ms. Such small delays are almost invisible even for perfect ears :)
416
417 Then application should load some samples to the library. Clunk itself does not provide code to decode audio formats, or load raw wave files.
418 Check ogg/vorbis library for a free production-quality audio codec. Samples allocates within context internally with clunk::Context::create_sample() method.
419
420 \code
421 clunk::Buffer data; //placeholder for a memory chunk
422 //decode ogg sample into data
423 clunk::Sample *sample = Context->create_sample();
424 sample->init(data, ogg_rate, AUDIO_S16LSB, ogg_channels);
425 \endcode
426
427 So all audio data were loaded and initialized. Next step is to allocate objects. Clunk was designed to be easily integrated into programs.
428 The most useful object is clunk::Object. It could hold several playing \link clunk::Source sources \endlink.
429 You could use two different approaches here:
430 \li create global mixer proxy object and leave all clunk stuff to it, such as mapping your objects to clunk ones.
431 \li directly include clunk::Object pointer into every object in game or program.
432
433 You wont ever need to track objects and/or manage its destruction, clunk will do it itself.
434 Example allowing sound to play after your object's death:
435 \code
436 clunk::Object *clunk_object;
437
438 GameObject::~GameObject() {
439 if (clunk_object != NULL) {
440 clunk_object->autodelete(); //destroy me!
441 clunk_object = NULL; //leave destruction to the clunk::Context
442 }
443 }
444 \endcode
445
446 So the next step is source management. It's the most easiest part. Each source connects to its audio sample.
447 Source holds data about actual playing sound: position in wave data, pitch, gain and distance. It processes audio data
448 and simulate 3d sound positioning with hrtf function.
449
450 Creating source and adding it to the object : (the most easiest part)
451 \code
452 clunk_object->play("voice", new Source(yeti_sound_sample)); // no loop, no pitch, no gain adjustments.
453 \endcode
454 Sources are automatically purged from the object when they are not needed anymore. So, you don't need to worry about its deletion or any management.
455 Anyway, you could cancel any playing source:
456 \code
457 clunk_object->cancel("voice");
458 \endcode
459
460 Or cancel all sounds from this object at once:
461 \code
462 clunk_object->cancel_all(true);
463 \endcode
464
465 \section positioning Object positioning
466 Usually objects are positioning the some sort of ticking function called every frame or from the on_object_update callback.
467 Positioning is really simple:
468 \code
469 clunk_object->update(clunk::v3<float>(x, y, z), clunk::v3<float>(velocity_x, velocity_y, velocity_z), clunk::v3<float>(direction_x, direction_y, direction_z));
470 \endcode
471 Moving listener is easy too, listener is regular clunk::Object, but it's stored in clunk::Context and holds information about your position
472 \code
473 context.get_listener()->update(clunk::v3<float>(x, y, z), clunk::v3<float>(velocity_x, velocity_y, velocity_z), clunk::v3<float>(direction_x, direction_y, direction_z));
474 \endcode
475
476 \section streaming Playing music and ambient sounds
477 Clunk is able to mix as many music streams as you want (or your CPU could handle :) ).
478 First of all you need to implement your stream class derived from the clunk::Stream.
479 Don't worry, you need to implement just 2(!) clunk-related methods to make the music play.
480 \code
481 class FooStream : public clunk::Stream {
482 public:
483 void FooStream(const std::string &file) {
484 //open music file.
485 //store music parameters into members :
486 sample_rate = music_rate;
487 channels = music_channels;
488 format = AUDIO_S16LSB;
489 //this values here are only for educational purpose. Don't forget to fill it with actual values from the music file!
490 }
491
492 void rewind() {
493 //rewind your stream here
494 }
495
496 bool read(clunk::Buffer &data, unsigned hint) {
497 //read as many data as you want, but it'd better to read around 'hint' bytes to avoid memory queue overhead.
498 }
499
500 virtual ~FooStream() {
501 //don't forget to close your stream here. Leaks are unwanted guests here.
502 }
503 };
504 \endcode
505
506 So, the most complicated part passed by. Let the party begin !
507 \code
508 context.play(0, new FooStream("data/background_music.ogg"), false); //do not loop music, look below for details.
509 context.play(1, new FooStream("data/ambience_city.ogg"), true); //loops ambient
510 \endcode
511
512 There's no magic numbers here. I've chosen 0 and 1 just for fun. You could use any integer id. 42 for example.
513 Why don't I use loop == true for music ? We need it to change various tunes. Let's periodically test if music ends and restart with new tune:
514 \code
515 if (!context.playing(0)) {
516 context.play(0, new FooStream(next_song));
517 }
518 \endcode
519
520 \section final Final words from author
521 I've covered almost all major topics of the clunk here in this tutorial. If you have suggestion - feel free to contact me directly.
522 Hope all this code will be useful for someone. Good luck! We're waiting for your feedback!
523
524 */
525