OpenClonk
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 
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 
41 {
42  LogF(LoadResStr("IDS_PRC_PLAYMUSIC"), GetFilename(FileName));
43  announced = true;
44 }
45 
47 {
48  // safety
49  if (SongExtracted) return true;
50  // extract entry
52  // ok
53  SongExtracted = true;
54  return true;
55 }
56 
58 {
59  if (!SongExtracted) return true;
60  // delete it
62  SongExtracted = false;
63  return true;
64 }
65 
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
74  Data(nullptr),
75  Music(nullptr)
76 {
77 }
78 
80 {
81  Stop();
82 }
83 
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
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 
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 
161 {
162  if (!Mix_PlayingMusic())
164 }
165 
167 {
168  Mix_VolumeMusic((int) ((iLevel * MIX_MAX_VOLUME) / 100));
169 }
170 
171 #elif AUDIO_TK == AUDIO_TK_OPENAL
172 
173 /* Ogg Vobis */
174 
175 C4MusicFileOgg::C4MusicFileOgg() :
176  last_interruption_time()
177 {
178  for (unsigned int & buffer : buffers)
179  buffer = 0;
180 }
181 
182 C4MusicFileOgg::~C4MusicFileOgg()
183 {
184  Clear();
185  Stop();
186 }
187 
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 
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 
296 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 
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 
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 
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 
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 
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 
427 void C4MusicFileOgg::CheckIfPlaying()
428 {
429  Execute();
430  if (!playing) Application.MusicSystem.NotifySuccess();
431 }
432 
433 void C4MusicFileOgg::SetVolume(int iLevel)
434 {
435  volume = ((float) iLevel) / 100.0f;
436  if (channel) alSourcef(channel, AL_GAIN, volume);
437 }
438 
439 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 
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 
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
#define C4CFN_TempMusic2
Definition: C4Components.h:152
C4Config Config
Definition: C4Config.cpp:930
C4Application Application
Definition: C4Globals.cpp:44
bool C4Group_CopyItem(const char *source, const char *target, bool no_sorting, bool reset_attributes)
Definition: C4Group.cpp:115
bool C4Group_ReadFile(const char *filename, char **data, size_t *size)
Definition: C4Group.cpp:491
const char * LoadResStr(const char *id)
Definition: C4Language.h:83
bool LogF(const char *strMessage,...)
Definition: C4Log.cpp:262
#define alErrorCheck(X)
Definition: C4MusicFile.cpp:35
uint8_t BYTE
int SCharPos(char cTarget, const char *szInStr, int iIndex)
Definition: Standard.cpp:239
void SCopy(const char *szSource, char *sTarget, size_t iMaxL)
Definition: Standard.cpp:152
bool SEqual2NoCase(const char *szStr1, const char *szStr2, int iLen)
Definition: Standard.cpp:226
size_t SLen(const char *sptr)
Definition: Standard.h:74
bool WildcardMatch(const char *szWildcard, const char *szString)
Definition: StdFile.cpp:396
char * GetFilename(char *szPath)
Definition: StdFile.cpp:42
bool FileExists(const char *szFileName)
bool EraseFile(const char *szFileName)
C4MusicSystem MusicSystem
Definition: C4Application.h:41
const char * AtTempPath(const char *filename)
Definition: C4Config.cpp:600
C4ConfigSound Sound
Definition: C4Config.h:258
int32_t MusicVolume
Definition: C4Config.h:131
char FileName[_MAX_FNAME_LEN]
Definition: C4MusicFile.h:34
bool SongExtracted
Definition: C4MusicFile.h:65
bool RemTempFile()
Definition: C4MusicFile.cpp:57
virtual bool Init(const char *strFile)
Definition: C4MusicFile.cpp:66
bool ExtractFile()
Definition: C4MusicFile.cpp:46
void Announce()
Definition: C4MusicFile.cpp:40
bool announced
Definition: C4MusicFile.h:39
bool Play(bool loop=false, double max_resume_time=0.0) override
Definition: C4MusicFile.cpp:84
~C4MusicFileSDL() override
Definition: C4MusicFile.cpp:79
void CheckIfPlaying() override
void SetVolume(int) override
Mix_Music * Music
Definition: C4MusicFile.h:82
void Stop(int fadeout_ms=0) override
static long file_tell_func(void *datasource)
static long mem_tell_func(void *datasource)
static int mem_close_func(void *datasource)
static int mem_seek_func(void *datasource, ogg_int64_t offset, int whence)
static int file_close_func(void *datasource)
static size_t file_read_func(void *ptr, size_t byte_size, size_t size_to_read, void *datasource)
static size_t mem_read_func(void *ptr, size_t byte_size, size_t size_to_read, void *datasource)
static int file_seek_func(void *datasource, ogg_int64_t offset, int whence)
static C4TimeMilliseconds Now()
void AppendFormat(const char *szFmt,...) GNUC_FORMAT_ATTRIBUTE_O
Definition: StdBuf.cpp:190
void AppendChar(char cChar)
Definition: StdBuf.h:588
void Copy()
Definition: StdBuf.h:467
void Append(const char *pnData, size_t iChars)
Definition: StdBuf.h:519