/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2009-2016, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ /* Handles Music Files */ #include "C4Include.h" #include "platform/C4MusicFile.h" #include "game/C4Application.h" #if AUDIO_TK == AUDIO_TK_OPENAL #if defined(__APPLE__) #import #import #else #ifdef _WIN32 // This is an ugly hack to make FreeALUT not dllimport everything. #define _XBOX #endif #include #undef _XBOX #endif #define alErrorCheck(X) do { X; { ALenum err = alGetError(); if (err) LogF("al error: %s (%x)", #X, err); } } while (0) #endif /* helpers */ void C4MusicFile::Announce() { LogF(LoadResStr("IDS_PRC_PLAYMUSIC"), GetFilename(FileName)); announced = true; } bool C4MusicFile::ExtractFile() { // safety if (SongExtracted) return true; // extract entry if (!C4Group_CopyItem(FileName, Config.AtTempPath(C4CFN_TempMusic2))) return false; // ok SongExtracted = true; return true; } bool C4MusicFile::RemTempFile() { if (!SongExtracted) return true; // delete it EraseFile(Config.AtTempPath(C4CFN_TempMusic2)); SongExtracted = false; return true; } bool C4MusicFile::Init(const char *szFile) { SCopy(szFile, FileName); return true; } #if AUDIO_TK == AUDIO_TK_SDL_MIXER C4MusicFileSDL::C4MusicFileSDL(): Data(nullptr), Music(nullptr) { } C4MusicFileSDL::~C4MusicFileSDL() { Stop(); } bool C4MusicFileSDL::Play(bool loop, double max_resume_time) { const SDL_version * link_version = Mix_Linked_Version(); if (link_version->major < 1 || (link_version->major == 1 && link_version->minor < 2) || (link_version->major == 1 && link_version->minor == 2 && link_version->patch < 7)) { // Check existance and try extracting it if (!FileExists(FileName)) if (!ExtractFile()) // Doesn't exist - or file is corrupt { LogF("Error reading %s", FileName); return false; } // Load Music = Mix_LoadMUS(SongExtracted ? Config.AtTempPath(C4CFN_TempMusic2) : FileName); // Load failed if (!Music) { LogF("SDL_mixer: %s", SDL_GetError()); return false; } // Play Song if (Mix_PlayMusic(Music, loop? -1 : 1) == -1) { LogF("SDL_mixer: %s", SDL_GetError()); return false; } } else { // Load Song // Fixme: Try loading this from the group incrementally for less lag size_t filesize; if (!C4Group_ReadFile(FileName, &Data, &filesize)) { LogF("Error reading %s", FileName); return false; } // Mix_FreeMusic frees the RWop Music = Mix_LoadMUS_RW(SDL_RWFromConstMem(Data, filesize), 1); if (!Music) { LogF("SDL_mixer: %s", SDL_GetError()); return false; } if (Mix_PlayMusic(Music, loop? -1 : 1) == -1) { LogF("SDL_mixer: %s", SDL_GetError()); return false; } } return true; } void C4MusicFileSDL::Stop(int fadeout_ms) { if (fadeout_ms && Music) { // Don't really stop yet Mix_FadeOutMusic(fadeout_ms); return; } if (Music) { Mix_FreeMusic(Music); Music = nullptr; } RemTempFile(); if (Data) { delete[] Data; Data = nullptr; } } void C4MusicFileSDL::CheckIfPlaying() { if (!Mix_PlayingMusic()) Application.MusicSystem.NotifySuccess(); } void C4MusicFileSDL::SetVolume(int iLevel) { Mix_VolumeMusic((int) ((iLevel * MIX_MAX_VOLUME) / 100)); } #elif AUDIO_TK == AUDIO_TK_OPENAL /* Ogg Vobis */ C4MusicFileOgg::C4MusicFileOgg() : last_interruption_time() { for (unsigned int & buffer : buffers) buffer = 0; } C4MusicFileOgg::~C4MusicFileOgg() { Clear(); Stop(); } void C4MusicFileOgg::Clear() { // clear ogg file if (loaded) { ov_clear(&ogg_file); loaded = false; } categories.clear(); is_loading_from_file = false; source_file.Close(); last_source_file_pos = 0; last_playback_pos_sec = 0; last_interruption_time = C4TimeMilliseconds(); } bool C4MusicFileOgg::Init(const char *strFile) { // Clear previous Clear(); // Base init file if (!C4MusicFile::Init(strFile)) return false; // Prepare ogg reader vorbis_info* info; memset(&ogg_file, 0, sizeof(ogg_file)); ov_callbacks callbacks; // Initial file loading // For packed groups, the whole compressed file is kept in memory because reading/seeking inside C4Group is problematic. Uncompress while playing. // This increases startup time a bit. // 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 // and then updating callbacks.read/seek/close/tell_func to read data from the group directly as needed bool is_loading_from_file = FileExists(strFile); void *data_source; if (!is_loading_from_file) { char *file_contents; size_t file_size; if (!C4Group_ReadFile(FileName, &file_contents, &file_size)) return false; data.SetOwnedData((BYTE *)file_contents, file_size); // C4Group preloaded ogg reader callbacks.read_func = &::C4SoundLoaders::VorbisLoader::mem_read_func; callbacks.seek_func = &::C4SoundLoaders::VorbisLoader::mem_seek_func; callbacks.close_func = &::C4SoundLoaders::VorbisLoader::mem_close_func; callbacks.tell_func = &::C4SoundLoaders::VorbisLoader::mem_tell_func; data_source = &data; } else { // Load directly from file if (!source_file.Open(FileName)) return false; // Uncompressed file ogg reader callbacks.read_func = &::C4SoundLoaders::VorbisLoader::file_read_func; callbacks.seek_func = &::C4SoundLoaders::VorbisLoader::file_seek_func; callbacks.close_func = &::C4SoundLoaders::VorbisLoader::file_close_func; callbacks.tell_func = &::C4SoundLoaders::VorbisLoader::file_tell_func; data_source = this; } // open using callbacks either to memory or to file loader if (ov_open_callbacks(data_source, &ogg_file, nullptr, 0, callbacks) != 0) { ov_clear(&ogg_file); return false; } // get information about music info = ov_info(&ogg_file, -1); if (info->channels == 1) ogg_info.format = AL_FORMAT_MONO16; else ogg_info.format = AL_FORMAT_STEREO16; ogg_info.sample_rate = info->rate; ogg_info.sample_length = ov_time_total(&ogg_file, -1) / 1000.0; // Get categories from ogg comment header vorbis_comment *comment = ov_comment(&ogg_file, -1); const char *comment_id = "COMMENT="; int comment_id_len = strlen(comment_id); for (int i = 0; i < comment->comments; ++i) { if (comment->comment_lengths[i] > comment_id_len) { if (SEqual2NoCase(comment->user_comments[i], comment_id, comment_id_len)) { // Add all categories delimeted by ';' const char *categories_string = comment->user_comments[i] + comment_id_len; for (;;) { int delimeter = SCharPos(';', categories_string); StdCopyStrBuf category; category.Copy(categories_string, delimeter >= 0 ? delimeter : SLen(categories_string)); categories.push_back(category); if (delimeter < 0) break; categories_string += delimeter+1; } } } } // File not needed for now UnprepareSourceFileReading(); // mark successfully loaded return loaded = true; } StdStrBuf C4MusicFileOgg::GetDebugInfo() const { StdStrBuf result; result.Append(FileName); result.AppendFormat("[%.0lf]", last_playback_pos_sec); result.AppendChar('['); bool sec = false; for (const auto & category : categories) { if (sec) result.AppendChar(','); result.Append(category.getData()); sec = true; } result.AppendChar(']'); return result; } void C4MusicFileOgg::UnprepareSourceFileReading() { // The file loader could just keep all files open. But if someone symlinks // Music.ocg into their music folder with a million files in it, we would // crash with too many open file handles. So close it for now and reopen // when that piece is actually requested. if (is_loading_from_file && source_file.IsOpen()) { last_source_file_pos = source_file.Tell(); source_file.Close(); } } bool C4MusicFileOgg::PrepareSourceFileReading() { // mem loading always OK if (!is_loading_from_file) return true; // ensure file is open if (!source_file.IsOpen()) { if (!source_file.Open(FileName)) return false; if (last_source_file_pos) if (source_file.Seek(last_source_file_pos, SEEK_SET) < 0) return false; } return true; } bool C4MusicFileOgg::Play(bool loop, double max_resume_time) { // Valid file? if (!loaded) return false; // stop previous if (playing) { if (max_resume_time > 0.0) return true; // no-op Stop(); } // Ensure data reading is ready PrepareSourceFileReading(); // Get channel to use alGenSources(1, (ALuint*)&channel); if (!channel) return false; playing = true; streaming_done = false; this->loop = loop; byte_pos_total = 0; // Resume setting if (max_resume_time > 0) { // Only resume if significant amount of data is left to be played double time_remaining_sec = GetRemainingTime(); if (time_remaining_sec < max_resume_time) last_playback_pos_sec = 0.0; } else { last_playback_pos_sec = 0; } // initial volume setting SetVolume(float(::Config.Sound.MusicVolume) / 100.0f); // prepare read ogg_info.sound_data.resize(num_buffers * buffer_size); alGenBuffers(num_buffers, buffers); ov_time_seek(&ogg_file, last_playback_pos_sec); // Fill initial buffers for (size_t i=0; i 0 && bytes_read_total < buffer_size); // buffer data if (bytes_read_total) { byte_pos_total += bytes_read_total; ALuint buffer = buffers[idx]; alErrorCheck(alBufferData(buffer, ogg_info.format, uncompressed_data, bytes_read_total, ogg_info.sample_rate)); // queue buffer alErrorCheck(alSourceQueueBuffers(channel, 1, &buffer)); } // streaming done? if (bytes_read_total < buffer_size) { // streaming done. loop or done. if (loop) { // reset pos in ogg file ov_raw_seek(&ogg_file, 0); // if looping and nothing has been committed to this buffer yet, try again // except if byte_pos_total==0, i.e. if the piece is completely empty size_t prev_bytes_total = byte_pos_total; byte_pos_total = 0; if (!bytes_read_total && prev_bytes_total) return FillBuffer(idx); return true; } else { // non-looping: we're done. return false; } } else { // might have more data to stream return true; } } void C4MusicFileOgg::Execute() { if (playing) { // get processed buffer count ALint num_processed = 0; alErrorCheck(alGetSourcei(channel, AL_BUFFERS_PROCESSED, &num_processed)); bool done = false; while (num_processed--) { // release processed buffer ALuint buffer; alErrorCheck(alSourceUnqueueBuffers(channel, 1, &buffer)); // add playback time of processed buffer to total playback time ALint buf_bits = 16, buf_chans = 2, buf_freq = 44100; alErrorCheck(alGetBufferi(buffer, AL_BITS, &buf_bits)); alErrorCheck(alGetBufferi(buffer, AL_CHANNELS, &buf_chans)); alErrorCheck(alGetBufferi(buffer, AL_FREQUENCY, &buf_freq)); double buffer_secs = double(buffer_size) / buf_bits / buf_chans / buf_freq * 8; last_playback_pos_sec += buffer_secs; // refill processed buffer size_t buffer_idx; for (buffer_idx=0; buffer_idx