/* * 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 #include #include #include #include #include #include extern "C" { #include #include #include #include #include #include } 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 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(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(seconds * static_cast(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(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(ctx->duration) / static_cast(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(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(std::numeric_limits::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 tmp(static_cast(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(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(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(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 tmp(static_cast(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(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(std::numeric_limits::max())) { return 0; } return static_cast(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; }