diff --git a/code/CMakeModules/FindFFmpeg.cmake b/code/CMakeModules/FindFFmpeg.cmake new file mode 100644 index 000000000..96cbb6ed0 --- /dev/null +++ b/code/CMakeModules/FindFFmpeg.cmake @@ -0,0 +1,173 @@ +# vim: ts=2 sw=2 +# - Try to find the required ffmpeg components(default: AVFORMAT, AVUTIL, AVCODEC) +# +# Once done this will define +# FFMPEG_FOUND - System has the all required components. +# FFMPEG_INCLUDE_DIRS - Include directory necessary for using the required components headers. +# FFMPEG_LIBRARIES - Link these to use the required ffmpeg components. +# FFMPEG_DEFINITIONS - Compiler switches required for using the required ffmpeg components. +# +# For each of the components it will additionaly set. +# - AVCODEC +# - AVDEVICE +# - AVFORMAT +# - AVUTIL +# - POSTPROC +# - SWSCALE +# - SWRESAMPLE +# the following variables will be defined +# _FOUND - System has +# _INCLUDE_DIRS - Include directory necessary for using the headers +# _LIBRARIES - Link these to use +# _DEFINITIONS - Compiler switches required for using +# _VERSION - The components version +# +# Copyright (c) 2006, Matthias Kretz, +# Copyright (c) 2008, Alexander Neundorf, +# Copyright (c) 2011, Michael Jansen, +# +# Redistribution and use is allowed according to the terms of the BSD license. + +include(FindPackageHandleStandardArgs) + +if(NOT FFmpeg_FIND_COMPONENTS) + set(FFmpeg_FIND_COMPONENTS AVFORMAT AVCODEC AVUTIL) +endif() + +# +### Macro: set_component_found +# +# Marks the given component as found if both *_LIBRARIES AND *_INCLUDE_DIRS is present. +# +macro(set_component_found _component) + if(${_component}_LIBRARIES AND ${_component}_INCLUDE_DIRS) + # message(STATUS " - ${_component} found.") + set(${_component}_FOUND TRUE) + else() + # message(STATUS " - ${_component} not found.") + endif() +endmacro() + +# +### Macro: find_component +# +# Checks for the given component by invoking pkgconfig and then looking up the libraries and +# include directories. +# +macro(find_component _component _pkgconfig _library _header) + if(NOT WIN32) + # use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + find_package(PkgConfig) + if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_${_component} ${_pkgconfig}) + endif() + endif() + + find_path(${_component}_INCLUDE_DIRS ${_header} + HINTS + ${FFMPEGSDK_INC} + ${PC_LIB${_component}_INCLUDEDIR} + ${PC_LIB${_component}_INCLUDE_DIRS} + PATH_SUFFIXES + ffmpeg + ) + + find_library(${_component}_LIBRARIES NAMES ${_library} + HINTS + ${FFMPEGSDK_LIB} + ${PC_LIB${_component}_LIBDIR} + ${PC_LIB${_component}_LIBRARY_DIRS} + ) + + STRING(REGEX REPLACE "/.*" "/version.h" _ver_header ${_header}) + if(EXISTS "${${_component}_INCLUDE_DIRS}/${_ver_header}") + file(STRINGS "${${_component}_INCLUDE_DIRS}/${_ver_header}" version_str REGEX "^#define[\t ]+LIB${_component}_VERSION_M.*") + + foreach(_str "${version_str}") + if(NOT version_maj) + string(REGEX REPLACE "^.*LIB${_component}_VERSION_MAJOR[\t ]+([0-9]*).*$" "\\1" version_maj "${_str}") + endif() + if(NOT version_min) + string(REGEX REPLACE "^.*LIB${_component}_VERSION_MINOR[\t ]+([0-9]*).*$" "\\1" version_min "${_str}") + endif() + if(NOT version_mic) + string(REGEX REPLACE "^.*LIB${_component}_VERSION_MICRO[\t ]+([0-9]*).*$" "\\1" version_mic "${_str}") + endif() + endforeach() + unset(version_str) + + set(${_component}_VERSION "${version_maj}.${version_min}.${version_mic}" CACHE STRING "The ${_component} version number.") + unset(version_maj) + unset(version_min) + unset(version_mic) + endif(EXISTS "${${_component}_INCLUDE_DIRS}/${_ver_header}") + set(${_component}_VERSION ${PC_${_component}_VERSION} CACHE STRING "The ${_component} version number.") + set(${_component}_DEFINITIONS ${PC_${_component}_CFLAGS_OTHER} CACHE STRING "The ${_component} CFLAGS.") + + set_component_found(${_component}) + + mark_as_advanced( + ${_component}_INCLUDE_DIRS + ${_component}_LIBRARIES + ${_component}_DEFINITIONS + ${_component}_VERSION) +endmacro() + + +set(FFMPEGSDK $ENV{FFMPEG_HOME}) +if(FFMPEGSDK) + set(FFMPEGSDK_INC "${FFMPEGSDK}/include") + set(FFMPEGSDK_LIB "${FFMPEGSDK}/lib") +endif() + +# Check for all possible components. +find_component(AVCODEC libavcodec avcodec libavcodec/avcodec.h) +find_component(AVFORMAT libavformat avformat libavformat/avformat.h) +find_component(AVDEVICE libavdevice avdevice libavdevice/avdevice.h) +find_component(AVUTIL libavutil avutil libavutil/avutil.h) +find_component(SWSCALE libswscale swscale libswscale/swscale.h) +find_component(SWRESAMPLE libswresample swresample libswresample/swresample.h) +find_component(POSTPROC libpostproc postproc libpostproc/postprocess.h) + +# Check if the required components were found and add their stuff to the FFMPEG_* vars. +foreach(_component ${FFmpeg_FIND_COMPONENTS}) + if(${_component}_FOUND) + # message(STATUS "Required component ${_component} present.") + set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${${_component}_LIBRARIES}) + set(FFMPEG_DEFINITIONS ${FFMPEG_DEFINITIONS} ${${_component}_DEFINITIONS}) + list(APPEND FFMPEG_INCLUDE_DIRS ${${_component}_INCLUDE_DIRS}) + else() + # message(STATUS "Required component ${_component} missing.") + endif() +endforeach() + +# Build the include path and library list with duplicates removed. +if(FFMPEG_INCLUDE_DIRS) + list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS) +endif() + +if(FFMPEG_LIBRARIES) + list(REMOVE_DUPLICATES FFMPEG_LIBRARIES) +endif() + +# cache the vars. +set(FFMPEG_INCLUDE_DIRS ${FFMPEG_INCLUDE_DIRS} CACHE STRING "The FFmpeg include directories." FORCE) +set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} CACHE STRING "The FFmpeg libraries." FORCE) +set(FFMPEG_DEFINITIONS ${FFMPEG_DEFINITIONS} CACHE STRING "The FFmpeg cflags." FORCE) + +mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARIES FFMPEG_DEFINITIONS) + +# Now set the noncached _FOUND vars for the components. +foreach(_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWRESAMPLE SWSCALE) + set_component_found(${_component}) +endforeach () + +# Compile the list of required vars +set(_FFmpeg_REQUIRED_VARS FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS) +foreach(_component ${FFmpeg_FIND_COMPONENTS}) + list(APPEND _FFmpeg_REQUIRED_VARS ${_component}_LIBRARIES ${_component}_INCLUDE_DIRS) +endforeach() + +# Give a nice error message if some of the required vars are missing. +find_package_handle_standard_args(FFmpeg DEFAULT_MSG ${_FFmpeg_REQUIRED_VARS}) diff --git a/code/nel/CMakeLists.txt b/code/nel/CMakeLists.txt index 84b820d33..3470fcebb 100644 --- a/code/nel/CMakeLists.txt +++ b/code/nel/CMakeLists.txt @@ -20,6 +20,7 @@ ENDIF() IF(WITH_SOUND) FIND_PACKAGE(Ogg) FIND_PACKAGE(Vorbis) + FIND_PACKAGE(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL SWRESAMPLE) IF(WITH_DRIVER_OPENAL) FIND_PACKAGE(OpenAL) diff --git a/code/nel/include/nel/sound/audio_decoder_ffmpeg.h b/code/nel/include/nel/sound/audio_decoder_ffmpeg.h new file mode 100644 index 000000000..c12f52f58 --- /dev/null +++ b/code/nel/include/nel/sound/audio_decoder_ffmpeg.h @@ -0,0 +1,108 @@ +// NeL - MMORPG Framework +// Copyright (C) 2018 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#ifndef NLSOUND_AUDIO_DECODER_FFMPEG_H +#define NLSOUND_AUDIO_DECODER_FFMPEG_H +#include + +#include + +struct AVCodecContext; +struct AVFormatContext; +struct AVIOContext; +struct AVPacket; +struct SwrContext; + +namespace NLSOUND { + +/** + * \brief CAudioDecoderFfmpeg + * \date 2018-10-21 08:08GMT + * \author Meelis Mägi (Nimetu) + * CAudioDecoderFfmpeg + * Create trough IAudioDecoder + */ +class CAudioDecoderFfmpeg : public IAudioDecoder +{ +protected: + NLMISC::IStream *_Stream; + + bool _IsSupported; + bool _Loop; + bool _IsMusicEnded; + sint32 _StreamOffset; + sint32 _StreamSize; + + AVIOContext *_AvioContext; + AVFormatContext *_FormatContext; + AVCodecContext *_AudioContext; + SwrContext *_SwrContext; + + // selected stream + sint32 _AudioStreamIndex; + + // output buffer for decoded frame + SwrContext *_ConvertContext; + +private: + // called from constructor if ffmpeg fails to initialize + // or from destructor to cleanup ffmpeg pointers + void release(); + +public: + CAudioDecoderFfmpeg(NLMISC::IStream *stream, bool loop); + virtual ~CAudioDecoderFfmpeg(); + + inline NLMISC::IStream *getStream() { return _Stream; } + inline sint32 getStreamSize() { return _StreamSize; } + inline sint32 getStreamOffset() { return _StreamOffset; } + + // Return true if ffmpeg is able to decode the stream + bool isFormatSupported() const; + + /// Get information on a music file (only artist and title at the moment). + static bool getInfo(NLMISC::IStream *stream, std::string &artist, std::string &title, float &length); + + /// Get how many bytes the music buffer requires for output minimum. + virtual uint32 getRequiredBytes(); + + /// Get an amount of bytes between minimum and maximum (can be lower than minimum if at end). + virtual uint32 getNextBytes(uint8 *buffer, uint32 minimum, uint32 maximum); + + /// Get the amount of channels (2 is stereo) in output. + virtual uint8 getChannels(); + + /// Get the samples per second (often 44100) in output. + virtual uint getSamplesPerSec(); + + /// Get the bits per sample (often 16) in output. + virtual uint8 getBitsPerSample(); + + /// Get if the music has ended playing (never true if loop). + virtual bool isMusicEnded(); + + /// Get the total time in seconds. + virtual float getLength(); + + /// Set looping + virtual void setLooping(bool loop); +}; /* class CAudioDecoderFfmpeg */ + +} /* namespace NLSOUND */ + +#endif // NLSOUND_AUDIO_DECODER_FFMPEG_H + +/* end of file */ diff --git a/code/nel/src/sound/CMakeLists.txt b/code/nel/src/sound/CMakeLists.txt index e4831c643..bc1816a17 100644 --- a/code/nel/src/sound/CMakeLists.txt +++ b/code/nel/src/sound/CMakeLists.txt @@ -58,6 +58,7 @@ FILE(GLOB STREAM FILE(GLOB STREAM_FILE audio_decoder.cpp ../../include/nel/sound/audio_decoder.h audio_decoder_vorbis.cpp ../../include/nel/sound/audio_decoder_vorbis.h + audio_decoder_ffmpeg.cpp ../../include/nel/sound/audio_decoder_ffmpeg.h stream_file_sound.cpp ../../include/nel/sound/stream_file_sound.h stream_file_source.cpp ../../include/nel/sound/stream_file_source.h ) @@ -95,6 +96,12 @@ IF(WITH_STATIC) TARGET_LINK_LIBRARIES(nelsound ${OGG_LIBRARY}) ENDIF() +IF(FFMPEG_FOUND) + ADD_DEFINITIONS(-DFFMPEG_ENABLED) + INCLUDE_DIRECTORIES(${FFMPEG_INCLUDE_DIRS}) + TARGET_LINK_LIBRARIES(nelsound ${FFMPEG_LIBRARIES}) +ENDIF() + INCLUDE_DIRECTORIES(${LIBXML2_INCLUDE_DIR}) diff --git a/code/nel/src/sound/audio_decoder.cpp b/code/nel/src/sound/audio_decoder.cpp index 6e9e42b61..f0eb80efd 100644 --- a/code/nel/src/sound/audio_decoder.cpp +++ b/code/nel/src/sound/audio_decoder.cpp @@ -37,6 +37,10 @@ // Project includes #include +#ifdef FFMPEG_ENABLED +#include +#endif + using namespace std; using namespace NLMISC; @@ -82,6 +86,17 @@ IAudioDecoder *IAudioDecoder::createAudioDecoder(const std::string &type, NLMISC nlwarning("Stream is NULL"); return NULL; } +#ifdef FFMPEG_ENABLED + try { + CAudioDecoderFfmpeg *decoder = new CAudioDecoderFfmpeg(stream, loop); + return static_cast(decoder); + } + catch(const Exception &e) + { + nlwarning("Exception %s during ffmpeg setup", e.what()); + return NULL; + } +#else std::string type_lower = toLower(type); if (type_lower == "ogg") { @@ -92,23 +107,32 @@ IAudioDecoder *IAudioDecoder::createAudioDecoder(const std::string &type, NLMISC nlwarning("Music file type unknown: '%s'", type_lower.c_str()); return NULL; } +#endif } bool IAudioDecoder::getInfo(const std::string &filepath, std::string &artist, std::string &title, float &length) { std::string lookup = CPath::lookup(filepath, false); if (lookup.empty()) - { + { nlwarning("Music file %s does not exist!", filepath.c_str()); - return false; + return false; } + +#ifdef FFMPEG_ENABLED + CIFile ifile; + ifile.setCacheFileOnOpen(false); + ifile.allowBNPCacheFileOnOpen(false); + if (ifile.open(lookup)) + return CAudioDecoderFfmpeg::getInfo(&ifile, artist, title, length); +#else std::string type = CFile::getExtension(filepath); std::string type_lower = NLMISC::toLower(type); if (type_lower == "ogg") { - CIFile ifile; - ifile.setCacheFileOnOpen(false); + CIFile ifile; + ifile.setCacheFileOnOpen(false); ifile.allowBNPCacheFileOnOpen(false); if (ifile.open(lookup)) return CAudioDecoderVorbis::getInfo(&ifile, artist, title, length); @@ -119,6 +143,7 @@ bool IAudioDecoder::getInfo(const std::string &filepath, std::string &artist, st { nlwarning("Music file type unknown: '%s'", type_lower.c_str()); } +#endif artist.clear(); title.clear(); return false; @@ -132,6 +157,11 @@ void IAudioDecoder::getMusicExtensions(std::vector &extensions) { extensions.push_back("ogg"); } +#ifdef FFMPEG_ENABLED + extensions.push_back("mp3"); + extensions.push_back("flac"); + extensions.push_back("aac"); +#endif // extensions.push_back("wav"); // TODO: Easy. } @@ -139,7 +169,11 @@ void IAudioDecoder::getMusicExtensions(std::vector &extensions) /// Return if a music extension is supported by the nel sound library. bool IAudioDecoder::isMusicExtensionSupported(const std::string &extension) { +#ifdef FFMPEG_ENABLED + return (extension == "ogg" || extension == "mp3" || extension == "flac" || extension == "aac"); +#else return (extension == "ogg"); +#endif } } /* namespace NLSOUND */ diff --git a/code/nel/src/sound/audio_decoder_ffmpeg.cpp b/code/nel/src/sound/audio_decoder_ffmpeg.cpp new file mode 100644 index 000000000..50acb17cc --- /dev/null +++ b/code/nel/src/sound/audio_decoder_ffmpeg.cpp @@ -0,0 +1,430 @@ +// NeL - MMORPG Framework +// Copyright (C) 2018 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +#include "stdsound.h" + +#include + +#define __STDC_CONSTANT_MACROS +extern "C" +{ +#include +#include +#include +#include +}; + +using namespace std; +using namespace NLMISC; + +namespace { + +const std::string av_err2string(sint err) +{ + char buf[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(err, buf, AV_ERROR_MAX_STRING_SIZE); + return (std::string)buf; +} + +void nel_logger(void *ptr, int level, const char *fmt, va_list vargs) +{ + static char msg[1024]; + + const char *module = NULL; + + // AV_LOG_DEBUG, AV_LOG_TRACE + if (level >= AV_LOG_DEBUG) return; + + if (ptr) + { + AVClass *avc = *(AVClass**) ptr; + module = avc->item_name(ptr); + } + + vsnprintf(msg, sizeof(msg), fmt, vargs); + msg[sizeof(msg)-1] = '\0'; + + switch(level) + { + case AV_LOG_PANIC: + // ffmpeg is about to crash so lets throw + nlerror("FFMPEG(P): (%s) %s", module, msg); + break; + case AV_LOG_FATAL: + // ffmpeg had unrecoverable error, corrupted stream or such + nlerrornoex("FFMPEG(F): (%s) %s", module, msg); + break; + case AV_LOG_ERROR: + nlwarning("FFMPEG(E): (%s) %s", module, msg); + break; + case AV_LOG_WARNING: + nlwarning("FFMPEG(W): (%s) %s", module, msg); + break; + case AV_LOG_INFO: + nlinfo("FFMPEG(I): (%s) %s", module, msg); + break; + case AV_LOG_VERBOSE: + nldebug("FFMPEG(V): (%s) %s", module, msg); + break; + case AV_LOG_DEBUG: + nldebug("FFMPEG(D): (%s) %s", module, msg); + break; + default: + nlinfo("FFMPEG: invalid log level:%d (%s) %s", level, module, msg); + break; + } +} + +class CFfmpegInstance +{ +public: + CFfmpegInstance() + { + av_log_set_level(AV_LOG_DEBUG); + av_log_set_callback(nel_logger); + + av_register_all(); + + //avformat_network_init(); + } + + virtual ~CFfmpegInstance() + { + //avformat_network_deinit(); + } +}; + +CFfmpegInstance ffmpeg; + +// Send bytes to ffmpeg +int avio_read_packet(void *opaque, uint8 *buf, int buf_size) +{ + NLSOUND::CAudioDecoderFfmpeg *decoder = static_cast(opaque); + NLMISC::IStream *stream = decoder->getStream(); + nlassert(stream->isReading()); + + uint32 available = decoder->getStreamSize() - stream->getPos(); + if (available == 0) return 0; + + buf_size = FFMIN(buf_size, available); + stream->serialBuffer((uint8 *)buf, buf_size); + return buf_size; +} + +sint64 avio_seek(void *opaque, sint64 offset, int whence) +{ + NLSOUND::CAudioDecoderFfmpeg *decoder = static_cast(opaque); + NLMISC::IStream *stream = decoder->getStream(); + nlassert(stream->isReading()); + + NLMISC::IStream::TSeekOrigin origin; + switch(whence) + { + case SEEK_SET: + origin = NLMISC::IStream::begin; + break; + case SEEK_CUR: + origin = NLMISC::IStream::current; + break; + case SEEK_END: + origin = NLMISC::IStream::end; + break; + case AVSEEK_SIZE: + return decoder->getStreamSize(); + default: + return -1; + } + + stream->seek((sint32) offset, origin); + return stream->getPos(); +} + +}//ns + +namespace NLSOUND { + +// swresample will convert audio to this format +#define FFMPEG_SAMPLE_RATE 44100 +#define FFMPEG_CHANNELS 2 +#define FFMPEG_CHANNEL_LAYOUT AV_CH_LAYOUT_STEREO +#define FFMPEG_BITS_PER_SAMPLE 16 +#define FFMPEG_SAMPLE_FORMAT AV_SAMPLE_FMT_S16 + +CAudioDecoderFfmpeg::CAudioDecoderFfmpeg(NLMISC::IStream *stream, bool loop) +: IAudioDecoder(), + _Stream(stream), _Loop(loop), _IsMusicEnded(false), _StreamSize(0), _IsSupported(false), + _AvioContext(NULL), _FormatContext(NULL), + _AudioContext(NULL), _AudioStreamIndex(0), + _SwrContext(NULL) +{ + _StreamOffset = stream->getPos(); + stream->seek(0, NLMISC::IStream::end); + _StreamSize = stream->getPos(); + stream->seek(_StreamOffset, NLMISC::IStream::begin); + + try { + _FormatContext = avformat_alloc_context(); + if (!_FormatContext) + throw Exception("Can't create AVFormatContext"); + + // avio_ctx_buffer can be reallocated by ffmpeg and assigned to avio_ctx->buffer + uint8 *avio_ctx_buffer = NULL; + size_t avio_ctx_buffer_size = 4096; + avio_ctx_buffer = static_cast(av_malloc(avio_ctx_buffer_size)); + if (!avio_ctx_buffer) + throw Exception("Can't allocate avio context buffer"); + + _AvioContext = avio_alloc_context(avio_ctx_buffer, avio_ctx_buffer_size, 0, this, &avio_read_packet, NULL, &avio_seek); + if (!_AvioContext) + throw Exception("Can't allocate avio context"); + + _FormatContext->pb = _AvioContext; + sint ret = avformat_open_input(&_FormatContext, NULL, NULL, NULL); + if (ret < 0) + throw Exception("avformat_open_input: %d", ret); + + // find stream and then audio codec to see if ffmpeg supports this + _IsSupported = false; + if (avformat_find_stream_info(_FormatContext, NULL) >= 0) + { + _AudioStreamIndex = av_find_best_stream(_FormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); + if (_AudioStreamIndex >= 0) + { + _AudioContext = _FormatContext->streams[_AudioStreamIndex]->codec; + av_opt_set_int(_AudioContext, "refcounted_frames", 1, 0); + + AVCodec *codec = avcodec_find_decoder(_AudioContext->codec_id); + if (codec != NULL && avcodec_open2(_AudioContext, codec, NULL) >= 0) + { + _IsSupported = true; + } + } + } + } + catch(...) + { + release(); + + throw; + } + + if (!_IsSupported) + { + nlwarning("FFMPEG: Decoder created, unknown stream format / codec"); + } +} + +CAudioDecoderFfmpeg::~CAudioDecoderFfmpeg() +{ + release(); +} + +void CAudioDecoderFfmpeg::release() +{ + if (_SwrContext) + swr_free(&_SwrContext); + + if (_AudioContext) + avcodec_close(_AudioContext); + + if (_FormatContext) + avformat_close_input(&_FormatContext); + + if (_AvioContext && _AvioContext->buffer) + av_freep(&_AvioContext->buffer); + + if (_AvioContext) + av_freep(&_AvioContext); +} + +bool CAudioDecoderFfmpeg::isFormatSupported() const +{ + return _IsSupported; +} + +/// Get information on a music file. +bool CAudioDecoderFfmpeg::getInfo(NLMISC::IStream *stream, std::string &artist, std::string &title, float &length) +{ + CAudioDecoderFfmpeg ffmpeg(stream, false); + if (!ffmpeg.isFormatSupported()) + { + title.clear(); + artist.clear(); + length = 0.f; + + return false; + } + + AVDictionaryEntry *tag = NULL; + while((tag = av_dict_get(ffmpeg._FormatContext->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) + { + if (!strcmp(tag->key, "artist")) + { + artist = tag->value; + } + else if (!strcmp(tag->key, "title")) + { + title = tag->value; + } + } + + if (ffmpeg._FormatContext->duration != AV_NOPTS_VALUE) + { + length = ffmpeg._FormatContext->duration * av_q2d(AV_TIME_BASE_Q); + } + else if (ffmpeg._FormatContext->streams[ffmpeg._AudioStreamIndex]->duration != AV_NOPTS_VALUE) + { + length = ffmpeg._FormatContext->streams[ffmpeg._AudioStreamIndex]->duration * av_q2d(ffmpeg._FormatContext->streams[ffmpeg._AudioStreamIndex]->time_base); + } + else + { + length = 0.f; + } + + return true; +} + +uint32 CAudioDecoderFfmpeg::getRequiredBytes() +{ + return 0; // no minimum requirement of bytes to buffer out +} + +uint32 CAudioDecoderFfmpeg::getNextBytes(uint8 *buffer, uint32 minimum, uint32 maximum) +{ + if (_IsMusicEnded) return 0; + nlassert(minimum <= maximum); // can't have this.. + + // TODO: CStreamFileSource::play() will stall when there is no frames on warmup + // supported can be set false if there is an issue creating converter + if (!_IsSupported) + { + _IsMusicEnded = true; + return 1; + } + + uint32 bytes_read = 0; + + AVFrame frame = {0}; + AVPacket packet = {0}; + + if (!_SwrContext) + { + sint64 in_channel_layout = av_get_default_channel_layout(_AudioContext->channels); + _SwrContext = swr_alloc_set_opts(NULL, + // output + FFMPEG_CHANNEL_LAYOUT, FFMPEG_SAMPLE_FORMAT, FFMPEG_SAMPLE_RATE, + // input + in_channel_layout, _AudioContext->sample_fmt, _AudioContext->sample_rate, + 0, NULL); + swr_init(_SwrContext); + } + + sint ret; + while(bytes_read < minimum) + { + // read packet from stream + if ((ret = av_read_frame(_FormatContext, &packet)) < 0) + { + _IsMusicEnded = true; + // TODO: looping + break; + } + + if (packet.stream_index == _AudioStreamIndex) + { + // packet can contain multiple frames + AVPacket first = packet; + int got_frame = 0; + do { + got_frame = 0; + ret = avcodec_decode_audio4(_AudioContext, &frame, &got_frame, &packet); + if (ret < 0) + { + nlwarning("FFMPEG: error decoding audio frame: %s", av_err2string(ret).c_str()); + break; + } + packet.size -= ret; + packet.data += ret; + + if (got_frame) + { + uint32 out_bps = av_get_bytes_per_sample(FFMPEG_SAMPLE_FORMAT) * FFMPEG_CHANNELS; + uint32 max_samples = (maximum - bytes_read) / out_bps; + + uint32 out_samples = av_rescale_rnd(swr_get_delay(_SwrContext, _AudioContext->sample_rate) + frame.nb_samples, + FFMPEG_SAMPLE_RATE, _AudioContext->sample_rate, AV_ROUND_UP); + + if (max_samples > out_samples) + max_samples = out_samples; + + uint32 converted = swr_convert(_SwrContext, &buffer, max_samples, (const uint8 **)frame.extended_data, frame.nb_samples); + uint32 size = out_bps * converted; + + bytes_read += size; + buffer += size; + + av_frame_unref(&frame); + } + } while (got_frame && packet.size > 0); + + av_packet_unref(&first); + } + else + { + ret = 0; + av_packet_unref(&packet); + } + } + + return bytes_read; +} + +uint8 CAudioDecoderFfmpeg::getChannels() +{ + return FFMPEG_CHANNELS; +} + +uint CAudioDecoderFfmpeg::getSamplesPerSec() +{ + return FFMPEG_SAMPLE_RATE; +} + +uint8 CAudioDecoderFfmpeg::getBitsPerSample() +{ + return FFMPEG_BITS_PER_SAMPLE; +} + +bool CAudioDecoderFfmpeg::isMusicEnded() +{ + return _IsMusicEnded; +} + +float CAudioDecoderFfmpeg::getLength() +{ + printf(">> CAudioDecoderFfmpeg::getLength\n"); + // TODO: return (float)ov_time_total(&_OggVorbisFile, -1); + return 0.f; +} + +void CAudioDecoderFfmpeg::setLooping(bool loop) +{ + _Loop = loop; +} + +} /* namespace NLSOUND */ + +/* end of file */ diff --git a/code/ryzom/client/src/interface_v3/music_player.cpp b/code/ryzom/client/src/interface_v3/music_player.cpp index 358189d42..55aa9e342 100644 --- a/code/ryzom/client/src/interface_v3/music_player.cpp +++ b/code/ryzom/client/src/interface_v3/music_player.cpp @@ -372,22 +372,10 @@ public: // no format supported if (extensions.empty()) return; - bool oggSupported = false; - bool mp3Supported = false; - std::string message; for(uint i = 0; i < extensions.size(); ++i) { - if (extensions[i] == "ogg") - { - oggSupported = true; - message += " ogg"; - } - else if (extensions[i] == "mp3") - { - mp3Supported = true; - message += " mp3"; - } + message += " " + extensions[i]; } message += " m3u m3u8"; nlinfo("Media player supports: '%s'", message.substr(1).c_str()); @@ -404,15 +392,9 @@ public: for (i = 0; i < filesToProcess.size(); ++i) { std::string ext = toLower(CFile::getExtension(filesToProcess[i])); - if (ext == "ogg") + if (std::find(extensions.begin(), extensions.end(), ext) != extensions.end()) { - if (oggSupported) - filenames.push_back(filesToProcess[i]); - } - else if (ext == "mp3" || ext == "mp2" || ext == "mp1") - { - if (mp3Supported) - filenames.push_back(filesToProcess[i]); + filenames.push_back(filesToProcess[i]); } else if (ext == "m3u" || ext == "m3u8") { @@ -448,6 +430,7 @@ public: CMusicPlayer::CSongs song; song.Filename = filenames[i]; + // TODO: cache the result for next refresh SoundMngr->getMixer()->getSongTitle(filenames[i], song.Title, song.Length); if (song.Length > 0) songs.push_back (song);