Added: FFmpeg based audio decoder

--HG--
branch : develop
This commit is contained in:
Nimetu 2018-10-23 11:19:07 +03:00
parent c93d8ffd50
commit e994c12983
7 changed files with 761 additions and 25 deletions

View file

@ -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
# <component>_FOUND - System has <component>
# <component>_INCLUDE_DIRS - Include directory necessary for using the <component> headers
# <component>_LIBRARIES - Link these to use <component>
# <component>_DEFINITIONS - Compiler switches required for using <component>
# <component>_VERSION - The components version
#
# Copyright (c) 2006, Matthias Kretz, <kretz@kde.org>
# Copyright (c) 2008, Alexander Neundorf, <neundorf@kde.org>
# Copyright (c) 2011, Michael Jansen, <kde@michael-jansen.biz>
#
# 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})

View file

@ -20,6 +20,7 @@ ENDIF()
IF(WITH_SOUND) IF(WITH_SOUND)
FIND_PACKAGE(Ogg) FIND_PACKAGE(Ogg)
FIND_PACKAGE(Vorbis) FIND_PACKAGE(Vorbis)
FIND_PACKAGE(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL SWRESAMPLE)
IF(WITH_DRIVER_OPENAL) IF(WITH_DRIVER_OPENAL)
FIND_PACKAGE(OpenAL) FIND_PACKAGE(OpenAL)

View file

@ -0,0 +1,108 @@
// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// 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 <http://www.gnu.org/licenses/>.
#ifndef NLSOUND_AUDIO_DECODER_FFMPEG_H
#define NLSOUND_AUDIO_DECODER_FFMPEG_H
#include <nel/misc/types_nl.h>
#include <nel/sound/audio_decoder.h>
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 */

View file

@ -58,6 +58,7 @@ FILE(GLOB STREAM
FILE(GLOB STREAM_FILE FILE(GLOB STREAM_FILE
audio_decoder.cpp ../../include/nel/sound/audio_decoder.h audio_decoder.cpp ../../include/nel/sound/audio_decoder.h
audio_decoder_vorbis.cpp ../../include/nel/sound/audio_decoder_vorbis.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_sound.cpp ../../include/nel/sound/stream_file_sound.h
stream_file_source.cpp ../../include/nel/sound/stream_file_source.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}) TARGET_LINK_LIBRARIES(nelsound ${OGG_LIBRARY})
ENDIF() 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}) INCLUDE_DIRECTORIES(${LIBXML2_INCLUDE_DIR})

View file

