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