825 lines
22 KiB
C++
825 lines
22 KiB
C++
/*
|
|
* Audio-only FFmpeg wrapper.
|
|
*
|
|
* This file is implemented in C++, but exports a plain C ABI. C++ is used only
|
|
* internally to make ownership understandable: strings are std::string, decoded
|
|
* PCM buffers are std::vector, and FFmpeg objects are released by destructors.
|
|
*
|
|
* Public design choices:
|
|
*
|
|
* - The caller opens one file.
|
|
* - The best audio stream is selected internally.
|
|
* - FFmpeg stream_index is not exposed.
|
|
* - File metadata is stored in the instance and accessed through getters.
|
|
* - Audio output is always signed 32-bit interleaved PCM.
|
|
* - There are no callbacks; file IO is handled by FFmpeg.
|
|
*/
|
|
|
|
#include "ffmpeg_audio.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <string>
|
|
#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>
|
|
}
|
|
|
|
static constexpr int AC_AUDIO_OUTPUT_BITS = 32;
|
|
static constexpr int AC_AUDIO_OUTPUT_BYTES = 4;
|
|
static constexpr AVSampleFormat AC_AUDIO_OUTPUT_FMT = AV_SAMPLE_FMT_S32;
|
|
|
|
/* Metadata stored inside fmpg_instance. */
|
|
struct file_info_storage {
|
|
std::string title;
|
|
std::string author;
|
|
std::string album;
|
|
std::string genre;
|
|
std::string comment;
|
|
std::string copyright;
|
|
|
|
int year = -1;
|
|
int track = -1;
|
|
int bitrate = -1;
|
|
|
|
void clear() {
|
|
title.clear();
|
|
author.clear();
|
|
album.clear();
|
|
genre.clear();
|
|
comment.clear();
|
|
copyright.clear();
|
|
year = -1;
|
|
track = -1;
|
|
bitrate = -1;
|
|
}
|
|
};
|
|
|
|
/* Audio information for the selected audio stream. */
|
|
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; /* Sample frames, not int32_t values. */
|
|
|
|
void clear() {
|
|
audio_stream_count = 0;
|
|
selected_stream_index = -1;
|
|
sample_rate = 0;
|
|
channels = 0;
|
|
duration_ms = -1;
|
|
duration_samples = -1;
|
|
}
|
|
};
|
|
|
|
struct __fmpg_instance__ {
|
|
bool opened = false;
|
|
AVFormatContext *format_ctx = nullptr;
|
|
file_info_storage file_info;
|
|
audio_info_storage audio_info;
|
|
|
|
~__fmpg_instance__() {
|
|
if (format_ctx) {
|
|
avformat_close_input(&format_ctx);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct __fmpg_package__ {
|
|
int64_t pts = AV_NOPTS_VALUE;
|
|
AVPacket *packet = nullptr;
|
|
|
|
__fmpg_package__() : packet(av_packet_alloc()) {}
|
|
|
|
~__fmpg_package__() {
|
|
av_packet_free(&packet);
|
|
}
|
|
};
|
|
|
|
struct __fmpg_decoder__ {
|
|
fmpg_instance *instance = nullptr;
|
|
|
|
const AVCodec *codec = nullptr;
|
|
AVCodecContext *codec_ctx = nullptr;
|
|
AVFrame *frame = nullptr;
|
|
SwrContext *swr_ctx = nullptr;
|
|
|
|
std::vector<uint8_t> pcm;
|
|
|
|
double timecode = 0.0;
|
|
int64_t last_samples = 0; /* sample frames in current output block */
|
|
int64_t sample_position = 0; /* total sample frames emitted */
|
|
|
|
~__fmpg_decoder__() {
|
|
avcodec_free_context(&codec_ctx);
|
|
av_frame_free(&frame);
|
|
swr_free(&swr_ctx);
|
|
}
|
|
};
|
|
|
|
static const char *string_c_str(const std::string &s)
|
|
{
|
|
return s.empty() ? "" : s.c_str();
|
|
}
|
|
|
|
static std::string get_metadata_string(const AVFormatContext *ctx, const char *key)
|
|
{
|
|
const AVDictionaryEntry *entry = av_dict_get(ctx->metadata,
|
|
key,
|
|
nullptr,
|
|
0);
|
|
|
|
return entry && entry->value ? std::string(entry->value)
|
|
: std::string();
|
|
}
|
|
|
|
static int get_metadata_int(const AVFormatContext *ctx, const char *key)
|
|
{
|
|
const AVDictionaryEntry *entry = av_dict_get(ctx->metadata,
|
|
key,
|
|
nullptr,
|
|
0);
|
|
|
|
if (!entry || !entry->value || !*entry->value) {
|
|
return -1;
|
|
}
|
|
|
|
return std::atoi(entry->value);
|
|
}
|
|
|
|
static int count_audio_streams(const AVFormatContext *ctx)
|
|
{
|
|
int count = 0;
|
|
|
|
if (!ctx) {
|
|
return 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 void fill_file_metadata(fmpg_instance *self)
|
|
{
|
|
AVFormatContext *ctx = self->format_ctx;
|
|
|
|
self->file_info.clear();
|
|
self->file_info.title = get_metadata_string(ctx, "title");
|
|
self->file_info.author = get_metadata_string(ctx, "artist");
|
|
self->file_info.album = get_metadata_string(ctx, "album");
|
|
self->file_info.genre = get_metadata_string(ctx, "genre");
|
|
self->file_info.comment = get_metadata_string(ctx, "comment");
|
|
self->file_info.copyright = get_metadata_string(ctx, "copyright");
|
|
self->file_info.year = get_metadata_int(ctx, "year");
|
|
self->file_info.track = get_metadata_int(ctx, "track");
|
|
self->file_info.bitrate = ctx->bit_rate > 0
|
|
? static_cast<int>(ctx->bit_rate)
|
|
: -1;
|
|
}
|
|
|
|
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;
|
|
|
|
/*
|
|
* Duration can come from the selected audio stream or from the container.
|
|
* Stream duration is preferred because it is tied to the audio stream's own
|
|
* time base. Some containers only provide container-level duration, so that
|
|
* is the fallback.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
fmpg_instance *fmpg_init(void) {
|
|
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 (avformat_open_input(&instance->format_ctx,
|
|
filename,
|
|
nullptr,
|
|
nullptr) < 0) {
|
|
fmpg_close(instance);
|
|
return 0;
|
|
}
|
|
|
|
if (avformat_find_stream_info(instance->format_ctx, nullptr) < 0) {
|
|
fmpg_close(instance);
|
|
return 0;
|
|
}
|
|
|
|
fill_file_metadata(instance);
|
|
|
|
if (!fill_audio_info(instance)) {
|
|
fmpg_close(instance);
|
|
return 0;
|
|
}
|
|
|
|
instance->opened = true;
|
|
return 1;
|
|
}
|
|
|
|
void fmpg_close(fmpg_instance *instance)
|
|
{
|
|
if (!instance) {
|
|
return;
|
|
}
|
|
|
|
if (instance->format_ctx) {
|
|
avformat_close_input(&instance->format_ctx);
|
|
}
|
|
|
|
instance->opened = false;
|
|
instance->file_info.clear();
|
|
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 AC_AUDIO_OUTPUT_BITS;
|
|
}
|
|
|
|
int fmpg_audio_bytes_per_sample(fmpg_instance *)
|
|
{
|
|
return AC_AUDIO_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;
|
|
}
|
|
|
|
const char *fmpg_file_title(fmpg_instance *instance)
|
|
{
|
|
return instance ? string_c_str(instance->file_info.title) : "";
|
|
}
|
|
|
|
const char *fmpg_file_author(fmpg_instance *instance)
|
|
{
|
|
return instance ? string_c_str(instance->file_info.author) : "";
|
|
}
|
|
|
|
const char *fmpg_file_album(fmpg_instance *instance) {
|
|
return instance ? string_c_str(instance->file_info.album) : "";
|
|
}
|
|
|
|
const char *fmpg_file_genre(fmpg_instance *instance)
|
|
{
|
|
return instance ? string_c_str(instance->file_info.genre) : "";
|
|
}
|
|
|
|
const char *fmpg_file_comment(fmpg_instance *instance)
|
|
{
|
|
return instance ? string_c_str(instance->file_info.comment) : "";
|
|
}
|
|
|
|
const char *fmpg_file_copyright(fmpg_instance *instance)
|
|
{
|
|
return instance ? string_c_str(instance->file_info.copyright) : "";
|
|
}
|
|
|
|
int fmpg_file_year(fmpg_instance *instance) {
|
|
return instance ? instance->file_info.year : -1;
|
|
}
|
|
|
|
int fmpg_file_track(fmpg_instance *instance)
|
|
{
|
|
return instance ? instance->file_info.track : -1;
|
|
}
|
|
|
|
int fmpg_file_bitrate(fmpg_instance *instance)
|
|
{
|
|
return instance ? instance->file_info.bitrate : -1;
|
|
}
|
|
|
|
fmpg_package *fmpg_read_package(fmpg_instance *instance)
|
|
{
|
|
if (!instance_ready(instance)) {
|
|
return nullptr;
|
|
}
|
|
|
|
const int wanted_stream = instance->audio_info.selected_stream_index;
|
|
|
|
for (;;) {
|
|
fmpg_package *pkg = nullptr;
|
|
|
|
try {
|
|
pkg = new fmpg_package();
|
|
} catch (...) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!pkg->packet) {
|
|
delete pkg;
|
|
return nullptr;
|
|
}
|
|
|
|
const int ret = av_read_frame(instance->format_ctx, pkg->packet);
|
|
if (ret < 0) {
|
|
delete pkg;
|
|
return nullptr;
|
|
}
|
|
|
|
if (pkg->packet->stream_index != wanted_stream) {
|
|
delete pkg;
|
|
continue;
|
|
}
|
|
|
|
pkg->pts = pkg->packet->dts != AV_NOPTS_VALUE
|
|
? pkg->packet->dts
|
|
: pkg->packet->pts;
|
|
return pkg;
|
|
}
|
|
}
|
|
|
|
void fmpg_free_package(fmpg_package *package)
|
|
{
|
|
delete package;
|
|
}
|
|
|
|
static bool init_codec_context(fmpg_decoder *dec, const AVCodecParameters *par)
|
|
{
|
|
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;
|
|
}
|
|
|
|
return avcodec_open2(dec->codec_ctx, dec->codec, nullptr) >= 0;
|
|
}
|
|
|
|
static bool init_resampler(fmpg_decoder *dec)
|
|
{
|
|
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,
|
|
AC_AUDIO_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;
|
|
}
|
|
|
|
fmpg_decoder *fmpg_create_decoder(fmpg_instance *instance)
|
|
{
|
|
if (!instance_ready(instance)) {
|
|
return nullptr;
|
|
}
|
|
|
|
fmpg_decoder *dec = nullptr;
|
|
|
|
try {
|
|
dec = new fmpg_decoder();
|
|
} catch (...) {
|
|
return nullptr;
|
|
}
|
|
|
|
dec->instance = instance;
|
|
|
|
const int stream_index = instance->audio_info.selected_stream_index;
|
|
const AVCodecParameters *par = instance->format_ctx->streams[stream_index]->codecpar;
|
|
|
|
if (!init_codec_context(dec, par)) {
|
|
delete dec;
|
|
return nullptr;
|
|
}
|
|
|
|
dec->frame = av_frame_alloc();
|
|
if (!dec->frame) {
|
|
delete dec;
|
|
return nullptr;
|
|
}
|
|
|
|
if (!init_resampler(dec)) {
|
|
delete dec;
|
|
return nullptr;
|
|
}
|
|
|
|
return dec;
|
|
}
|
|
|
|
void fmpg_free_decoder(fmpg_decoder *decoder)
|
|
{
|
|
delete decoder;
|
|
}
|
|
|
|
static bool append_bytes(fmpg_decoder *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_decoder *dec, const AVFrame *frame)
|
|
{
|
|
const int channels = dec->codec_ctx->ch_layout.nb_channels;
|
|
|
|
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,
|
|
AC_AUDIO_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,
|
|
AC_AUDIO_OUTPUT_FMT,
|
|
1);
|
|
if (used_bytes < 0) {
|
|
return false;
|
|
}
|
|
|
|
if (!append_bytes(dec, tmp.data(), static_cast<size_t>(used_bytes))) {
|
|
return false;
|
|
}
|
|
|
|
dec->last_samples += out_samples;
|
|
dec->sample_position += out_samples;
|
|
return true;
|
|
}
|
|
|
|
static int receive_available_frames(fmpg_decoder *dec)
|
|
{
|
|
int produced = 0;
|
|
|
|
for (;;) {
|
|
const int ret = avcodec_receive_frame(dec->codec_ctx, dec->frame);
|
|
|
|
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
|
return produced;
|
|
}
|
|
|
|
if (ret < 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (!append_converted_frame(dec, dec->frame)) {
|
|
av_frame_unref(dec->frame);
|
|
return -1;
|
|
}
|
|
|
|
produced = 1;
|
|
av_frame_unref(dec->frame);
|
|
}
|
|
}
|
|
|
|
static void update_timecode_from_packet(fmpg_decoder *dec, const fmpg_package *pkg)
|
|
{
|
|
if (!dec || !pkg || pkg->pts == AV_NOPTS_VALUE) {
|
|
return;
|
|
}
|
|
|
|
const int stream_index = dec->instance->audio_info.selected_stream_index;
|
|
AVStream *stream = dec->instance->format_ctx->streams[stream_index];
|
|
dec->timecode = pkg->pts * av_q2d(stream->time_base);
|
|
}
|
|
|
|
int fmpg_decode_package(fmpg_package *package, fmpg_decoder *decoder)
|
|
{
|
|
if (!package || !decoder || !package->packet) {
|
|
return 0;
|
|
}
|
|
|
|
decoder->pcm.clear();
|
|
decoder->last_samples = 0;
|
|
update_timecode_from_packet(decoder, package);
|
|
|
|
int ret = avcodec_send_packet(decoder->codec_ctx, package->packet);
|
|
|
|
if (ret == AVERROR(EAGAIN)) {
|
|
if (receive_available_frames(decoder) < 0) {
|
|
return 0;
|
|
}
|
|
ret = avcodec_send_packet(decoder->codec_ctx, package->packet);
|
|
}
|
|
|
|
if (ret < 0) {
|
|
return 0;
|
|
}
|
|
|
|
return receive_available_frames(decoder) > 0 ? 1 : 0;
|
|
}
|
|
|
|
int fmpg_flush_decoder(fmpg_decoder *decoder)
|
|
{
|
|
if (!decoder) {
|
|
return 0;
|
|
}
|
|
|
|
decoder->pcm.clear();
|
|
decoder->last_samples = 0;
|
|
|
|
const int ret = avcodec_send_packet(decoder->codec_ctx, nullptr);
|
|
if (ret < 0 && ret != AVERROR_EOF) {
|
|
return 0;
|
|
}
|
|
|
|
const int produced = receive_available_frames(decoder);
|
|
if (produced < 0) {
|
|
return 0;
|
|
}
|
|
|
|
const int channels = decoder->codec_ctx->ch_layout.nb_channels;
|
|
|
|
for (;;) {
|
|
const int delay = static_cast<int>(swr_get_delay(decoder->swr_ctx,
|
|
decoder->codec_ctx->sample_rate));
|
|
if (delay <= 0) {
|
|
break;
|
|
}
|
|
|
|
const int max_bytes = av_samples_get_buffer_size(nullptr,
|
|
channels,
|
|
delay,
|
|
AC_AUDIO_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(decoder->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,
|
|
AC_AUDIO_OUTPUT_FMT,
|
|
1);
|
|
if (used_bytes < 0 ||
|
|
!append_bytes(decoder,
|
|
tmp.data(),
|
|
static_cast<size_t>(used_bytes))) {
|
|
break;
|
|
}
|
|
|
|
decoder->last_samples += out_samples;
|
|
decoder->sample_position += out_samples;
|
|
}
|
|
|
|
return decoder->pcm.empty() ? 0 : 1;
|
|
}
|
|
|
|
int fmpg_seek_ms(fmpg_decoder *decoder, int64_t target_pos_ms)
|
|
{
|
|
if (!decoder || !instance_ready(decoder->instance)) {
|
|
return 0;
|
|
}
|
|
|
|
const int stream_index = decoder->instance->audio_info.selected_stream_index;
|
|
AVStream *stream = decoder->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(decoder->instance->format_ctx,
|
|
stream_index,
|
|
stream_ts,
|
|
AVSEEK_FLAG_BACKWARD) < 0) {
|
|
return 0;
|
|
}
|
|
|
|
decoder->timecode = target_pos_ms / 1000.0;
|
|
decoder->pcm.clear();
|
|
decoder->last_samples = 0;
|
|
decoder->sample_position = samples_from_seconds(decoder->timecode,
|
|
decoder->instance->audio_info.sample_rate);
|
|
|
|
avcodec_flush_buffers(decoder->codec_ctx);
|
|
|
|
swr_close(decoder->swr_ctx);
|
|
return swr_init(decoder->swr_ctx) >= 0 ? 1 : 0;
|
|
}
|
|
|
|
const uint8_t *fmpg_decoder_buffer(fmpg_decoder *decoder)
|
|
{
|
|
return decoder && !decoder->pcm.empty() ? decoder->pcm.data() : nullptr;
|
|
}
|
|
|
|
int fmpg_decoder_buffer_size(fmpg_decoder *decoder) {
|
|
if (!decoder || decoder->pcm.size() >
|
|
static_cast<size_t>(std::numeric_limits<int>::max())) {
|
|
return 0;
|
|
}
|
|
|
|
return static_cast<int>(decoder->pcm.size());
|
|
}
|
|
|
|
double fmpg_decoder_timecode(fmpg_decoder *decoder)
|
|
{
|
|
return decoder ? decoder->timecode : 0.0;
|
|
}
|
|
|
|
int64_t fmpg_decoder_last_samples(fmpg_decoder *decoder)
|
|
{
|
|
return decoder ? decoder->last_samples : 0;
|
|
}
|
|
|
|
int64_t fmpg_decoder_sample_position(fmpg_decoder *decoder)
|
|
{
|
|
return decoder ? decoder->sample_position : 0;
|
|
}
|