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