@ -37,6 +37,10 @@
// Project includes // Project includes
#include <nel/sound/audio_decoder_vorbis.h> #include <nel/sound/audio_decoder_vorbis.h>
#ifdef FFMPEG_ENABLED
#include <nel/sound/audio_decoder_ffmpeg.h>
#endif
using namespace std; using namespace std;
using namespace NLMISC; using namespace NLMISC;
@ -82,6 +86,17 @@ IAudioDecoder *IAudioDecoder::createAudioDecoder(const std::string &type, NLMISC
nlwarning("Stream is NULL"); nlwarning("Stream is NULL");
return NULL; return NULL;
} }
#ifdef FFMPEG_ENABLED
try {
CAudioDecoderFfmpeg *decoder = new CAudioDecoderFfmpeg(stream, loop);
return static_cast<IAudioDecoder *>(decoder);
}
catch(const Exception &e)
{
nlwarning("Exception %s during ffmpeg setup", e.what());
return NULL;
}
#else
std::string type_lower = toLower(type); std::string type_lower = toLower(type);
if (type_lower == "ogg") if (type_lower == "ogg")
{ {
@ -92,6 +107,7 @@ IAudioDecoder *IAudioDecoder::createAudioDecoder(const std::string &type, NLMISC
nlwarning("Music file type unknown: '%s'", type_lower.c_str()); nlwarning("Music file type unknown: '%s'", type_lower.c_str());
return NULL; return NULL;
} }
#endif
} }
bool IAudioDecoder::getInfo(const std::string &filepath, std::string &artist, std::string &title, float &length) bool IAudioDecoder::getInfo(const std::string &filepath, std::string &artist, std::string &title, float &length)
@ -102,6 +118,14 @@ bool IAudioDecoder::getInfo(const std::string &filepath, std::string &artist, st
nlwarning("Music file %s does not exist!", filepath.c_str()); 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 = CFile::getExtension(filepath);
std::string type_lower = NLMISC::toLower(type); std::string type_lower = NLMISC::toLower(type);
@ -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()); nlwarning("Music file type unknown: '%s'", type_lower.c_str());
} }
#endif
artist.clear(); title.clear(); artist.clear(); title.clear();
return false; return false;
@ -132,6 +157,11 @@ void IAudioDecoder::getMusicExtensions(std::vector<std::string> &extensions)
{ {
extensions.push_back("ogg"); 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. // extensions.push_back("wav"); // TODO: Easy.
} }
@ -139,7 +169,11 @@ void IAudioDecoder::getMusicExtensions(std::vector<std::string> &extensions)
/// Return if a music extension is supported by the nel sound library. /// Return if a music extension is supported by the nel sound library.
bool IAudioDecoder::isMusicExtensionSupported(const std::string &extension) bool IAudioDecoder::isMusicExtensionSupported(const std::string &extension)
{ {
#ifdef FFMPEG_ENABLED
return (extension == "ogg" || extension == "mp3" || extension == "flac" || extension == "aac");
#else
return (extension == "ogg"); return (extension == "ogg");
#endif
} }
} /* namespace NLSOUND */ } /* namespace NLSOUND */

View file

@ -0,0 +1,430 @@
// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// 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 <http://www.gnu.org/licenses/>.
#include "stdsound.h"
#include <nel/sound/audio_decoder_ffmpeg.h>
#define __STDC_CONSTANT_MACROS
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <libavutil/opt.h>
};
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<NLSOUND::CAudioDecoderFfmpeg *>(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<NLSOUND::CAudioDecoderFfmpeg *>(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<uint8 *>(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 */

View file

@ -372,22 +372,10 @@ public:
// no format supported // no format supported
if (extensions.empty()) return; if (extensions.empty()) return;
bool oggSupported = false;
bool mp3Supported = false;
std::string message; std::string message;
for(uint i = 0; i < extensions.size(); ++i) for(uint i = 0; i < extensions.size(); ++i)
{ {
if (extensions[i] == "ogg") message += " " + extensions[i];
{
oggSupported = true;
message += " ogg";
}
else if (extensions[i] == "mp3")
{
mp3Supported = true;
message += " mp3";
}
} }
message += " m3u m3u8"; message += " m3u m3u8";
nlinfo("Media player supports: '%s'", message.substr(1).c_str()); nlinfo("Media player supports: '%s'", message.substr(1).c_str());
@ -404,15 +392,9 @@ public:
for (i = 0; i < filesToProcess.size(); ++i) for (i = 0; i < filesToProcess.size(); ++i)
{ {
std::string ext = toLower(CFile::getExtension(filesToProcess[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]);
filenames.push_back(filesToProcess[i]);
}
else if (ext == "mp3" || ext == "mp2" || ext == "mp1")
{
if (mp3Supported)
filenames.push_back(filesToProcess[i]);
} }
else if (ext == "m3u" || ext == "m3u8") else if (ext == "m3u" || ext == "m3u8")
{ {
@ -448,6 +430,7 @@ public:
CMusicPlayer::CSongs song; CMusicPlayer::CSongs song;
song.Filename = filenames[i]; song.Filename = filenames[i];
// TODO: cache the result for next refresh
SoundMngr->getMixer()->getSongTitle(filenames[i], song.Title, song.Length); SoundMngr->getMixer()->getSongTitle(filenames[i], song.Title, song.Length);
if (song.Length > 0) if (song.Length > 0)
songs.push_back (song); songs.push_back (song);