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, ¤t_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