Compare commits
12 Commits
0-2-1
...
3c28763a61
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c28763a61 | |||
| 33b79e55e2 | |||
| 7de84cbf30 | |||
| fc4b6f8ced | |||
| b8996d55e4 | |||
| 977eb5a456 | |||
| a358a8593d | |||
| f4e2a6aa31 | |||
| 51a0138877 | |||
| 4134cf0549 | |||
| 9a0ef7d76e | |||
| 2606634c11 |
@@ -20,5 +20,8 @@ build
|
||||
lib/linux-x86_64
|
||||
build-ffmpeg
|
||||
|
||||
.DS_Store
|
||||
*.user
|
||||
|
||||
|
||||
/ffmpeg-audio/.qtcreator
|
||||
|
||||
@@ -10,8 +10,12 @@ all:
|
||||
|
||||
install: all
|
||||
mkdir -p lib/$(SUBDIR)
|
||||
@echo "copying from src/$(SUBDIR) to lib/$(SUBDIR)"
|
||||
exit
|
||||
(cd src/$(SUBDIR);tar cf - . ) | (cd lib/$(SUBDIR); tar xvf - )
|
||||
FILES=`ls build/*.so build-ffmpeg/*.so` 2>/dev/null; if [ "$$FILES" != "" ]; then cp $$FILES lib/$(SUBDIR); fi
|
||||
FILES=`ls build/*.dll build-ffmpeg/*.dll` 2>/dev/null; if [ "$$FILES" != "" ]; then cp $$FILES lib/$(SUBDIR); fi
|
||||
FILES=`ls build/*.dylib build-ffmpeg/*.dylib` 2>/dev/null; if [ "$$FILES" != "" ]; then cp $$FILES lib/$(SUBDIR); fi
|
||||
|
||||
test: install
|
||||
cp lib/linux-x86_64/*.so ~/.local/share/racket/racket-sound-lib/linux-x86_64
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
# racket-sound-lib
|
||||
|
||||
Binary packages for the racket-sound FFI binding
|
||||
|
||||
## Using on Mac OS X
|
||||
|
||||
Make sure you have libao, libFLAC, mpg123 and ffmpeg-full installed using brew.
|
||||
|
||||
% brew install libao
|
||||
% brew install flac
|
||||
% brew install mpg123
|
||||
% brew install ffmpeg-full
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ project(ao-play-async LANGUAGES C)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
|
||||
add_library(ao-play-async SHARED
|
||||
ao_playasync.c
|
||||
ao_playasync.h
|
||||
@@ -16,6 +18,12 @@ if(WIN32)
|
||||
target_link_directories(ao-play-async PRIVATE ../lib/windows-x86_64)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(AO_HOME /opt/homebrew)
|
||||
include_directories(${AO_HOME}/include)
|
||||
target_link_directories(ao-play-async PRIVATE ${AO_HOME}/lib)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(ao-play-async PRIVATE libao-1.2.2 winmm.lib ksuser.lib)
|
||||
else()
|
||||
|
||||
+403
-339
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
#define AOPLAYASYNC_EXPORT extern
|
||||
#endif
|
||||
|
||||
#define VERSION 2
|
||||
#include "../ffi_version.h"
|
||||
|
||||
typedef enum {
|
||||
ao = 1,
|
||||
@@ -46,4 +46,5 @@ AOPLAYASYNC_EXPORT double ao_volume_async(void *ao_handle);
|
||||
AOPLAYASYNC_EXPORT int ao_bufsize_async(void *handle);
|
||||
AOPLAYASYNC_EXPORT int ao_real_output_bits_async(void *handle);
|
||||
|
||||
|
||||
#endif // AO_PLAYASYNC_H
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef __FFI_VERSION__
|
||||
#define __FFI_VERSION__
|
||||
|
||||
#define VERSION_MAJOR 1
|
||||
#define VERSION_MINOR 0
|
||||
#define VERSION_PATCH 1
|
||||
|
||||
#define ffi_version() ((VERSION_MAJOR << 16) + (VERSION_MINOR << 8) + VERSION_PATCH)
|
||||
|
||||
#endif
|
||||
@@ -8,6 +8,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
add_library(ffmpeg_audio SHARED
|
||||
ffmpeg_audio.cpp
|
||||
ffmpeg_audio.h
|
||||
../ffi_version.h
|
||||
ffmpeg_audio_refactored.cpp
|
||||
)
|
||||
|
||||
add_executable(demo_ffmpeg_audio
|
||||
@@ -21,6 +23,11 @@ if(WIN32)
|
||||
target_link_directories(ffmpeg_audio PRIVATE ${FFMPEG_LIB})
|
||||
set(FFMPEG_LIBRARIES avcodec.lib avformat.lib swresample.lib avutil.lib avdevice.lib avfilter.lib swscale.lib)
|
||||
target_link_directories(demo_ffmpeg_audio PRIVATE ${FFMPEG_LIB})
|
||||
elseif(APPLE)
|
||||
target_link_directories(ffmpeg_audio PRIVATE /opt/homebrew/opt/ffmpeg-full/lib)
|
||||
include_directories(/opt/homebrew/opt/ffmpeg-full/include)
|
||||
set(FFMPEG_LIBRARIES avcodec avformat swresample avutil avdevice avfilter swscale)
|
||||
target_link_directories(demo_ffmpeg_audio PRIVATE /opt/homebrew/opt/ffmpeg-full/lib)
|
||||
else()
|
||||
set(FFMPEG_LIBRARIES avcodec avformat swresample avutil avdevice avfilter swscale)
|
||||
endif()
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
*/
|
||||
|
||||
#include "ffmpeg_audio.h"
|
||||
#include "../ffi_version.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -1021,3 +1022,8 @@ const char *fmpg_int_version2string(int ver)
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
int fmpg_version()
|
||||
{
|
||||
return ffi_version();
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ FFMPEG_EXTERN int64_t fmpg_sample_position(fmpg_instance *instance);
|
||||
/* Approximate start time of the current decoded block in seconds. */
|
||||
FFMPEG_EXTERN double fmpg_timecode(fmpg_instance *instance);
|
||||
|
||||
FFMPEG_EXTERN int fmpg_version();
|
||||
FFMPEG_EXTERN const char *fmpg_ffmpeg_version();
|
||||
FFMPEG_EXTERN const char *fmpg_int_version2string(int ver);
|
||||
FFMPEG_EXTERN int fmpg_compatible_ffmpeg();
|
||||
|
||||
@@ -0,0 +1,901 @@
|
||||
/*
|
||||
* Audio-only FFmpeg wrapper with a plain C ABI.
|
||||
*
|
||||
* This implementation intentionally hides FFmpeg concepts from the public API:
|
||||
*
|
||||
* - no stream_index in the API;
|
||||
* - no AVPacket/package object in the API;
|
||||
* - no explicit decoder object in the API;
|
||||
* - no metadata/tag API exposed to C callers.
|
||||
*
|
||||
* Internally the instance owns everything needed to decode one selected audio
|
||||
* stream. The caller simply opens a file and repeatedly calls fmpg_decode_next().
|
||||
*/
|
||||
|
||||
#include "ffmpeg_audio.h"
|
||||
#include "../ffi_version.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavutil/channel_layout.h>
|
||||
#include <libavutil/samplefmt.h>
|
||||
#include <libswresample/swresample.h>
|
||||
}
|
||||
|
||||
#define MSG0(type, msg) fprintf(stderr, type ":");fprintf(stderr, "%s", __FUNCTION__);fprintf(stderr, ": %s\n", msg)
|
||||
#define MSG1(type, msg, a) fprintf(stderr, type ":");fprintf(stderr, "%s", __FUNCTION__);fprintf(stderr, ": " msg "\n", a)
|
||||
#define MSG2(type, msg, a, b) fprintf(stderr, type ":");fprintf(stderr, "%s", __FUNCTION__);fprintf(stderr, ": " msg "\n", a, b)
|
||||
#define MSG3(type, msg, a, b, c) fprintf(stderr, type ":");fprintf(stderr, "%s", __FUNCTION__);fprintf(stderr, ": " msg "\n", a, b, c)
|
||||
|
||||
#define INFO0(msg) MSG0("info", msg)
|
||||
#define INFO1(msg, a) MSG1("info", msg, a)
|
||||
#define INFO2(msg, a, b) MSG2("info", msg, a, b)
|
||||
#define INFO3(msg, a, b, c) MSG3("info", msg, a, b, c)
|
||||
|
||||
#define ERROR0(msg) MSG0("error", msg)
|
||||
#define ERROR1(msg, a) MSG1("error", msg, a)
|
||||
#define ERROR2(msg, a, b) MSG2("error", msg, a, b)
|
||||
#define ERROR3(msg, a, b, c) MSG3("error", msg, a, b, c)
|
||||
|
||||
static constexpr int FMPG_OUTPUT_BITS = 32;
|
||||
static constexpr int FMPG_OUTPUT_BYTES = 4;
|
||||
static constexpr AVSampleFormat FMPG_OUTPUT_FMT = AV_SAMPLE_FMT_S32;
|
||||
|
||||
struct audio_info_storage {
|
||||
int audio_stream_count = 0;
|
||||
int selected_stream_index = -1; /* Internal FFmpeg stream index. */
|
||||
int sample_rate = 0;
|
||||
int channels = 0;
|
||||
int64_t duration_ms = -1;
|
||||
int64_t duration_samples = -1; /* Output sample frames. */
|
||||
|
||||
void clear()
|
||||
{
|
||||
audio_stream_count = 0;
|
||||
selected_stream_index = -1;
|
||||
sample_rate = 0;
|
||||
channels = 0;
|
||||
duration_ms = -1;
|
||||
duration_samples = -1;
|
||||
}
|
||||
};
|
||||
|
||||
struct decoder_storage {
|
||||
const AVCodec *codec = nullptr;
|
||||
AVCodecContext *codec_ctx = nullptr;
|
||||
AVFrame *frame = nullptr;
|
||||
SwrContext *swr_ctx = nullptr;
|
||||
|
||||
std::vector<uint8_t> pcm;
|
||||
|
||||
bool eof_seen = false;
|
||||
bool decoder_drained = false;
|
||||
|
||||
double timecode = 0.0;
|
||||
|
||||
int64_t last_samples = 0;
|
||||
int64_t buffer_start_sample = 0;
|
||||
int64_t next_sample_position = 0;
|
||||
|
||||
/* >= 0 while a seek has requested us to discard decoded pre-roll samples. */
|
||||
int64_t discard_until_sample = -1;
|
||||
|
||||
void clear_output()
|
||||
{
|
||||
pcm.clear();
|
||||
last_samples = 0;
|
||||
buffer_start_sample = next_sample_position;
|
||||
}
|
||||
|
||||
void free_ffmpeg()
|
||||
{
|
||||
avcodec_free_context(&codec_ctx);
|
||||
av_frame_free(&frame);
|
||||
swr_free(&swr_ctx);
|
||||
codec = nullptr;
|
||||
pcm.clear();
|
||||
eof_seen = false;
|
||||
decoder_drained = false;
|
||||
timecode = 0.0;
|
||||
last_samples = 0;
|
||||
buffer_start_sample = 0;
|
||||
next_sample_position = 0;
|
||||
discard_until_sample = -1;
|
||||
}
|
||||
|
||||
~decoder_storage()
|
||||
{
|
||||
free_ffmpeg();
|
||||
}
|
||||
};
|
||||
|
||||
struct __fmpg_instance__ {
|
||||
bool opened = false;
|
||||
AVFormatContext *format_ctx = nullptr;
|
||||
audio_info_storage audio_info;
|
||||
decoder_storage decoder;
|
||||
|
||||
~__fmpg_instance__()
|
||||
{
|
||||
if (format_ctx) {
|
||||
avformat_close_input(&format_ctx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static int count_audio_streams(const AVFormatContext *ctx)
|
||||
{
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (unsigned i = 0; i < ctx->nb_streams; ++i) {
|
||||
const AVCodecParameters *par = ctx->streams[i]->codecpar;
|
||||
if (par && par->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int64_t milliseconds_from_seconds(double seconds)
|
||||
{
|
||||
if (seconds < 0.0) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<int64_t>(seconds * 1000.0 + 0.5);
|
||||
}
|
||||
|
||||
static int64_t samples_from_seconds(double seconds, int sample_rate)
|
||||
{
|
||||
if (seconds < 0.0 || sample_rate <= 0) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<int64_t>(seconds * static_cast<double>(sample_rate) +
|
||||
0.5);
|
||||
}
|
||||
|
||||
static double stream_duration_seconds(const AVStream *stream)
|
||||
{
|
||||
if (!stream || stream->duration == AV_NOPTS_VALUE) {
|
||||
return -1.0;
|
||||
}
|
||||
return static_cast<double>(stream->duration) * av_q2d(stream->time_base);
|
||||
}
|
||||
|
||||
static double format_duration_seconds(const AVFormatContext *ctx)
|
||||
{
|
||||
if (!ctx || ctx->duration == AV_NOPTS_VALUE) {
|
||||
return -1.0;
|
||||
}
|
||||
return static_cast<double>(ctx->duration) /
|
||||
static_cast<double>(AV_TIME_BASE);
|
||||
}
|
||||
|
||||
static int64_t timestamp_to_samples(int64_t timestamp,
|
||||
const AVStream *stream,
|
||||
int sample_rate)
|
||||
{
|
||||
if (!stream || timestamp == AV_NOPTS_VALUE || sample_rate <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const double seconds = static_cast<double>(timestamp) *
|
||||
av_q2d(stream->time_base);
|
||||
return samples_from_seconds(seconds, sample_rate);
|
||||
}
|
||||
|
||||
static bool fill_audio_info(fmpg_instance *self)
|
||||
{
|
||||
AVFormatContext *ctx = self->format_ctx;
|
||||
|
||||
self->audio_info.clear();
|
||||
self->audio_info.audio_stream_count = count_audio_streams(ctx);
|
||||
|
||||
const int best = av_find_best_stream(ctx,
|
||||
AVMEDIA_TYPE_AUDIO,
|
||||
-1,
|
||||
-1,
|
||||
nullptr,
|
||||
0);
|
||||
if (best < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AVStream *stream = ctx->streams[best];
|
||||
const AVCodecParameters *par = stream->codecpar;
|
||||
|
||||
if (!par || par->codec_type != AVMEDIA_TYPE_AUDIO ||
|
||||
par->sample_rate <= 0 || par->ch_layout.nb_channels <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self->audio_info.selected_stream_index = best;
|
||||
self->audio_info.sample_rate = par->sample_rate;
|
||||
self->audio_info.channels = par->ch_layout.nb_channels;
|
||||
|
||||
double seconds = stream_duration_seconds(stream);
|
||||
if (seconds < 0.0) {
|
||||
seconds = format_duration_seconds(ctx);
|
||||
}
|
||||
|
||||
self->audio_info.duration_ms = milliseconds_from_seconds(seconds);
|
||||
self->audio_info.duration_samples =
|
||||
samples_from_seconds(seconds, self->audio_info.sample_rate);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool instance_ready(const fmpg_instance *instance)
|
||||
{
|
||||
return instance && instance->opened && instance->format_ctx &&
|
||||
instance->audio_info.selected_stream_index >= 0 &&
|
||||
instance->decoder.codec_ctx && instance->decoder.swr_ctx;
|
||||
}
|
||||
|
||||
static bool init_codec_context(fmpg_instance *self)
|
||||
{
|
||||
decoder_storage &dec = self->decoder;
|
||||
const int stream_index = self->audio_info.selected_stream_index;
|
||||
const AVCodecParameters *par =
|
||||
self->format_ctx->streams[stream_index]->codecpar;
|
||||
|
||||
dec.codec = avcodec_find_decoder(par->codec_id);
|
||||
if (!dec.codec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dec.codec_ctx = avcodec_alloc_context3(dec.codec);
|
||||
if (!dec.codec_ctx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avcodec_parameters_to_context(dec.codec_ctx, par) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avcodec_open2(dec.codec_ctx, dec.codec, nullptr) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dec.frame = av_frame_alloc();
|
||||
return dec.frame != nullptr;
|
||||
}
|
||||
|
||||
static bool init_resampler(fmpg_instance *self)
|
||||
{
|
||||
decoder_storage &dec = self->decoder;
|
||||
const AVChannelLayout *layout = &dec.codec_ctx->ch_layout;
|
||||
|
||||
if (layout->nb_channels <= 0 || dec.codec_ctx->sample_rate <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (swr_alloc_set_opts2(&dec.swr_ctx,
|
||||
layout,
|
||||
FMPG_OUTPUT_FMT,
|
||||
dec.codec_ctx->sample_rate,
|
||||
layout,
|
||||
dec.codec_ctx->sample_fmt,
|
||||
dec.codec_ctx->sample_rate,
|
||||
0,
|
||||
nullptr) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return swr_init(dec.swr_ctx) >= 0;
|
||||
}
|
||||
|
||||
static bool init_decoder(fmpg_instance *self)
|
||||
{
|
||||
self->decoder.free_ffmpeg();
|
||||
return init_codec_context(self) && init_resampler(self);
|
||||
}
|
||||
|
||||
int fmpg_compatible_ffmpeg()
|
||||
{
|
||||
int compiled_avformat_major = LIBAVFORMAT_VERSION_MAJOR;
|
||||
int compiled_avcodec_major = LIBAVCODEC_VERSION_MAJOR;
|
||||
int compiled_swresample_major = LIBSWRESAMPLE_VERSION_MAJOR;
|
||||
int compiled_avutil_major = LIBAVUTIL_VERSION_MAJOR;
|
||||
|
||||
int current_avformat_major = AV_VERSION_MAJOR(avformat_version());
|
||||
int current_avcodec_major = AV_VERSION_MAJOR(avcodec_version());
|
||||
int current_swresample_major = AV_VERSION_MAJOR(swresample_version());
|
||||
int current_avutil_major = AV_VERSION_MAJOR(avutil_version());
|
||||
|
||||
auto comp = [](const char *lib, int cv, int rv) {
|
||||
if (cv != rv) {
|
||||
ERROR3("FFMPEG %s Major versions not equal. Compile time: %d, runtime: %d", lib, cv, rv);
|
||||
}
|
||||
};
|
||||
|
||||
comp("AVFormat", compiled_avformat_major, current_avformat_major);
|
||||
comp("AVCodec", compiled_avcodec_major, current_avcodec_major);
|
||||
comp("SWResample", compiled_swresample_major, current_swresample_major);
|
||||
comp("AVUtil", compiled_avutil_major, current_avutil_major);
|
||||
|
||||
int compatible = (compiled_avformat_major == current_avformat_major) &&
|
||||
(compiled_avcodec_major == current_avcodec_major) &&
|
||||
(compiled_swresample_major == current_swresample_major) &&
|
||||
(compiled_avutil_major == current_avutil_major);
|
||||
|
||||
return compatible;
|
||||
}
|
||||
|
||||
fmpg_instance *fmpg_init(void)
|
||||
{
|
||||
if (!fmpg_compatible_ffmpeg()) {
|
||||
ERROR0("Compiled major ffmpeg version ≃ runtime major version, not compatible.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try {
|
||||
return new fmpg_instance();
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void fmpg_free(fmpg_instance *instance)
|
||||
{
|
||||
delete instance;
|
||||
}
|
||||
|
||||
int fmpg_open_file(fmpg_instance *instance, const char *filename)
|
||||
{
|
||||
if (!instance || instance->opened || !filename) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (instance->format_ctx != nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int r = avformat_open_input(&instance->format_ctx,
|
||||
filename,
|
||||
nullptr,
|
||||
nullptr);
|
||||
if (r < 0) {
|
||||
fmpg_close(instance);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (avformat_find_stream_info(instance->format_ctx, nullptr) < 0) {
|
||||
fmpg_close(instance);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (!fill_audio_info(instance) || !init_decoder(instance)) {
|
||||
fmpg_close(instance);
|
||||
return 0;
|
||||
}
|
||||
|
||||
instance->opened = true;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void fmpg_close(fmpg_instance *instance)
|
||||
{
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance->decoder.free_ffmpeg();
|
||||
|
||||
if (instance->format_ctx) {
|
||||
avformat_close_input(&instance->format_ctx);
|
||||
}
|
||||
|
||||
instance->opened = false;
|
||||
instance->audio_info.clear();
|
||||
}
|
||||
|
||||
int fmpg_is_open(fmpg_instance *instance)
|
||||
{
|
||||
return instance_ready(instance) ? 1 : 0;
|
||||
}
|
||||
|
||||
int fmpg_audio_stream_count(fmpg_instance *instance)
|
||||
{
|
||||
return instance && instance->opened ? instance->audio_info.audio_stream_count
|
||||
: 0;
|
||||
}
|
||||
|
||||
int fmpg_audio_sample_rate(fmpg_instance *instance)
|
||||
{
|
||||
return instance_ready(instance) ? instance->audio_info.sample_rate : 0;
|
||||
}
|
||||
|
||||
int fmpg_audio_channels(fmpg_instance *instance)
|
||||
{
|
||||
return instance_ready(instance) ? instance->audio_info.channels : 0;
|
||||
}
|
||||
|
||||
int fmpg_audio_bits_per_sample(fmpg_instance *)
|
||||
{
|
||||
return FMPG_OUTPUT_BITS;
|
||||
}
|
||||
|
||||
int fmpg_audio_bytes_per_sample(fmpg_instance *)
|
||||
{
|
||||
return FMPG_OUTPUT_BYTES;
|
||||
}
|
||||
|
||||
int64_t fmpg_duration_ms(fmpg_instance *instance)
|
||||
{
|
||||
return instance_ready(instance) ? instance->audio_info.duration_ms : -1;
|
||||
}
|
||||
|
||||
int64_t fmpg_duration_samples(fmpg_instance *instance)
|
||||
{
|
||||
return instance_ready(instance) ? instance->audio_info.duration_samples : -1;
|
||||
}
|
||||
|
||||
|
||||
static bool append_bytes(decoder_storage &dec,
|
||||
const uint8_t *src,
|
||||
size_t bytes)
|
||||
{
|
||||
if (!bytes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bytes > static_cast<size_t>(std::numeric_limits<int>::max()) -
|
||||
dec.pcm.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const size_t old_size = dec.pcm.size();
|
||||
dec.pcm.resize(old_size + bytes);
|
||||
std::memcpy(dec.pcm.data() + old_size, src, bytes);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool append_converted_frame(fmpg_instance *self,
|
||||
const AVFrame *frame)
|
||||
{
|
||||
decoder_storage &dec = self->decoder;
|
||||
const int channels = self->audio_info.channels;
|
||||
const int sample_rate = self->audio_info.sample_rate;
|
||||
|
||||
if (channels <= 0 || frame->nb_samples <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const int max_out_samples = swr_get_out_samples(dec.swr_ctx,
|
||||
frame->nb_samples);
|
||||
if (max_out_samples <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int max_bytes = av_samples_get_buffer_size(nullptr,
|
||||
channels,
|
||||
max_out_samples,
|
||||
FMPG_OUTPUT_FMT,
|
||||
1);
|
||||
if (max_bytes <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> tmp(static_cast<size_t>(max_bytes));
|
||||
uint8_t *out_planes[1] = { tmp.data() };
|
||||
|
||||
const int out_samples = swr_convert(dec.swr_ctx,
|
||||
out_planes,
|
||||
max_out_samples,
|
||||
const_cast<const uint8_t **>(frame->data),
|
||||
frame->nb_samples);
|
||||
if (out_samples < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int used_bytes = av_samples_get_buffer_size(nullptr,
|
||||
channels,
|
||||
out_samples,
|
||||
FMPG_OUTPUT_FMT,
|
||||
1);
|
||||
if (used_bytes < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int stream_index = self->audio_info.selected_stream_index;
|
||||
const AVStream *stream = self->format_ctx->streams[stream_index];
|
||||
|
||||
int64_t frame_start = timestamp_to_samples(frame->best_effort_timestamp,
|
||||
stream,
|
||||
sample_rate);
|
||||
if (frame_start < 0) {
|
||||
frame_start = dec.next_sample_position;
|
||||
}
|
||||
|
||||
int64_t keep_start = frame_start;
|
||||
int keep_samples = out_samples;
|
||||
size_t byte_offset = 0;
|
||||
|
||||
/*
|
||||
* After seeking, FFmpeg may first return decoded samples from before the
|
||||
* requested position. Discard them so public sample positions refer to the
|
||||
* actual music position requested by the caller.
|
||||
*/
|
||||
if (dec.discard_until_sample >= 0) {
|
||||
const int64_t target = dec.discard_until_sample;
|
||||
const int64_t frame_end = frame_start + out_samples;
|
||||
|
||||
if (frame_end <= target) {
|
||||
dec.next_sample_position = frame_end;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (frame_start < target) {
|
||||
const int64_t drop = target - frame_start;
|
||||
if (drop > 0 && drop < out_samples) {
|
||||
byte_offset = static_cast<size_t>(drop) *
|
||||
static_cast<size_t>(channels) *
|
||||
FMPG_OUTPUT_BYTES;
|
||||
keep_samples = static_cast<int>(out_samples - drop);
|
||||
keep_start = target;
|
||||
}
|
||||
}
|
||||
|
||||
dec.discard_until_sample = -1;
|
||||
}
|
||||
|
||||
if (keep_samples <= 0) {
|
||||
dec.next_sample_position = frame_start + out_samples;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dec.pcm.empty()) {
|
||||
dec.buffer_start_sample = keep_start;
|
||||
dec.timecode = static_cast<double>(keep_start) /
|
||||
static_cast<double>(sample_rate);
|
||||
}
|
||||
|
||||
const size_t keep_bytes = static_cast<size_t>(keep_samples) *
|
||||
static_cast<size_t>(channels) *
|
||||
FMPG_OUTPUT_BYTES;
|
||||
|
||||
if (!append_bytes(dec, tmp.data() + byte_offset, keep_bytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dec.last_samples += keep_samples;
|
||||
dec.next_sample_position = keep_start + keep_samples;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int receive_available_frames(fmpg_instance *self)
|
||||
{
|
||||
decoder_storage &dec = self->decoder;
|
||||
int produced = 0;
|
||||
|
||||
for (;;) {
|
||||
const int ret = avcodec_receive_frame(dec.codec_ctx, dec.frame);
|
||||
|
||||
if (ret == AVERROR(EAGAIN)) {
|
||||
return produced;
|
||||
}
|
||||
|
||||
if (ret == AVERROR_EOF) {
|
||||
dec.decoder_drained = true;
|
||||
return produced;
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!append_converted_frame(self, dec.frame)) {
|
||||
av_frame_unref(dec.frame);
|
||||
return -1;
|
||||
}
|
||||
|
||||
produced = dec.last_samples > 0 ? 1 : produced;
|
||||
av_frame_unref(dec.frame);
|
||||
}
|
||||
}
|
||||
|
||||
static bool read_selected_audio_packet(fmpg_instance *self, AVPacket *pkt)
|
||||
{
|
||||
const int wanted_stream = self->audio_info.selected_stream_index;
|
||||
|
||||
for (;;) {
|
||||
const int ret = av_read_frame(self->format_ctx, pkt);
|
||||
if (ret < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pkt->stream_index == wanted_stream) {
|
||||
return true;
|
||||
}
|
||||
|
||||
av_packet_unref(pkt);
|
||||
}
|
||||
}
|
||||
|
||||
static int drain_resampler(fmpg_instance *self)
|
||||
{
|
||||
decoder_storage &dec = self->decoder;
|
||||
const int channels = self->audio_info.channels;
|
||||
const int sample_rate = self->audio_info.sample_rate;
|
||||
int produced = 0;
|
||||
|
||||
for (;;) {
|
||||
const int delay = static_cast<int>(swr_get_delay(dec.swr_ctx,
|
||||
sample_rate));
|
||||
if (delay <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const int max_bytes = av_samples_get_buffer_size(nullptr,
|
||||
channels,
|
||||
delay,
|
||||
FMPG_OUTPUT_FMT,
|
||||
1);
|
||||
if (max_bytes <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> tmp(static_cast<size_t>(max_bytes));
|
||||
uint8_t *out_planes[1] = { tmp.data() };
|
||||
|
||||
const int out_samples = swr_convert(dec.swr_ctx,
|
||||
out_planes,
|
||||
delay,
|
||||
nullptr,
|
||||
0);
|
||||
if (out_samples <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const int used_bytes = av_samples_get_buffer_size(nullptr,
|
||||
channels,
|
||||
out_samples,
|
||||
FMPG_OUTPUT_FMT,
|
||||
1);
|
||||
if (used_bytes < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (dec.pcm.empty()) {
|
||||
dec.buffer_start_sample = dec.next_sample_position;
|
||||
dec.timecode = static_cast<double>(dec.buffer_start_sample) /
|
||||
static_cast<double>(sample_rate);
|
||||
}
|
||||
|
||||
if (!append_bytes(dec, tmp.data(), static_cast<size_t>(used_bytes))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
dec.last_samples += out_samples;
|
||||
dec.next_sample_position += out_samples;
|
||||
produced = 1;
|
||||
}
|
||||
|
||||
return produced;
|
||||
}
|
||||
|
||||
int fmpg_decode_next(fmpg_instance *instance)
|
||||
{
|
||||
if (!instance_ready(instance)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
decoder_storage &dec = instance->decoder;
|
||||
dec.clear_output();
|
||||
|
||||
/* First return any frames that are already pending in the decoder. */
|
||||
int produced = receive_available_frames(instance);
|
||||
if (produced < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (produced > 0 && !dec.pcm.empty()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
AVPacket *pkt = av_packet_alloc();
|
||||
if (!pkt) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (!dec.eof_seen) {
|
||||
if (!read_selected_audio_packet(instance, pkt)) {
|
||||
dec.eof_seen = true;
|
||||
av_packet_unref(pkt);
|
||||
break;
|
||||
}
|
||||
|
||||
int ret = avcodec_send_packet(dec.codec_ctx, pkt);
|
||||
av_packet_unref(pkt);
|
||||
|
||||
if (ret == AVERROR(EAGAIN)) {
|
||||
produced = receive_available_frames(instance);
|
||||
if (produced < 0) {
|
||||
av_packet_free(&pkt);
|
||||
return 0;
|
||||
}
|
||||
if (produced > 0 && !dec.pcm.empty()) {
|
||||
av_packet_free(&pkt);
|
||||
return 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
av_packet_free(&pkt);
|
||||
return 0;
|
||||
}
|
||||
|
||||
produced = receive_available_frames(instance);
|
||||
if (produced < 0) {
|
||||
av_packet_free(&pkt);
|
||||
return 0;
|
||||
}
|
||||
if (produced > 0 && !dec.pcm.empty()) {
|
||||
av_packet_free(&pkt);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
av_packet_free(&pkt);
|
||||
|
||||
if (!dec.decoder_drained) {
|
||||
const int ret = avcodec_send_packet(dec.codec_ctx, nullptr);
|
||||
if (ret < 0 && ret != AVERROR_EOF) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
produced = receive_available_frames(instance);
|
||||
if (produced < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (produced > 0 && !dec.pcm.empty()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
produced = drain_resampler(instance);
|
||||
return produced > 0 && !dec.pcm.empty() ? 1 : 0;
|
||||
}
|
||||
|
||||
int fmpg_seek_ms(fmpg_instance *instance, int64_t target_pos_ms)
|
||||
{
|
||||
if (!instance_ready(instance) || target_pos_ms < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int stream_index = instance->audio_info.selected_stream_index;
|
||||
AVStream *stream = instance->format_ctx->streams[stream_index];
|
||||
|
||||
const int64_t pos_us = av_rescale(target_pos_ms, AV_TIME_BASE, 1000);
|
||||
const int64_t stream_ts = av_rescale_q(pos_us,
|
||||
AV_TIME_BASE_Q,
|
||||
stream->time_base);
|
||||
|
||||
if (av_seek_frame(instance->format_ctx,
|
||||
stream_index,
|
||||
stream_ts,
|
||||
AVSEEK_FLAG_BACKWARD) < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
decoder_storage &dec = instance->decoder;
|
||||
const int64_t target_samples = samples_from_seconds(target_pos_ms / 1000.0,
|
||||
instance->audio_info.sample_rate);
|
||||
|
||||
avcodec_flush_buffers(dec.codec_ctx);
|
||||
swr_close(dec.swr_ctx);
|
||||
if (swr_init(dec.swr_ctx) < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
dec.pcm.clear();
|
||||
dec.last_samples = 0;
|
||||
dec.buffer_start_sample = target_samples >= 0 ? target_samples : 0;
|
||||
dec.next_sample_position = target_samples >= 0 ? target_samples : 0;
|
||||
dec.discard_until_sample = target_samples;
|
||||
dec.timecode = target_pos_ms / 1000.0;
|
||||
dec.eof_seen = false;
|
||||
dec.decoder_drained = false;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint8_t *fmpg_buffer(fmpg_instance *instance)
|
||||
{
|
||||
return instance && !instance->decoder.pcm.empty()
|
||||
? instance->decoder.pcm.data()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
int fmpg_buffer_size(fmpg_instance *instance)
|
||||
{
|
||||
if (!instance || instance->decoder.pcm.size() >
|
||||
static_cast<size_t>(std::numeric_limits<int>::max())) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(instance->decoder.pcm.size());
|
||||
}
|
||||
|
||||
int64_t fmpg_buffer_samples(fmpg_instance *instance)
|
||||
{
|
||||
return instance ? instance->decoder.last_samples : 0;
|
||||
}
|
||||
|
||||
int64_t fmpg_buffer_start_sample(fmpg_instance *instance)
|
||||
{
|
||||
return instance ? instance->decoder.buffer_start_sample : 0;
|
||||
}
|
||||
|
||||
int64_t fmpg_buffer_end_sample(fmpg_instance *instance)
|
||||
{
|
||||
if (!instance) {
|
||||
return 0;
|
||||
}
|
||||
return instance->decoder.buffer_start_sample + instance->decoder.last_samples;
|
||||
}
|
||||
|
||||
int64_t fmpg_sample_position(fmpg_instance *instance)
|
||||
{
|
||||
return instance ? instance->decoder.next_sample_position : 0;
|
||||
}
|
||||
|
||||
double fmpg_timecode(fmpg_instance *instance)
|
||||
{
|
||||
return instance ? instance->decoder.timecode : 0.0;
|
||||
}
|
||||
|
||||
const char *fmpg_ffmpeg_version()
|
||||
{
|
||||
static char *version = nullptr;
|
||||
|
||||
if (version == nullptr) {
|
||||
version = static_cast<char *>(malloc(1024));
|
||||
}
|
||||
sprintf(version, "avformat: %u.%u.%u (%d), avcodec: %u.%u.%u (%d), swresample: %u.%u.%u (%d), avutil: %u.%u.%u (%d)",
|
||||
LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO, LIBAVFORMAT_VERSION_INT,
|
||||
LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, LIBAVCODEC_VERSION_MICRO, LIBAVCODEC_VERSION_INT,
|
||||
LIBSWRESAMPLE_VERSION_MAJOR, LIBSWRESAMPLE_VERSION_MINOR, LIBSWRESAMPLE_VERSION_MICRO, LIBSWRESAMPLE_VERSION_INT,
|
||||
LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO, LIBAVUTIL_VERSION_INT
|
||||
);
|
||||
return version;
|
||||
}
|
||||
|
||||
const char *fmpg_int_version2string(int ver)
|
||||
{
|
||||
static char *version = nullptr;
|
||||
|
||||
if (version == nullptr) {
|
||||
version = static_cast<char *>(malloc(1024));
|
||||
}
|
||||
|
||||
int major = AV_VERSION_MAJOR(ver);
|
||||
int minor = AV_VERSION_MINOR(ver);
|
||||
int micro = AV_VERSION_MICRO(ver);
|
||||
sprintf(version, "%u.%u.%u", major, minor, micro);
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
int fmpg_version()
|
||||
{
|
||||
return ffi_version();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
Building for Win32.
|
||||
|
||||
Extract the taglib sources.
|
||||
|
||||
Add this in CMakeLists.txt:
|
||||
|
||||
set(ZLIB_LIBRARY "c:/devel/libraries/nwin64/lib/zlib.lib")
|
||||
set(ZLIB_INCLUDE_DIR "c:/devel/libraries/nwin64/include")
|
||||
set(BUILD_TESTING OFF)
|
||||
set(BUILD_SHARED_LIBS ON)
|
||||
|
||||
Or any place you have zlib.lib stored.
|
||||
|
||||
Copy the shared libraries to ../lib/windows_<arch>/
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
TAGLIB=taglib-2.2.1
|
||||
|
||||
all:
|
||||
tar xf ${TAGLIB}.tar.gz
|
||||
SUBDIR=`racket -e "(display (format \"~a-~a\" (system-type 'os*) (system-type 'arch)))"`; \
|
||||
(cd ${TAGLIB}; cmake -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=../lib/$$SUBDIR -DCMAKE_BUILD_TYPE=Release .)
|
||||
(cd ${TAGLIB}; make)
|
||||
(cd ${TAGLIB}; make install)
|
||||
SUBDIR=`racket -e "(display (format \"~a-~a\" (system-type 'os*) (system-type 'arch)))"`; \
|
||||
(cd lib/$$SUBDIR/lib; tar cf - *so *.so.*) | (cd ../lib/$$SUBDIR; tar xvf - )
|
||||
|
||||
clean:
|
||||
rm -rf ${TAGLIB}
|
||||
rm -rf lib
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Symlink
+1
@@ -0,0 +1 @@
|
||||
libtag.so.2
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
libtag.so.2.2.1
|
||||
Binary file not shown.
Symlink
+1
@@ -0,0 +1 @@
|
||||
libtag_c.so.2
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
libtag_c.so.2.2.1
|
||||
Binary file not shown.
Reference in New Issue
Block a user