1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
5  * Copyright (c) 2009-2016, The OpenClonk Team and contributors
6  *
7  * Distributed under the terms of the ISC license; see accompanying file
8  * "COPYING" for details.
9  *
10  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11  * See accompanying file "TRADEMARK" for details.
12  *
13  * To redistribute this file separately, substitute the full license texts
14  * for the above references.
15  */
16 /* Handles Music Files */
17 
18 #include "C4Include.h"
19 #include "platform/C4MusicFile.h"
20 
21 #include "game/C4Application.h"
22 
23 #if AUDIO_TK == AUDIO_TK_OPENAL
24 #if defined(__APPLE__)
25 #import <CoreFoundation/CoreFoundation.h>
26 #import <AudioToolbox/AudioToolbox.h>
27 #else
28 #ifdef _WIN32
29 // This is an ugly hack to make FreeALUT not dllimport everything.
30 #define _XBOX
31 #endif
32 #include <AL/alut.h>
33 #undef _XBOX
34 #endif
35 #define alErrorCheck(X) do { X; { ALenum err = alGetError(); if (err) LogF("al error: %s (%x)", #X, err); } } while (0)
36 #endif
37 
38 /* helpers */
39 
Announce()40 void C4MusicFile::Announce()
41 {
42 	LogF(LoadResStr("IDS_PRC_PLAYMUSIC"), GetFilename(FileName));
43 	announced = true;
44 }
45 
ExtractFile()46 bool C4MusicFile::ExtractFile()
47 {
48 	// safety
49 	if (SongExtracted) return true;
50 	// extract entry
51 	if (!C4Group_CopyItem(FileName, Config.AtTempPath(C4CFN_TempMusic2))) return false;
52 	// ok
53 	SongExtracted = true;
54 	return true;
55 }
56 
RemTempFile()57 bool C4MusicFile::RemTempFile()
58 {
59 	if (!SongExtracted) return true;
60 	// delete it
61 	EraseFile(Config.AtTempPath(C4CFN_TempMusic2));
62 	SongExtracted = false;
63 	return true;
64 }
65 
Init(const char * szFile)66 bool C4MusicFile::Init(const char *szFile)
67 {
68 	SCopy(szFile, FileName);
69 	return true;
70 }
71 
72 #if AUDIO_TK == AUDIO_TK_SDL_MIXER
C4MusicFileSDL()73 C4MusicFileSDL::C4MusicFileSDL():
74 		Data(nullptr),
75 		Music(nullptr)
76 {
77 }
78 
~C4MusicFileSDL()79 C4MusicFileSDL::~C4MusicFileSDL()
80 {
81 	Stop();
82 }
83 
Play(bool loop,double max_resume_time)84 bool C4MusicFileSDL::Play(bool loop, double max_resume_time)
85 {
86 	const SDL_version * link_version = Mix_Linked_Version();
87 	if (link_version->major < 1
88 	    || (link_version->major == 1 && link_version->minor < 2)
89 	    || (link_version->major == 1 && link_version->minor == 2 && link_version->patch < 7))
90 	{
91 		// Check existance and try extracting it
92 		if (!FileExists(FileName)) if (!ExtractFile())
93 				// Doesn't exist - or file is corrupt
94 			{
95 				LogF("Error reading %s", FileName);
96 				return false;
97 			}
98 		// Load
99 		Music = Mix_LoadMUS(SongExtracted ? Config.AtTempPath(C4CFN_TempMusic2) : FileName);
100 		// Load failed
101 		if (!Music)
102 		{
103 			LogF("SDL_mixer: %s", SDL_GetError());
104 			return false;
105 		}
106 		// Play Song
107 		if (Mix_PlayMusic(Music, loop? -1 : 1) == -1)
108 		{
109 			LogF("SDL_mixer: %s", SDL_GetError());
110 			return false;
111 		}
112 	}
113 	else
114 	{
115 		// Load Song
116 		// Fixme: Try loading this from the group incrementally for less lag
117 		size_t filesize;
118 		if (!C4Group_ReadFile(FileName, &Data, &filesize))
119 		{
120 			LogF("Error reading %s", FileName);
121 			return false;
122 		}
123 		// Mix_FreeMusic frees the RWop
124 		Music = Mix_LoadMUS_RW(SDL_RWFromConstMem(Data, filesize), 1);
125 		if (!Music)
126 		{
127 			LogF("SDL_mixer: %s", SDL_GetError());
128 			return false;
129 		}
130 		if (Mix_PlayMusic(Music, loop? -1 : 1) == -1)
131 		{
132 			LogF("SDL_mixer: %s", SDL_GetError());
133 			return false;
134 		}
135 	}
136 	return true;
137 }
138 
Stop(int fadeout_ms)139 void C4MusicFileSDL::Stop(int fadeout_ms)
140 {
141 	if (fadeout_ms && Music)
142 	{
143 		// Don't really stop yet
144 		Mix_FadeOutMusic(fadeout_ms);
145 		return;
146 	}
147 	if (Music)
148 	{
149 		Mix_FreeMusic(Music);
150 		Music = nullptr;
151 	}
152 	RemTempFile();
153 	if (Data)
154 	{
155 		delete[] Data;
156 		Data = nullptr;
157 	}
158 }
159 
CheckIfPlaying()160 void C4MusicFileSDL::CheckIfPlaying()
161 {
162 	if (!Mix_PlayingMusic())
163 		Application.MusicSystem.NotifySuccess();
164 }
165 
SetVolume(int iLevel)166 void C4MusicFileSDL::SetVolume(int iLevel)
167 {
168 	Mix_VolumeMusic((int) ((iLevel * MIX_MAX_VOLUME) / 100));
169 }
170 
171 #elif AUDIO_TK == AUDIO_TK_OPENAL
172 
173 /* Ogg Vobis */
174 
C4MusicFileOgg()175 C4MusicFileOgg::C4MusicFileOgg() :
176 	last_interruption_time()
177 {
178 	for (unsigned int & buffer : buffers)
179 		buffer = 0;
180 }
181 
~C4MusicFileOgg()182 C4MusicFileOgg::~C4MusicFileOgg()
183 {
184 	Clear();
185 	Stop();
186 }
187 
Clear()188 void C4MusicFileOgg::Clear()
189 {
190 	// clear ogg file
191 	if (loaded)
192 	{
193 		ov_clear(&ogg_file);
194 		loaded = false;
195 	}
196 	categories.clear();
197 	is_loading_from_file = false;
198 	source_file.Close();
199 	last_source_file_pos = 0;
200 	last_playback_pos_sec = 0;
201 	last_interruption_time = C4TimeMilliseconds();
202 }
203 
Init(const char * strFile)204 bool C4MusicFileOgg::Init(const char *strFile)
205 {
206 	// Clear previous
207 	Clear();
208 	// Base init file
209 	if (!C4MusicFile::Init(strFile)) return false;
210 	// Prepare ogg reader
211 	vorbis_info* info;
212 	memset(&ogg_file, 0, sizeof(ogg_file));
213 	ov_callbacks callbacks;
214 	// Initial file loading
215 	// For packed groups, the whole compressed file is kept in memory because reading/seeking inside C4Group is problematic. Uncompress while playing.
216 	// This increases startup time a bit.
217 	// Later, this could be replaced with proper random access in c4group. Either replacing the file format or e.g. storing the current zlib state here
218 	//  and then updating callbacks.read/seek/close/tell_func to read data from the group directly as needed
219 	bool is_loading_from_file = FileExists(strFile);
220 	void *data_source;
221 	if (!is_loading_from_file)
222 	{
223 		char *file_contents;
224 		size_t file_size;
225 		if (!C4Group_ReadFile(FileName, &file_contents, &file_size))
226 			return false;
227 		data.SetOwnedData((BYTE *)file_contents, file_size);
228 		// C4Group preloaded ogg reader
229 		callbacks.read_func = &::C4SoundLoaders::VorbisLoader::mem_read_func;
230 		callbacks.seek_func = &::C4SoundLoaders::VorbisLoader::mem_seek_func;
231 		callbacks.close_func = &::C4SoundLoaders::VorbisLoader::mem_close_func;
232 		callbacks.tell_func = &::C4SoundLoaders::VorbisLoader::mem_tell_func;
233 		data_source = &data;
234 	}
235 	else
236 	{
237 		// Load directly from file
238 		if (!source_file.Open(FileName))
239 			return false;
240 		// Uncompressed file ogg reader
241 		callbacks.read_func = &::C4SoundLoaders::VorbisLoader::file_read_func;
242 		callbacks.seek_func = &::C4SoundLoaders::VorbisLoader::file_seek_func;
243 		callbacks.close_func = &::C4SoundLoaders::VorbisLoader::file_close_func;
244 		callbacks.tell_func = &::C4SoundLoaders::VorbisLoader::file_tell_func;
245 		data_source = this;
246 	}
247 
248 	// open using callbacks either to memory or to file loader
249 	if (ov_open_callbacks(data_source, &ogg_file, nullptr, 0, callbacks) != 0)
250 	{
251 		ov_clear(&ogg_file);
252 		return false;
253 	}
254 
255 	// get information about music
256 	info = ov_info(&ogg_file, -1);
257 	if (info->channels == 1)
258 		ogg_info.format = AL_FORMAT_MONO16;
259 	else
260 		ogg_info.format = AL_FORMAT_STEREO16;
261 	ogg_info.sample_rate = info->rate;
262 	ogg_info.sample_length = ov_time_total(&ogg_file, -1) / 1000.0;
263 
264 	// Get categories from ogg comment header
265 	vorbis_comment *comment = ov_comment(&ogg_file, -1);
266 	const char *comment_id = "COMMENT=";
267 	int comment_id_len = strlen(comment_id);
268 	for (int i = 0; i < comment->comments; ++i)
269 	{
270 		if (comment->comment_lengths[i] > comment_id_len)
271 		{
272 			if (SEqual2NoCase(comment->user_comments[i], comment_id, comment_id_len))
273 			{
274 				// Add all categories delimeted by ';'
275 				const char *categories_string = comment->user_comments[i] + comment_id_len;
276 				for (;;)
277 				{
278 					int delimeter = SCharPos(';', categories_string);
279 					StdCopyStrBuf category;
280 					category.Copy(categories_string, delimeter >= 0 ? delimeter : SLen(categories_string));
281 					categories.push_back(category);
282 					if (delimeter < 0) break;
283 					categories_string += delimeter+1;
284 				}
285 			}
286 		}
287 	}
288 
289 	// File not needed for now
290 	UnprepareSourceFileReading();
291 
292 	// mark successfully loaded
293 	return loaded = true;
294 }
295 
GetDebugInfo() const296 StdStrBuf C4MusicFileOgg::GetDebugInfo() const
297 {
298 	StdStrBuf result;
299 	result.Append(FileName);
300 	result.AppendFormat("[%.0lf]", last_playback_pos_sec);
301 	result.AppendChar('[');
302 	bool sec = false;
303 	for (const auto & category : categories)
304 	{
305 		if (sec) result.AppendChar(',');
306 		result.Append(category.getData());
307 		sec = true;
308 	}
309 	result.AppendChar(']');
310 	return result;
311 }
312 
UnprepareSourceFileReading()313 void C4MusicFileOgg::UnprepareSourceFileReading()
314 {
315 	// The file loader could just keep all files open. But if someone symlinks
316 	// Music.ocg into their music folder with a million files in it, we would
317 	// crash with too many open file handles. So close it for now and reopen
318 	// when that piece is actually requested.
319 	if (is_loading_from_file && source_file.IsOpen())
320 	{
321 		last_source_file_pos = source_file.Tell();
322 		source_file.Close();
323 	}
324 }
325 
PrepareSourceFileReading()326 bool C4MusicFileOgg::PrepareSourceFileReading()
327 {
328 	// mem loading always OK
329 	if (!is_loading_from_file) return true;
330 	// ensure file is open
331 	if (!source_file.IsOpen())
332 	{
333 		if (!source_file.Open(FileName)) return false;
334 		if (last_source_file_pos) if (source_file.Seek(last_source_file_pos, SEEK_SET) < 0) return false;
335 	}
336 	return true;
337 }
338 
Play(bool loop,double max_resume_time)339 bool C4MusicFileOgg::Play(bool loop, double max_resume_time)
340 {
341 	// Valid file?
342 	if (!loaded) return false;
343 	// stop previous
344 	if (playing)
345 	{
346 		if (max_resume_time > 0.0) return true; // no-op
347 		Stop();
348 	}
349 	// Ensure data reading is ready
350 	PrepareSourceFileReading();
351 	// Get channel to use
352 	alGenSources(1, (ALuint*)&channel);
353 	if (!channel) return false;
354 
355 	playing = true;
356 	streaming_done = false;
357 	this->loop = loop;
358 	byte_pos_total = 0;
359 
360 	// Resume setting
361 	if (max_resume_time > 0)
362 	{
363 		// Only resume if significant amount of data is left to be played
364 		double time_remaining_sec = GetRemainingTime();
365 		if (time_remaining_sec < max_resume_time) last_playback_pos_sec = 0.0;
366 	}
367 	else
368 	{
369 		last_playback_pos_sec = 0;
370 	}
371 
372 	// initial volume setting
373 	SetVolume(float(::Config.Sound.MusicVolume) / 100.0f);
374 
375 	// prepare read
376 	ogg_info.sound_data.resize(num_buffers * buffer_size);
377 	alGenBuffers(num_buffers, buffers);
378 	ov_time_seek(&ogg_file, last_playback_pos_sec);
379 
380 	// Fill initial buffers
381 	for (size_t i=0; i<num_buffers; ++i)
382 		if (!FillBuffer(i)) break; // if this fails, the piece is shorter than the initial buffers
383 
384 	// play!
385 	alErrorCheck(alSourcePlay(channel));
386 
387 	return true;
388 }
389 
GetRemainingTime()390 double C4MusicFileOgg::GetRemainingTime()
391 {
392 	// Note: Only valid after piece has been stopped
393 	return ov_time_total(&ogg_file, -1) - last_playback_pos_sec;
394 }
395 
Stop(int fadeout_ms)396 void C4MusicFileOgg::Stop(int fadeout_ms)
397 {
398 	if (playing)
399 	{
400 		// remember position for eventual later resume
401 		ALfloat playback_pos_in_buffer = 0;
402 		alErrorCheck(alGetSourcef(channel, AL_SEC_OFFSET, &playback_pos_in_buffer));
403 		last_playback_pos_sec += playback_pos_in_buffer;
404 		last_interruption_time = C4TimeMilliseconds::Now();
405 		// stop!
406 		alSourceStop(channel);
407 		// clear queue
408 		ALint num_queued=0;
409 		alErrorCheck(alGetSourcei(channel, AL_BUFFERS_QUEUED, &num_queued));
410 		ALuint buffer;
411 		for (size_t i = 0; i < (size_t)num_queued; ++i)
412 			alErrorCheck(alSourceUnqueueBuffers(channel, 1, &buffer));
413 	}
414 	// clear buffers
415 	if (channel)
416 	{
417 		alSourcei(channel, AL_BUFFER, 0);
418 		alDeleteBuffers(num_buffers, buffers);
419 		alDeleteSources(1, &channel);
420 	}
421 	playing = false;
422 	channel = 0;
423 	// close file
424 	UnprepareSourceFileReading();
425 }
426 
CheckIfPlaying()427 void C4MusicFileOgg::CheckIfPlaying()
428 {
429 	Execute();
430 	if (!playing) Application.MusicSystem.NotifySuccess();
431 }
432 
SetVolume(int iLevel)433 void C4MusicFileOgg::SetVolume(int iLevel)
434 {
435 	volume = ((float) iLevel) / 100.0f;
436 	if (channel) alSourcef(channel, AL_GAIN, volume);
437 }
438 
HasCategory(const char * szcat) const439 bool C4MusicFileOgg::HasCategory(const char *szcat) const
440 {
441 	if (!szcat) return false;
442 	// check all stored categories
443 	for (const auto & category : categories)
444 		if (WildcardMatch(szcat, category.getData()))
445 			return true;
446 	return false;
447 }
448 
FillBuffer(size_t idx)449 bool C4MusicFileOgg::FillBuffer(size_t idx)
450 {
451 	// uncompress from ogg data
452 	int endian = 0;
453 	long bytes_read_total = 0, bytes_read;
454 	char uncompressed_data[buffer_size];
455 	do {
456 		bytes_read = ov_read(&ogg_file, uncompressed_data+bytes_read_total, (buffer_size-bytes_read_total)*sizeof(BYTE), endian, 2, 1, &current_section);
457 		bytes_read_total += bytes_read;
458 	} while (bytes_read > 0 && bytes_read_total < buffer_size);
459 	// buffer data
460 	if (bytes_read_total)
461 	{
462 		byte_pos_total += bytes_read_total;
463 		ALuint buffer = buffers[idx];
464 		alErrorCheck(alBufferData(buffer, ogg_info.format, uncompressed_data, bytes_read_total, ogg_info.sample_rate));
465 		// queue buffer
466 		alErrorCheck(alSourceQueueBuffers(channel, 1, &buffer));
467 	}
468 	// streaming done?
469 	if (bytes_read_total < buffer_size)
470 	{
471 		// streaming done. loop or done.
472 		if (loop)
473 		{
474 			// reset pos in ogg file
475 			ov_raw_seek(&ogg_file, 0);
476 			// if looping and nothing has been committed to this buffer yet, try again
477 			// except if byte_pos_total==0, i.e. if the piece is completely empty
478 			size_t prev_bytes_total = byte_pos_total;
479 			byte_pos_total = 0;
480 			if (!bytes_read_total && prev_bytes_total) return FillBuffer(idx);
481 			return true;
482 		}
483 		else
484 		{
485 			// non-looping: we're done.
486 			return false;
487 		}
488 	}
489 	else
490 	{
491 		// might have more data to stream
492 		return true;
493 	}
494 }
495 
Execute()496 void C4MusicFileOgg::Execute()
497 {
498 	if (playing)
499 	{
500 		// get processed buffer count
501 		ALint num_processed = 0;
502 		alErrorCheck(alGetSourcei(channel, AL_BUFFERS_PROCESSED, &num_processed));
503 		bool done = false;
504 		while (num_processed--)
505 		{
506 			// release processed buffer
507 			ALuint buffer;
508 			alErrorCheck(alSourceUnqueueBuffers(channel, 1, &buffer));
509 			// add playback time of processed buffer to total playback time
510 			ALint buf_bits = 16, buf_chans = 2, buf_freq = 44100;
511 			alErrorCheck(alGetBufferi(buffer, AL_BITS, &buf_bits));
512 			alErrorCheck(alGetBufferi(buffer, AL_CHANNELS, &buf_chans));
513 			alErrorCheck(alGetBufferi(buffer, AL_FREQUENCY, &buf_freq));
514 			double buffer_secs = double(buffer_size) / buf_bits / buf_chans / buf_freq * 8;
515 			last_playback_pos_sec += buffer_secs;
516 			// refill processed buffer
517 			size_t buffer_idx;
518 			for (buffer_idx=0; buffer_idx<num_buffers; ++buffer_idx)
519 				if (buffers[buffer_idx] == buffer) break;
520 			if (!done) done = !FillBuffer(buffer_idx);
521 		}
522 		if (done) streaming_done = true;
523 		// check if done
524 		ALint state = 0;
525 		alErrorCheck(alGetSourcei(channel, AL_SOURCE_STATE, &state));
526 		if (state != AL_PLAYING && streaming_done)
527 		{
528 			Stop();
529 			// reset playback to beginning for next time this piece is playing
530 			last_playback_pos_sec = 0.0;
531 		}
532 		else if (state == AL_STOPPED)
533 		{
534 			alErrorCheck(alSourcePlay(channel));
535 		}
536 	}
537 }
538 
539 #endif
540