From 985bfed7aaf5fe576fe9b02b1e8696afe248b448 Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:07:50 +0300 Subject: [PATCH 1/8] Do not use POS=-1 for the dummy gap packets `AVPacket.pos=-1` can still contain real audio. We need to separate these cases by using `POS=-2` for the dummy gap packets. Change the pre-roll behavior: - a real previous frame with `POS=-1` can still be used for pre-roll; - a dummy gap with `POS=-2` is not used; - the decoder is not forced to start across a dummy gap. --- AviSynth/lwlibav_source.cpp | 2 +- VapourSynth/lwlibav_audio_source.c | 4 ++-- common/audio_output.h | 12 ++++++++++++ common/lwindex.c | 21 +++++++++++---------- common/lwindex.h | 2 +- common/lwlibav_audio.c | 17 ++++++++--------- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/AviSynth/lwlibav_source.cpp b/AviSynth/lwlibav_source.cpp index 9dd0f06..64ce399 100644 --- a/AviSynth/lwlibav_source.cpp +++ b/AviSynth/lwlibav_source.cpp @@ -324,7 +324,7 @@ LWLibavAudioSource::LWLibavAudioSource(lwlibav_option_t* opt, const char* channe sequence_pcm_count += info[i].length; const uint64_t frame_end = prior_sequences_resampled_count + count_sequence_output_pcm_samples(sequence_pcm_count, current_sample_rate, aohp->output_sample_rate); - if (info[i].file_offset == -1) + if (lw_audio_is_gap_offset(info[i].file_offset)) gap_info_list.emplace_back(static_cast(frame_start) + lwh.av_gap, static_cast(frame_end - frame_start)); } const int gap_count = gap_info_list.size(); diff --git a/VapourSynth/lwlibav_audio_source.c b/VapourSynth/lwlibav_audio_source.c index 23bd1d3..ebaaa5e 100644 --- a/VapourSynth/lwlibav_audio_source.c +++ b/VapourSynth/lwlibav_audio_source.c @@ -91,7 +91,7 @@ static int make_gap_list(lwlibav_audio_handler_t* hp, VSMap* out, const VSAPI* v const audio_frame_info_t* info = adhp->frame_list; int gap_count = 0; for (uint32_t i = 1; i <= adhp->frame_count; i++) { - if (info[i].file_offset == -1) + if (lw_audio_is_gap_offset(info[i].file_offset)) gap_count++; } if (!gap_count) @@ -114,7 +114,7 @@ static int make_gap_list(lwlibav_audio_handler_t* hp, VSMap* out, const VSAPI* v current_sample_rate = info[i].sample_rate > 0 ? info[i].sample_rate : hp->adhp->ctx->sample_rate; current_frame_length = info[i].length; } - if (info[i].file_offset == -1) { + if (lw_audio_is_gap_offset(info[i].file_offset)) { int64_t gap_start = prior_sequence_sample_count + av_rescale_rnd(sequence_sample_count, hp->aohp->output_sample_rate, current_sample_rate, AV_ROUND_UP); int64_t gap_end = prior_sequence_sample_count diff --git a/common/audio_output.h b/common/audio_output.h index 0b9a73f..85a49fc 100644 --- a/common/audio_output.h +++ b/common/audio_output.h @@ -73,4 +73,16 @@ uint64_t output_pcm_samples_from_packet(lw_audio_output_handler_t* aohp, AVCodec void lw_cleanup_audio_output_handler(lw_audio_output_handler_t* aohp); +#define LW_AUDIO_GAP_FILE_OFFSET (-2) + +static inline int lw_audio_is_gap_offset(int64_t file_offset) +{ + return file_offset == LW_AUDIO_GAP_FILE_OFFSET; +} + +static inline int lw_audio_has_valid_file_offset(int64_t file_offset) +{ + return file_offset >= 0; +} + #endif // AUDIO_OUTPUT_H diff --git a/common/lwindex.c b/common/lwindex.c index 1850bb7..2fbf359 100644 --- a/common/lwindex.c +++ b/common/lwindex.c @@ -554,7 +554,7 @@ static void decide_audio_seek_method(lwlibav_file_handler_t* lwhp, lwlibav_audio else { uint32_t error_count = 0; for (uint32_t i = 1; i <= sample_count; i++) - error_count += (info[i].file_offset == -1); + error_count += !lw_audio_has_valid_file_offset(info[i].file_offset); if (error_count == sample_count) adhp->lw_seek_flags &= ~SEEK_POS_BASED; } @@ -564,35 +564,36 @@ static void decide_audio_seek_method(lwlibav_file_handler_t* lwhp, lwlibav_audio info[i].pts = info[i].dts; /* Treat audio frames with unique value as a keyframe. */ if (adhp->lw_seek_flags & SEEK_POS_BASED) { - info[1].keyframe = (info[1].file_offset != -1); + info[1].keyframe = lw_audio_has_valid_file_offset(info[1].file_offset); for (uint32_t i = 2; i <= sample_count; i++) - if (info[i].file_offset == -1) + if (!lw_audio_has_valid_file_offset(info[i].file_offset)) info[i].keyframe = 0; else if (info[i].file_offset == info[i - 1].file_offset) info[i].keyframe = info[i - 1].keyframe = 0; else info[i].keyframe = 1; } else if (adhp->lw_seek_flags & SEEK_PTS_BASED) { - info[1].keyframe = (info[1].pts != AV_NOPTS_VALUE); + info[1].keyframe = (info[1].pts != AV_NOPTS_VALUE && !lw_audio_is_gap_offset(info[1].file_offset)); for (uint32_t i = 2; i <= sample_count; i++) - if (info[i].pts == AV_NOPTS_VALUE) + if (info[i].pts == AV_NOPTS_VALUE || lw_audio_is_gap_offset(info[i].file_offset)) info[i].keyframe = 0; else if (info[i].pts == info[i - 1].pts) info[i].keyframe = info[i - 1].keyframe = 0; else info[i].keyframe = 1; } else if (adhp->lw_seek_flags & SEEK_DTS_BASED) { - info[1].keyframe = (info[1].dts != AV_NOPTS_VALUE); + info[1].keyframe = (info[1].dts != AV_NOPTS_VALUE && !lw_audio_is_gap_offset(info[1].file_offset)); for (uint32_t i = 2; i <= sample_count; i++) - if (info[i].dts == AV_NOPTS_VALUE) + if (info[i].dts == AV_NOPTS_VALUE || lw_audio_is_gap_offset(info[i].file_offset)) info[i].keyframe = 0; else if (info[i].dts == info[i - 1].dts) info[i].keyframe = info[i - 1].keyframe = 0; else info[i].keyframe = 1; } else - for (uint32_t i = 1; i <= sample_count; i++) - info[i].keyframe = 1; + for (uint32_t i = 1; i <= sample_count; i++) { + info[i].keyframe = !lw_audio_is_gap_offset(info[i].file_offset); + } } static int64_t calculate_av_gap( @@ -2031,7 +2032,7 @@ static int create_index(lwlibav_file_handler_t* lwhp, lwlibav_video_decode_handl = (int)av_rescale_q(time_diff, adhp->time_base, (AVRational) { 1, aohp->output_sample_rate }); info->pts = prev_end; info->dts = prev_info->dts + prev_duration; - info->file_offset = -1; + info->file_offset = LW_AUDIO_GAP_FILE_OFFSET; print_index(index, "Index=%d,POS=%" PRId64 ",PTS=%" PRId64 ",DTS=%" PRId64 ",EDI=%d\n" "Length=%d\n", diff --git a/common/lwindex.h b/common/lwindex.h index 8fbdd31..0cc8b98 100644 --- a/common/lwindex.h +++ b/common/lwindex.h @@ -45,7 +45,7 @@ extern "C" { /* index file version * This version is bumped when its structure changed so that the lwindex invokes * reindexing opened file immediately. */ -#define LWINDEX_INDEX_FILE_VERSION 19 +#define LWINDEX_INDEX_FILE_VERSION 20 typedef struct { const char* file_path; diff --git a/common/lwlibav_audio.c b/common/lwlibav_audio.c index f4b0d17..4c0f589 100644 --- a/common/lwlibav_audio.c +++ b/common/lwlibav_audio.c @@ -226,15 +226,11 @@ static int find_start_audio_frame( * Synthetic gap entries have no packet and must not be used as pre-roll. */ enum AVCodecID codec_id = adhp->exh.entries[frame_list[frame_number].extradata_index].codec_id; const AVCodecDescriptor* desc = avcodec_descriptor_get(codec_id); - if (desc->props & AV_CODEC_PROP_LOSSY) { - uint32_t pre_roll_frame = frame_number - 1; - while (pre_roll_frame > 0 && frame_list[pre_roll_frame].file_offset == -1) - --pre_roll_frame; - if (pre_roll_frame > 0 - && frame_list[frame_number].extradata_index == frame_list[pre_roll_frame].extradata_index) { - *start_offset += (uint64_t)frame_list[pre_roll_frame].length; - frame_number = pre_roll_frame; - } + uint32_t pre_roll_frame = frame_number - 1; + if ((desc->props & AV_CODEC_PROP_LOSSY) && !lw_audio_is_gap_offset(frame_list[pre_roll_frame].file_offset) + && frame_list[frame_number].extradata_index == frame_list[pre_roll_frame].extradata_index) { + *start_offset += (uint64_t)frame_list[pre_roll_frame].length; + frame_number = pre_roll_frame; } } return frame_number; @@ -354,6 +350,9 @@ retry_seek:; if (adhp->lw_seek_flags & SEEK_POS_BASED) { if (pkt->pos == -1 || adhp->frame_list[i].file_offset == -1) continue; + /* Dummy gap entries have file_offset == LW_AUDIO_GAP_FILE_OFFSET. + * They are intentionally not skipped here. shift_current_frame_number_pos() + * will move past them while matching the current packet's valid file offset. */ i = shift_current_frame_number_pos(adhp->frame_list, pkt, i, frame_number); } else if (adhp->lw_seek_flags & SEEK_PTS_BASED) { if (pkt->pts == AV_NOPTS_VALUE) From bb9e0a1765893b58acaf6993c56f831868195289 Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:16:31 +0300 Subject: [PATCH 2/8] lwlibav_audio: fix position-based seeking --- common/lwlibav_audio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lwlibav_audio.c b/common/lwlibav_audio.c index 4c0f589..140e0ef 100644 --- a/common/lwlibav_audio.c +++ b/common/lwlibav_audio.c @@ -265,7 +265,7 @@ static uint32_t shift_current_frame_number_pos(audio_frame_info_t* info, AVPacke if (i > goal) return 0; } else { - while ((pkt->dts != info[--i].file_offset) && i) + while ((pkt->pos != info[--i].file_offset) && i) ; if (i == 0) return 0; From 06067334d08447a814cf075ad99ffdd53a36be0e Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:05:54 +0300 Subject: [PATCH 3/8] LSMASHAudioSource: overhaul the gappless processing - Convert the priming skip directly into `start_output_samples`. - Use explicit rounding modes. - Replace `sscanf` with more robust parser. - Make the gappless shared between AviSynth and VapourSynth. - Add overflow guards. - Added `skip_tail` parameter. 1. Use SMPB `duration_samples` (if sane and `skip_priming` is active). 2. Use SMPB `padding_samples` (if sane and duration wasn't used). 3. No trim (return `base_output_samples`). --- AviSynth/README.md | 9 +- AviSynth/libavsmash_source.cpp | 107 ++------ AviSynth/libavsmash_source.h | 2 +- AviSynth/lsmashsource.cpp | 2 +- VapourSynth/libavsmash_audio_source.c | 98 ++----- VapourSynth/lsmashsource.c | 2 +- common/libavsmash_audio.c | 365 +++++++++++++++++++++++++- common/libavsmash_audio.h | 30 ++- 8 files changed, 432 insertions(+), 183 deletions(-) diff --git a/AviSynth/README.md b/AviSynth/README.md index 581864d..da47be8 100644 --- a/AviSynth/README.md +++ b/AviSynth/README.md @@ -158,7 +158,7 @@ ###### LSMASHAudioSource * `LSMASHAudioSource(string source, int track = 0, bool skip_priming = true, string layout = "", int rate = 0, - string decoder = "", int ff_loglevel = 0, float drc_scale = 1.0, string ff_options = "")` + string decoder = "", int ff_loglevel = 0, float drc_scale = 1.0, string ff_options = "", bool skip_tail = false)` * This function uses libavcodec as audio decoder and L-SMASH as demuxer. [Arguments] @@ -169,7 +169,7 @@ The value 0 means trying to get the first detected audio stream. + skip_priming (default : true) Whether skip priming samples or not. - Priming samples is detected from iTunSMPB or the first non-empty edit. + Priming samples are detected from iTunSMPB or the first non-empty edit. If any priming samples, do pre-roll whenever any seek of audio stream occurs. + layout (default : "") Output audio channel layout. @@ -257,8 +257,11 @@ 0.0 < drc_scale <= 1.0 : DRC enabled. Applies a fraction of the stream DRC value. Audio reproduction is between full range and full compression. > 1.0 : DRC enabled. Applies drc_scale asymmetrically. Loud sounds are fully compressed. Soft sounds are enhanced. If `ff_options="drc_scale=x"` is used, `drc_scale` is ignored. - + ff_options (default: "") + + ff_options (default : "") Same as 'ff_options' of LSMASHVideoSource(). + * skip_tail (default : false) + Whether skip trailing samples or not. + Trailing samples are detected from iTunSMPB. ###### LWLibavVideoSource diff --git a/AviSynth/libavsmash_source.cpp b/AviSynth/libavsmash_source.cpp index 428e298..23c7555 100644 --- a/AviSynth/libavsmash_source.cpp +++ b/AviSynth/libavsmash_source.cpp @@ -252,32 +252,6 @@ uint32_t LSMASHAudioSource::open_file(const char* source, IScriptEnvironment* en return movie_param.number_of_tracks; } -static int64_t get_start_time(lsmash_root_t* root, uint32_t track_ID) -{ - /* Consider start time of this media if any non-empty edit is present. */ - uint32_t edit_count = lsmash_count_explicit_timeline_map(root, track_ID); - for (uint32_t edit_number = 1; edit_number <= edit_count; edit_number++) { - lsmash_edit_t edit; - if (lsmash_get_explicit_timeline_map(root, track_ID, edit_number, &edit)) - return 0; - if (edit.duration == 0) - return 0; /* no edits */ - if (edit.start_time >= 0) - return edit.start_time; - } - return 0; -} - -static char* duplicate_as_string(void* src, size_t length) -{ - char* dst = new char[length + 1]; - if (!dst) - return nullptr; - memcpy(dst, src, length); - dst[length] = '\0'; - return dst; -} - void LSMASHAudioSource::get_audio_track(const char* source, uint32_t track_number, IScriptEnvironment* env) { libavsmash_audio_decode_handler_t* adhp = this->adhp.get(); @@ -289,70 +263,24 @@ void LSMASHAudioSource::get_audio_track(const char* source, uint32_t track_numbe } static void count_output_audio_samples(libavsmash_audio_decode_handler_t* adhp, libavsmash_audio_output_handler_t* aohp, bool skip_priming, - VideoInfo& vi, IScriptEnvironment* env) + bool skip_tail, VideoInfo& vi, IScriptEnvironment* env) { - lsmash_root_t* root = libavsmash_audio_get_root(adhp); - uint32_t track_id = libavsmash_audio_get_track_id(adhp); - uint64_t start_time = 0; - if (skip_priming) { - uint32_t media_timescale = libavsmash_audio_get_media_timescale(adhp); - uint32_t itunes_metadata_count = lsmash_count_itunes_metadata(root); - for (uint32_t i = 1; i <= itunes_metadata_count; i++) { - lsmash_itunes_metadata_t metadata; - if (lsmash_get_itunes_metadata(root, i, &metadata) < 0) - continue; - if (metadata.item != ITUNES_METADATA_ITEM_CUSTOM - || (metadata.type != ITUNES_METADATA_TYPE_STRING && metadata.type != ITUNES_METADATA_TYPE_BINARY) || !metadata.meaning - || !metadata.name || memcmp("com.apple.iTunes", metadata.meaning, strlen(metadata.meaning)) - || memcmp("iTunSMPB", metadata.name, strlen(metadata.name))) { - lsmash_cleanup_itunes_metadata(&metadata); - continue; - } - char* value = nullptr; - if (metadata.type == ITUNES_METADATA_TYPE_STRING) { - size_t length = strlen(metadata.value.string); - if (length >= 116) - value = duplicate_as_string(metadata.value.string, length); - } else /* metadata.type == ITUNES_METADATA_TYPE_BINARY */ - { - if (metadata.value.binary.size >= 116) - value = duplicate_as_string(metadata.value.binary.data, metadata.value.binary.size); - } - lsmash_cleanup_itunes_metadata(&metadata); - if (!value) - continue; - uint32_t dummy[9]; - uint32_t priming_samples; - uint32_t padding; - uint64_t duration; - if (12 - != sscanf(value, " %x %x %x %" SCNx64 " %x %x %x %x %x %x %x %x", &dummy[0], &priming_samples, &padding, &duration, - &dummy[1], &dummy[2], &dummy[3], &dummy[4], &dummy[5], &dummy[6], &dummy[7], &dummy[8])) { - delete[] value; - continue; - } - delete[] value; - libavsmash_audio_set_implicit_preroll(adhp); - start_time = av_rescale(priming_samples, media_timescale, aohp->output_sample_rate); - aohp->skip_decoded_samples = priming_samples; - // vi.num_audio_samples = duration + priming_samples; - break; - } - if (aohp->skip_decoded_samples == 0) { - uint32_t ctd_shift; - if (lsmash_get_composition_to_decode_shift_from_media_timeline(root, track_id, &ctd_shift)) - env->ThrowError("LSMASHAudioSource: failed to get the timeline shift."); - start_time = ctd_shift + get_start_time(root, track_id); - aohp->skip_decoded_samples = av_rescale(start_time, aohp->output_sample_rate, media_timescale); - } + uint64_t final_num_samples = 0; + char error_msg[256] = { 0 }; + if (libavsmash_audio_setup_sample_count( + adhp, aohp, skip_priming ? 1 : 0, skip_tail ? 1 : 0, &final_num_samples, error_msg, sizeof(error_msg)) + < 0) { + env->ThrowError("LSMASHAudioSource: %s", error_msg); + } + if (final_num_samples > (uint64_t)INT64_MAX) { + env->ThrowError("LSMASHAudioSource: audio sample count exceeds INT64_MAX."); } - vi.num_audio_samples = libavsmash_audio_count_overall_pcm_samples(adhp, aohp->output_sample_rate, start_time); - if (vi.num_audio_samples == 0) - env->ThrowError("LSMASHAudioSource: no valid audio frame."); + vi.num_audio_samples = (int64_t)final_num_samples; } static void prepare_audio_decoding(libavsmash_audio_decode_handler_t* adhp, libavsmash_audio_output_handler_t* aohp, - AVFormatContext* format_ctx, const char* channel_layout, int sample_rate, bool skip_priming, VideoInfo& vi, IScriptEnvironment* env) + AVFormatContext* format_ctx, const char* channel_layout, int sample_rate, bool skip_priming, bool skip_tail, VideoInfo& vi, + IScriptEnvironment* env) { /* Initialize the audio decoder configuration. */ if (libavsmash_audio_initialize_decoder_configuration(adhp, format_ctx, 0) < 0) @@ -363,13 +291,13 @@ static void prepare_audio_decoding(libavsmash_audio_decode_handler_t* adhp, liba aohp->output_bits_per_sample = libavsmash_audio_get_best_used_bits_per_sample(adhp); AVCodecContext* ctx = libavsmash_audio_get_codec_context(adhp); as_setup_audio_rendering(aohp, ctx, &vi, env, "LSMASHAudioSource", channel_layout, sample_rate); - count_output_audio_samples(adhp, aohp, skip_priming, vi, env); + count_output_audio_samples(adhp, aohp, skip_priming, skip_tail, vi, env); /* Force seeking at the first reading. */ libavsmash_audio_force_seek(adhp); } LSMASHAudioSource::LSMASHAudioSource(const char* source, uint32_t track_number, bool skip_priming, const char* channel_layout, - int sample_rate, const char* preferred_decoder_names, const double drc, const char* ff_options, IScriptEnvironment* env) + int sample_rate, const char* preferred_decoder_names, const double drc, const char* ff_options, bool skip_tail, IScriptEnvironment* env) : LSMASHAudioSource {} { memset(&vi, 0, sizeof(VideoInfo)); @@ -380,7 +308,7 @@ LSMASHAudioSource::LSMASHAudioSource(const char* source, uint32_t track_number, libavsmash_audio_set_drc(adhp, drc); libavsmash_audio_set_decoder_options(adhp, ff_options); get_audio_track(source, track_number, env); - prepare_audio_decoding(adhp, aohp, format_ctx.get(), channel_layout, sample_rate, skip_priming, vi, env); + prepare_audio_decoding(adhp, aohp, format_ctx.get(), channel_layout, sample_rate, skip_priming, skip_tail, vi, env); lsmash_discard_boxes(libavsmash_audio_get_root(adhp)); } @@ -468,7 +396,8 @@ AVSValue __cdecl CreateLSMASHAudioSource(AVSValue args, void* user_data, IScript int ff_loglevel = args[6].AsInt(0); const double drc = args[7].AsFloat(-1.0); const char* ff_options = args[8].AsString(nullptr); + bool skip_tail = args[9].AsBool(false); set_av_log_level(ff_loglevel); return new LSMASHAudioSource( - source, track_number, skip_priming, layout_string, sample_rate, preferred_decoder_names, drc, ff_options, env); + source, track_number, skip_priming, layout_string, sample_rate, preferred_decoder_names, drc, ff_options, skip_tail, env); } diff --git a/AviSynth/libavsmash_source.h b/AviSynth/libavsmash_source.h index d6d22ad..6715db7 100644 --- a/AviSynth/libavsmash_source.h +++ b/AviSynth/libavsmash_source.h @@ -90,7 +90,7 @@ class LSMASHAudioSource : public LibavSMASHSource { public: LSMASHAudioSource(const char* source, uint32_t track_number, bool skip_priming, const char* channel_layout, int sample_rate, - const char* preferred_decoder_names, const double drc, const char* ff_options, IScriptEnvironment* env); + const char* preferred_decoder_names, const double drc, const char* ff_options, bool skip_tail, IScriptEnvironment* env); ~LSMASHAudioSource(); PVideoFrame __stdcall GetFrame(int n, IScriptEnvironment* env) { diff --git a/AviSynth/lsmashsource.cpp b/AviSynth/lsmashsource.cpp index 2c3c412..62caac4 100644 --- a/AviSynth/lsmashsource.cpp +++ b/AviSynth/lsmashsource.cpp @@ -57,7 +57,7 @@ extern "C" AVS_EXPORT const char* __stdcall AvisynthPluginInit3(IScriptEnvironme CreateLSMASHVideoSource, 0); /* LSMASHAudioSource */ env->AddFunction("LSMASHAudioSource", - "[source]s[track]i[skip_priming]b[layout]s[rate]i[decoder]s[ff_loglevel]i[drc_scale]f[ff_options]s", CreateLSMASHAudioSource, 0); + "[source]s[track]i[skip_priming]b[layout]s[rate]i[decoder]s[ff_loglevel]i[drc_scale]f[ff_options]s[skip_tail]b", CreateLSMASHAudioSource, 0); /* LWLibavVideoSource */ env->AddFunction("LWLibavVideoSource", "[source]s[stream_index]i[threads]i[cache]b[cachefile]s[seek_mode]i[seek_threshold]i[dr]b[fpsnum]i[fpsden]i[repeat]b[dominance]i[" diff --git a/VapourSynth/libavsmash_audio_source.c b/VapourSynth/libavsmash_audio_source.c index a0637f5..62daed6 100644 --- a/VapourSynth/libavsmash_audio_source.c +++ b/VapourSynth/libavsmash_audio_source.c @@ -9,8 +9,8 @@ #include #include -#include #include +#include #include "audio_output.h" #include "lsmashsource.h" @@ -63,86 +63,23 @@ static lsmas_audio_handler_t* alloc_handler(void) return hp; } -static int64_t get_start_time(lsmash_root_t* root, uint32_t track_id) -{ - uint32_t edit_count = lsmash_count_explicit_timeline_map(root, track_id); - for (uint32_t edit_number = 1; edit_number <= edit_count; edit_number++) { - lsmash_edit_t edit; - if (lsmash_get_explicit_timeline_map(root, track_id, edit_number, &edit) || edit.duration == 0) - return 0; - if (edit.start_time >= 0) - return edit.start_time; - } - return 0; -} - -static char* duplicate_string(const void* src, size_t length) -{ - char* dst = (char*)malloc(length + 1); - if (!dst) - return NULL; - memcpy(dst, src, length); - dst[length] = '\0'; - return dst; -} - -static int count_output_audio_samples(lsmas_audio_handler_t* hp, int skip_priming, VSMap* out, const VSAPI* vsapi) +static int count_output_audio_samples(lsmas_audio_handler_t* hp, int skip_priming, int skip_tail, VSMap* out, const VSAPI* vsapi) { - libavsmash_audio_decode_handler_t* adhp = hp->adhp; - libavsmash_audio_output_handler_t* aohp = hp->aohp; - lsmash_root_t* root = libavsmash_audio_get_root(adhp); - uint32_t track_id = libavsmash_audio_get_track_id(adhp); - uint64_t start_time = 0; - if (skip_priming) { - uint32_t media_timescale = libavsmash_audio_get_media_timescale(adhp); - uint32_t metadata_count = lsmash_count_itunes_metadata(root); - for (uint32_t i = 1; i <= metadata_count; i++) { - lsmash_itunes_metadata_t metadata; - if (lsmash_get_itunes_metadata(root, i, &metadata) < 0) - continue; - int matches = metadata.item == ITUNES_METADATA_ITEM_CUSTOM - && (metadata.type == ITUNES_METADATA_TYPE_STRING || metadata.type == ITUNES_METADATA_TYPE_BINARY) && metadata.meaning - && metadata.name && !strcmp(metadata.meaning, "com.apple.iTunes") && !strcmp(metadata.name, "iTunSMPB"); - char* value = NULL; - if (matches && metadata.type == ITUNES_METADATA_TYPE_STRING) { - size_t length = strlen(metadata.value.string); - if (length >= 116) - value = duplicate_string(metadata.value.string, length); - } else if (matches && metadata.value.binary.size >= 116) - value = duplicate_string(metadata.value.binary.data, metadata.value.binary.size); - lsmash_cleanup_itunes_metadata(&metadata); - if (!value) - continue; - uint32_t dummy[9]; - uint32_t priming_samples; - uint32_t padding; - uint64_t duration; - int fields = sscanf(value, " %x %x %x %" SCNx64 " %x %x %x %x %x %x %x %x", &dummy[0], &priming_samples, &padding, - &duration, &dummy[1], &dummy[2], &dummy[3], &dummy[4], &dummy[5], &dummy[6], &dummy[7], &dummy[8]); - lw_free(value); - if (fields != 12) - continue; - libavsmash_audio_set_implicit_preroll(adhp); - start_time = av_rescale(priming_samples, media_timescale, aohp->output_sample_rate); - aohp->skip_decoded_samples = priming_samples; - break; - } - if (aohp->skip_decoded_samples == 0) { - uint32_t ctd_shift; - if (lsmash_get_composition_to_decode_shift_from_media_timeline(root, track_id, &ctd_shift)) { - set_error_on_init(out, vsapi, "lsmas: failed to get the audio timeline shift."); - return -1; - } - start_time = ctd_shift + get_start_time(root, track_id); - aohp->skip_decoded_samples = av_rescale(start_time, aohp->output_sample_rate, media_timescale); - } + uint64_t final_num_samples = 0; + char error_msg[256] = { 0 }; + if (libavsmash_audio_setup_sample_count(hp->adhp, hp->aohp, skip_priming, skip_tail, &final_num_samples, error_msg, sizeof(error_msg)) + < 0) { + char vs_msg[300]; + snprintf(vs_msg, sizeof(vs_msg), "lsmas: %s", error_msg); + set_error_on_init(out, vsapi, vs_msg); + return -1; } - hp->ai.numSamples = libavsmash_audio_count_overall_pcm_samples(adhp, aohp->output_sample_rate, start_time); - if (hp->ai.numSamples <= 0) { - set_error_on_init(out, vsapi, "lsmas: no valid audio frame."); + if (final_num_samples > (uint64_t)INT64_MAX) { + set_error_on_init(out, vsapi, "lsmas: audio sample count exceeds INT64_MAX."); return -1; } - hp->ai.numFrames = (int)((hp->ai.numSamples + VS_AUDIO_FRAME_SAMPLES - 1) / VS_AUDIO_FRAME_SAMPLES); + hp->ai.numSamples = (int64_t)final_num_samples; + hp->ai.numFrames = (int)((final_num_samples + VS_AUDIO_FRAME_SAMPLES - 1) / VS_AUDIO_FRAME_SAMPLES); return 0; } @@ -208,6 +145,7 @@ void VS_CC vs_libavsmashaudiosource_create(const VSMap* in, VSMap* out, void* us const char* layout; const char* decoder; const char* ff_options; + int64_t skip_tail; int error; double drc = vsapi->mapGetFloat(in, "drc_scale", 0, &error); if (error) @@ -219,6 +157,7 @@ void VS_CC vs_libavsmashaudiosource_create(const VSMap* in, VSMap* out, void* us set_option_string(&layout, NULL, "layout", in, vsapi); set_option_string(&decoder, NULL, "decoder", in, vsapi); set_option_string(&ff_options, NULL, "ff_options", in, vsapi); + set_option_int64(&skip_tail, 0, "skip_tail", in, vsapi); set_av_log_level(ff_loglevel); lsmas_audio_handler_t* hp = alloc_handler(); @@ -261,7 +200,7 @@ void VS_CC vs_libavsmashaudiosource_create(const VSMap* in, VSMap* out, void* us hp->aohp->output_bits_per_sample = libavsmash_audio_get_best_used_bits_per_sample(hp->adhp); AVCodecContext* ctx = libavsmash_audio_get_codec_context(hp->adhp); if (vs_setup_audio_rendering(hp->aohp, ctx, layout, (int)MAX(rate, 0), &hp->ai, core, vsapi) < 0 - || count_output_audio_samples(hp, skip_priming != 0, out, vsapi) < 0) { + || count_output_audio_samples(hp, skip_priming != 0, skip_tail != 0, out, vsapi) < 0) { free_handler(&hp); if (!vsapi->mapGetError(out)) set_error_on_init(out, vsapi, "lsmas: failed to configure audio output."); @@ -270,8 +209,7 @@ void VS_CC vs_libavsmashaudiosource_create(const VSMap* in, VSMap* out, void* us libavsmash_audio_force_seek(hp->adhp); lsmash_discard_boxes(root); - VSNode* node = vsapi->createAudioFilter2( - "LibavSMASHAudioSource", &hp->ai, audio_get_frame, audio_free, fmUnordered, NULL, 0, hp, core); + VSNode* node = vsapi->createAudioFilter2("LibavSMASHAudioSource", &hp->ai, audio_get_frame, audio_free, fmUnordered, NULL, 0, hp, core); if (!node) { free_handler(&hp); vsapi->mapSetError(out, "lsmas: failed to create the LibavSMASH audio node."); diff --git a/VapourSynth/lsmashsource.c b/VapourSynth/lsmashsource.c index ef96318..c97116f 100644 --- a/VapourSynth/lsmashsource.c +++ b/VapourSynth/lsmashsource.c @@ -76,7 +76,7 @@ VS_EXTERNAL_API(void) VapourSynthPluginInit2(VSPlugin* plugin, const VSPLUGINAPI "clip:vnode;", vs_libavsmashsource_create, NULL, plugin); vspapi->registerFunction("LibavSMASHAudioSource", "source:data;track:int:opt;skip_priming:int:opt;layout:data:opt;rate:int:opt;decoder:data:opt;ff_loglevel:int:opt;drc_scale:" - "float:opt;ff_options:data:opt;", + "float:opt;ff_options:data:opt;skip_tail:int:opt;", "audio:anode;", vs_libavsmashaudiosource_create, NULL, plugin); vspapi->registerFunction("LWLibavSource", "source:data;stream_index:int:opt;cache:int:opt;cachefile:data:opt;" COMMON_OPTS diff --git a/common/libavsmash_audio.c b/common/libavsmash_audio.c index 126f71c..eb980b1 100644 --- a/common/libavsmash_audio.c +++ b/common/libavsmash_audio.c @@ -23,6 +23,9 @@ #include "libavsmash_audio.h" #include "cpp_compat.h" #include "resample.h" +#include + +#include /***************************************************************************** * Allocators / Deallocators @@ -346,10 +349,254 @@ static int get_frame_length( return 0; } -/* Count the number of whole output PCM samples. */ -uint64_t libavsmash_audio_count_overall_pcm_samples(libavsmash_audio_decode_handler_t* adhp, int output_sample_rate, uint64_t start_time) +static int parse_smpb_hex_u64(const char** pp, uint64_t* out) +{ + const char* p = *pp; + while (*p != '\0' && isspace((unsigned char)*p)) + p++; + /* SMPB fields are unsigned hex. Reject explicit signs. */ + if (*p == '+' || *p == '-') + return -1; + if (!isxdigit((unsigned char)*p)) + return -1; + errno = 0; + char* end = NULL; + unsigned long long v = strtoull(p, &end, 16); + if (errno == ERANGE || end == p) + return -1; + *out = (uint64_t)v; + *pp = end; + return 0; +} + +static int parse_itun_smpb_value(const char* value, uint32_t* priming_samples, uint32_t* padding_samples, uint64_t* duration_samples) +{ + if (!value || !priming_samples || !padding_samples || !duration_samples) + return -1; + const char* p = value; + for (int i = 0; i < 12; i++) { + uint64_t val = 0; + if (parse_smpb_hex_u64(&p, &val) < 0) + return -1; + // Only extract the fields we actually use. Ignore the size of the dummy fields. + if (i == 1) { + // Priming samples physically cannot exceed 32 bits (that would be >27 hours of audio) + if (val > UINT32_MAX) + return -1; + *priming_samples = (uint32_t)val; + } else if (i == 2) { + if (val > UINT32_MAX) + return -1; + *padding_samples = (uint32_t)val; + } else if (i == 3) { + *duration_samples = val; + } + } + return 0; +} + +static char* duplicate_itunes_metadata_value_as_cstring(const lsmash_itunes_metadata_t* metadata) +{ + if (metadata->type == ITUNES_METADATA_TYPE_STRING) { + if (!metadata->value.string) + return NULL; + size_t length = strlen(metadata->value.string); + char* value = (char*)malloc(length + 1); + if (!value) + return NULL; + memcpy(value, metadata->value.string, length + 1); + return value; + } + if (metadata->type == ITUNES_METADATA_TYPE_BINARY) { + if (!metadata->value.binary.data || metadata->value.binary.size == 0) + return NULL; + char* value = (char*)malloc((size_t)metadata->value.binary.size + 1); + if (!value) + return NULL; + memcpy(value, metadata->value.binary.data, metadata->value.binary.size); + value[metadata->value.binary.size] = '\0'; + return value; + } + return NULL; +} + +static int is_itun_smpb_metadata(const lsmash_itunes_metadata_t* metadata) +{ + return metadata->item == ITUNES_METADATA_ITEM_CUSTOM + && (metadata->type == ITUNES_METADATA_TYPE_STRING || metadata->type == ITUNES_METADATA_TYPE_BINARY) && metadata->meaning + && metadata->name && strcmp(metadata->meaning, "com.apple.iTunes") == 0 && strcmp(metadata->name, "iTunSMPB") == 0; +} + +int libavsmash_audio_get_itun_smpb(lsmash_root_t* root, uint32_t* priming_samples, uint32_t* padding_samples, uint64_t* duration_samples) +{ + if (!root || !priming_samples || !padding_samples || !duration_samples) + return -1; + uint32_t metadata_count = lsmash_count_itunes_metadata(root); + for (uint32_t i = 1; i <= metadata_count; i++) { + lsmash_itunes_metadata_t metadata; + if (lsmash_get_itunes_metadata(root, i, &metadata) < 0) + continue; + char* value = NULL; + if (is_itun_smpb_metadata(&metadata)) + value = duplicate_itunes_metadata_value_as_cstring(&metadata); + lsmash_cleanup_itunes_metadata(&metadata); + if (!value) + continue; + int ret = parse_itun_smpb_value(value, priming_samples, padding_samples, duration_samples); + free(value); + if (ret == 0) + return 0; + } + return -1; +} + +static int get_smpb_codec_sample_rate(libavsmash_audio_decode_handler_t* adhp, lw_audio_output_handler_t* aohp, int* codec_sample_rate) +{ + if (!adhp || !aohp || !codec_sample_rate) + return -1; + int output_sample_rate = aohp->output_sample_rate; + if (output_sample_rate <= 0) + return -1; + int rate = 0; + AVCodecContext* ctx = libavsmash_audio_get_codec_context(adhp); + if (ctx && ctx->sample_rate > 0) + rate = ctx->sample_rate; + if (rate <= 0) + rate = libavsmash_audio_get_best_used_sample_rate(adhp); + if (rate <= 0) { + uint32_t media_timescale = libavsmash_audio_get_media_timescale(adhp); + if (media_timescale > 0 && media_timescale <= (uint32_t)INT_MAX) + rate = (int)media_timescale; + } + if (rate <= 0) + return -1; + *codec_sample_rate = rate; + return 0; +} + +static int convert_smpb_samples64_to_output_rnd(libavsmash_audio_decode_handler_t* adhp, lw_audio_output_handler_t* aohp, uint64_t samples, + uint64_t* output_samples, enum AVRounding rnd) +{ + if (!adhp || !aohp || !output_samples) + return -1; + if (samples == 0) { + *output_samples = 0; + return 0; + } + if (samples > (uint64_t)INT64_MAX) + return -1; + int codec_sample_rate = 0; + if (get_smpb_codec_sample_rate(adhp, aohp, &codec_sample_rate) < 0) + return -1; + if (codec_sample_rate <= 0 || aohp->output_sample_rate <= 0) + return -1; + int64_t converted = av_rescale_rnd((int64_t)samples, aohp->output_sample_rate, codec_sample_rate, rnd); + if (converted < 0) + return -1; + *output_samples = (uint64_t)converted; + return 0; +} + +int libavsmash_audio_convert_smpb_samples_to_output(libavsmash_audio_decode_handler_t* adhp, lw_audio_output_handler_t* aohp, + uint64_t samples, uint64_t* output_samples, enum AVRounding rnd) +{ + return convert_smpb_samples64_to_output_rnd(adhp, aohp, samples, output_samples, rnd); +} + +uint64_t libavsmash_audio_count_total_codec_samples(libavsmash_audio_decode_handler_t* adhp) +{ + if (!adhp || adhp->frame_count == 0 || adhp->media_timescale == 0) + return 0; + codec_configuration_t* config = &adhp->config; + int fallback_sample_rate = config->ctx ? config->ctx->sample_rate : 0; + extended_summary_t* es = NULL; + int current_sample_rate = 0; + uint64_t current_frame_length = 0; + uint64_t total_codec_samples = 0; + for (uint32_t i = 1; i <= adhp->frame_count; i++) { + uint64_t frame_length; + if (get_frame_length(adhp, i, &frame_length, &es) < 0) + continue; + if (!es) + continue; + if ((current_sample_rate != es->sample_rate && es->sample_rate > 0) || current_frame_length != frame_length) { + current_sample_rate = es->sample_rate > 0 ? es->sample_rate : fallback_sample_rate; + current_frame_length = frame_length; + } + if (current_sample_rate > 0) + total_codec_samples += frame_length; + } + return total_codec_samples; +} + +int libavsmash_audio_apply_tail_trim(libavsmash_audio_decode_handler_t* adhp, lw_audio_output_handler_t* aohp, int have_smpb, + uint32_t priming_samples, uint32_t padding_samples, uint64_t duration_samples, int skip_priming, uint64_t total_codec_samples, + uint64_t base_output_samples, uint64_t* final_output_samples) +{ + if (!adhp || !aohp || !final_output_samples) + return -1; + uint64_t result = base_output_samples; + if (have_smpb) { + uint64_t effective_priming = 0; + if (skip_priming && priming_samples <= total_codec_samples) + effective_priming = priming_samples; + int trimmed_by_duration = 0; + /* + Preferred method: + Use SMPB duration only when: + - SMPB is present, + - tail trimming is enabled, + - priming skipping is enabled, + - duration is sane. + Duration is considered sane if: + duration > 0 + duration <= total_codec_samples - effective_priming + */ + if (skip_priming && duration_samples > 0 && duration_samples <= total_codec_samples - effective_priming) { + uint64_t duration_out = 0; + /* + For VapourSynth strict EOF we use conservative rounding AV_ROUND_DOWN. + If VapourSynth EOF is changed to match the AviSynth, more relaxed rounding AV_ROUND_UP can be used. + */ + if (libavsmash_audio_convert_smpb_samples_to_output(adhp, aohp, duration_samples, &duration_out, AV_ROUND_DOWN) == 0 + && duration_out > 0) { + if (duration_out > base_output_samples) + duration_out = base_output_samples; + result = duration_out; + trimmed_by_duration = 1; + } + } + /* + Fallback method: + If duration was not used, use padding if sane. + Padding is considered sane if: + padding <= total_codec_samples - effective_priming + */ + if (!trimmed_by_duration && padding_samples <= total_codec_samples - effective_priming) { + uint64_t padding_out = 0; + if (libavsmash_audio_convert_smpb_samples_to_output(adhp, aohp, padding_samples, &padding_out, AV_ROUND_UP) == 0) { + if (padding_out > base_output_samples) + padding_out = base_output_samples; + if (padding_out > 0 && padding_out < base_output_samples) + // We assume the older result is safe, instead return -1; + result = base_output_samples - padding_out; + } + } + } + *final_output_samples = result; + return 0; +} + +uint64_t libavsmash_audio_count_overall_pcm_samples( + libavsmash_audio_decode_handler_t* adhp, int output_sample_rate, uint64_t start_output_samples) { + if (!adhp || output_sample_rate <= 0) { + if (adhp) + adhp->pcm_sample_count = 0; + return 0; + } codec_configuration_t* config = &adhp->config; + int fallback_sample_rate = config->ctx ? config->ctx->sample_rate : 0; extended_summary_t* es = NULL; int current_sample_rate = 0; uint64_t current_frame_length = 0; @@ -367,18 +614,27 @@ uint64_t libavsmash_audio_count_overall_pcm_samples(libavsmash_audio_decode_hand overall_pcm_count += count_sequence_output_pcm_samples(sequence_pcm_count, current_sample_rate, output_sample_rate); sequence_pcm_count = 0; } - current_sample_rate = es->sample_rate > 0 ? es->sample_rate : config->ctx->sample_rate; + current_sample_rate = es->sample_rate > 0 ? es->sample_rate : fallback_sample_rate; current_frame_length = frame_length; } - sequence_pcm_count += frame_length; + if (current_sample_rate > 0) + sequence_pcm_count += frame_length; } if (!es || (sequence_pcm_count == 0 && overall_pcm_count == 0)) { adhp->pcm_sample_count = 0; return 0; } - current_sample_rate = es->sample_rate > 0 ? es->sample_rate : config->ctx->sample_rate; - overall_pcm_count += count_sequence_output_pcm_samples(sequence_pcm_count, current_sample_rate, output_sample_rate); - overall_pcm_count -= av_rescale(start_time, output_sample_rate, adhp->media_timescale); + current_sample_rate = es->sample_rate > 0 ? es->sample_rate : fallback_sample_rate; + if (current_sample_rate > 0) { + overall_pcm_count += count_sequence_output_pcm_samples(sequence_pcm_count, current_sample_rate, output_sample_rate); + } + /* + start_output_samples is already in the output sample rate. + + This avoids a media-time round trip and keeps the total length consistent + with aohp->skip_decoded_samples. + */ + overall_pcm_count = overall_pcm_count > start_output_samples ? overall_pcm_count - start_output_samples : 0; adhp->pcm_sample_count = overall_pcm_count; return overall_pcm_count; } @@ -548,3 +804,98 @@ uint64_t libavsmash_audio_get_pcm_samples( adhp->last_frame_number = frame_number; return output_length; } + +static int64_t get_start_time(lsmash_root_t* root, uint32_t track_id) +{ + uint32_t edit_count = lsmash_count_explicit_timeline_map(root, track_id); + for (uint32_t edit_number = 1; edit_number <= edit_count; edit_number++) { + lsmash_edit_t edit; + if (lsmash_get_explicit_timeline_map(root, track_id, edit_number, &edit) || edit.duration == 0) + return 0; + if (edit.start_time >= 0) + return edit.start_time; + } + return 0; +} + +int libavsmash_audio_setup_sample_count(libavsmash_audio_decode_handler_t* adhp, libavsmash_audio_output_handler_t* aohp, int skip_priming, + int skip_tail, uint64_t* out_final_num_samples, char* error_msg, size_t error_msg_size) +{ + if (!adhp || !aohp || !out_final_num_samples) { + if (error_msg) + snprintf(error_msg, error_msg_size, "invalid internal arguments."); + return -1; + } + if (aohp->output_sample_rate <= 0) { + if (error_msg) + snprintf(error_msg, error_msg_size, "invalid output sample rate."); + return -1; + } + lsmash_root_t* root = libavsmash_audio_get_root(adhp); + uint32_t track_id = libavsmash_audio_get_track_id(adhp); + uint32_t media_timescale = libavsmash_audio_get_media_timescale(adhp); + uint32_t priming_samples = 0; + uint32_t padding_samples = 0; + uint64_t duration_samples = 0; + int have_itun_smpb = (libavsmash_audio_get_itun_smpb(root, &priming_samples, &padding_samples, &duration_samples) == 0); + uint64_t total_codec_samples = 0; + if (have_itun_smpb && (skip_priming || skip_tail)) + total_codec_samples = libavsmash_audio_count_total_codec_samples(adhp); + uint64_t start_output_samples = 0; + if (skip_priming) { + if (have_itun_smpb) { + if (priming_samples > total_codec_samples) + priming_samples = 0; + uint64_t priming_skip_output = 0; + if (libavsmash_audio_convert_smpb_samples_to_output(adhp, aohp, priming_samples, &priming_skip_output, AV_ROUND_UP) < 0) { + if (error_msg) + snprintf(error_msg, error_msg_size, "invalid SMPB priming information."); + return -1; + } + libavsmash_audio_set_implicit_preroll(adhp); + aohp->skip_decoded_samples = priming_skip_output; + start_output_samples = priming_skip_output; + } else { + if (media_timescale == 0) { + if (error_msg) + snprintf(error_msg, error_msg_size, "invalid audio media timescale."); + return -1; + } + uint32_t ctd_shift = 0; + if (lsmash_get_composition_to_decode_shift_from_media_timeline(root, track_id, &ctd_shift)) { + if (error_msg) + snprintf(error_msg, error_msg_size, "failed to get the timeline shift."); + return -1; + } + int64_t edit_start = get_start_time(root, track_id); + if (edit_start < 0) + edit_start = 0; + uint64_t media_start_time = (uint64_t)edit_start + (uint64_t)ctd_shift; + int64_t skip = 0; + if (media_start_time <= (uint64_t)INT64_MAX) + skip = av_rescale((int64_t)media_start_time, aohp->output_sample_rate, media_timescale); + if (skip < 0) + skip = 0; + aohp->skip_decoded_samples = (uint64_t)skip; + start_output_samples = (uint64_t)skip; + } + } + uint64_t base_num_samples = libavsmash_audio_count_overall_pcm_samples(adhp, aohp->output_sample_rate, start_output_samples); + uint64_t final_num_samples = base_num_samples; + if (skip_tail) { + if (libavsmash_audio_apply_tail_trim(adhp, aohp, have_itun_smpb, priming_samples, padding_samples, duration_samples, skip_priming, + total_codec_samples, base_num_samples, &final_num_samples) + < 0) { + if (error_msg) + snprintf(error_msg, error_msg_size, "failed to apply tail trimming."); + return -1; + } + } + if (final_num_samples == 0) { + if (error_msg) + snprintf(error_msg, error_msg_size, "no valid audio frame."); + return -1; + } + *out_final_num_samples = final_num_samples; + return 0; +} diff --git a/common/libavsmash_audio.h b/common/libavsmash_audio.h index ad2dc8f..a1bfec2 100644 --- a/common/libavsmash_audio.h +++ b/common/libavsmash_audio.h @@ -127,11 +127,39 @@ void libavsmash_audio_apply_delay(libavsmash_audio_decode_handler_t* adhp, int64 void libavsmash_audio_set_implicit_preroll(libavsmash_audio_decode_handler_t* adhp); -uint64_t libavsmash_audio_count_overall_pcm_samples(libavsmash_audio_decode_handler_t* adhp, int output_sample_rate, uint64_t start_time); +int libavsmash_audio_get_itun_smpb(lsmash_root_t* root, uint32_t* priming_samples, uint32_t* padding_samples, uint64_t* duration_samples); + +/* + start_output_samples: + Number of leading output samples to discard from the total count. + This is expressed in the output sample rate, not in media time. +*/ +uint64_t libavsmash_audio_count_overall_pcm_samples( + libavsmash_audio_decode_handler_t* adhp, int output_sample_rate, uint64_t start_output_samples); + +/* + Convert an iTunSMPB sample count, which is normally expressed in codec samples, into output samples. + This is intended for priming now, and can also be reused for padding later. +*/ +int libavsmash_audio_convert_smpb_samples_to_output(libavsmash_audio_decode_handler_t* adhp, lw_audio_output_handler_t* aohp, + uint64_t samples, uint64_t* output_samples, enum AVRounding rnd); uint64_t libavsmash_audio_get_pcm_samples( libavsmash_audio_decode_handler_t* adhp, libavsmash_audio_output_handler_t* aohp, void* buf, int64_t start, int64_t wanted_length); +uint64_t libavsmash_audio_count_total_codec_samples(libavsmash_audio_decode_handler_t* adhp); + +int libavsmash_audio_apply_tail_trim(libavsmash_audio_decode_handler_t* adhp, lw_audio_output_handler_t* aohp, int have_smpb, + uint32_t priming_samples, uint32_t padding_samples, uint64_t duration_samples, int skip_priming, uint64_t total_codec_samples, + uint64_t base_output_samples, uint64_t* final_output_samples); + +/* + Calculates the final output sample count, applying priming skip and tail trimming. + Returns 0 on success, -1 on failure (with error_msg populated). +*/ +int libavsmash_audio_setup_sample_count(libavsmash_audio_decode_handler_t* adhp, libavsmash_audio_output_handler_t* aohp, int skip_priming, + int skip_tail, uint64_t* out_final_num_samples, char* error_msg, size_t error_msg_size); + #ifdef __cplusplus } #endif /* __cplusplus */ From 1f0364c38792de36f5369a465ee9c52228b34cd9 Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:40:57 +0300 Subject: [PATCH 4/8] Gapless: disable FFmpeg internal priming skip --- common/libavsmash_audio.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/libavsmash_audio.c b/common/libavsmash_audio.c index eb980b1..637c37c 100644 --- a/common/libavsmash_audio.c +++ b/common/libavsmash_audio.c @@ -266,6 +266,7 @@ int libavsmash_audio_initialize_decoder_configuration(libavsmash_audio_decode_ha strcpy(error_string, "Failed to find stream by libavformat.\n"); goto fail; } + codecpar->initial_padding = 0; /* libavcodec */ if (libavsmash_find_and_open_decoder(&adhp->config, codecpar, threads) < 0) { strcpy(error_string, "Failed to find and open the audio decoder.\n"); @@ -846,6 +847,9 @@ int libavsmash_audio_setup_sample_count(libavsmash_audio_decode_handler_t* adhp, if (have_itun_smpb) { if (priming_samples > total_codec_samples) priming_samples = 0; + AVCodecContext* ctx = libavsmash_audio_get_codec_context(adhp); + if (ctx && ctx->delay > 0) + ctx->delay = 0; uint64_t priming_skip_output = 0; if (libavsmash_audio_convert_smpb_samples_to_output(adhp, aohp, priming_samples, &priming_skip_output, AV_ROUND_UP) < 0) { if (error_msg) From 3e38960dd2fd2d3ce0f337a2302e6f9d8d96533c Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:20:42 +0300 Subject: [PATCH 5/8] VapourSynth: gapless random seek when resampling Fix the crash at the last frame: - fix resampler buffered samples lost at end of stream; - fix resampler warm-up samples lost at seek point. --- common/libavsmash_audio.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/common/libavsmash_audio.c b/common/libavsmash_audio.c index 637c37c..842264b 100644 --- a/common/libavsmash_audio.c +++ b/common/libavsmash_audio.c @@ -326,6 +326,14 @@ static inline uint64_t count_sequence_output_pcm_samples(uint64_t sequence_pcm_c return resampled_sample_count; } +static inline uint64_t count_sequence_output_pcm_samples_rnd( + uint64_t sequence_pcm_count, int current_sample_rate, int output_sample_rate, enum AVRounding rnd) +{ + if (output_sample_rate == current_sample_rate) + return sequence_pcm_count; + return av_rescale_rnd(sequence_pcm_count, output_sample_rate, current_sample_rate, rnd); +} + static int get_frame_length( libavsmash_audio_decode_handler_t* adhp, uint32_t frame_number, uint64_t* frame_length, extended_summary_t** esp) { @@ -612,7 +620,8 @@ uint64_t libavsmash_audio_count_overall_pcm_samples( /* Encountered a new sequence. */ if (current_sample_rate > 0) { /* Add the number of output PCM audio samples in the previous sequence. */ - overall_pcm_count += count_sequence_output_pcm_samples(sequence_pcm_count, current_sample_rate, output_sample_rate); + overall_pcm_count + += count_sequence_output_pcm_samples_rnd(sequence_pcm_count, current_sample_rate, output_sample_rate, AV_ROUND_DOWN); sequence_pcm_count = 0; } current_sample_rate = es->sample_rate > 0 ? es->sample_rate : fallback_sample_rate; @@ -627,7 +636,8 @@ uint64_t libavsmash_audio_count_overall_pcm_samples( } current_sample_rate = es->sample_rate > 0 ? es->sample_rate : fallback_sample_rate; if (current_sample_rate > 0) { - overall_pcm_count += count_sequence_output_pcm_samples(sequence_pcm_count, current_sample_rate, output_sample_rate); + overall_pcm_count + += count_sequence_output_pcm_samples_rnd(sequence_pcm_count, current_sample_rate, output_sample_rate, AV_ROUND_DOWN); } /* start_output_samples is already in the output sample rate. @@ -801,6 +811,10 @@ uint64_t libavsmash_audio_get_pcm_samples( ++frame_number; } while (1); audio_out: + if (aohp->request_length > 0) { + enum audio_output_flag flush_flags = AUDIO_OUTPUT_NO_FLAGS; + output_length += output_pcm_samples_from_buffer(aohp, adhp->frame_buffer, (uint8_t**)&buf, &flush_flags); + } adhp->next_pcm_sample_number = start + output_length; adhp->last_frame_number = frame_number; return output_length; @@ -895,6 +909,12 @@ int libavsmash_audio_setup_sample_count(libavsmash_audio_decode_handler_t* adhp, return -1; } } + int codec_rate = 0; + get_smpb_codec_sample_rate(adhp, aohp, &codec_rate); + if (codec_rate > 0 && aohp->output_sample_rate != codec_rate) { + if (final_num_samples > 32) + final_num_samples -= 32; + } if (final_num_samples == 0) { if (error_msg) snprintf(error_msg, error_msg_size, "no valid audio frame."); From 329324a2126d10a2a3e8cf92239f6a28279d0ffd Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:00:26 +0300 Subject: [PATCH 6/8] pypi: use Github nv-codec-headers repo --- .github/workflows/publish-pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 926a617..23a819b 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -42,7 +42,7 @@ env: # nv-codec-headers (Linux/Windows only) if [[ "$EXTRA_FFMPEG_FLAGS" == *"--enable-nvdec"* ]]; then - git clone -b "$nv_version" --depth 1 https://git.videolan.org/git/ffmpeg/nv-codec-headers + git clone -b "$nv_version" --depth 1 https://github.com/FFmpeg/nv-codec-headers cd nv-codec-headers $SUDO make install PREFIX="$PREFIX" cd .. && rm -rf nv-codec-headers From 7b9487166d79b7cce2083f7b4cdfc8b68f7ac9a0 Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:02:15 +0300 Subject: [PATCH 7/8] Include missing header --- common/libavsmash_audio.c | 1 + 1 file changed, 1 insertion(+) diff --git a/common/libavsmash_audio.c b/common/libavsmash_audio.c index 842264b..198b678 100644 --- a/common/libavsmash_audio.c +++ b/common/libavsmash_audio.c @@ -23,6 +23,7 @@ #include "libavsmash_audio.h" #include "cpp_compat.h" #include "resample.h" +#include #include #include From fa897a7ff30fbffb3bb456286f0ffe1822f93966 Mon Sep 17 00:00:00 2001 From: Asd-g <65298684+Asd-g@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:05:35 +0300 Subject: [PATCH 8/8] Update l-smash --- l-smash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l-smash b/l-smash index 84740c5..d186eb9 160000 --- a/l-smash +++ b/l-smash @@ -1 +1 @@ -Subproject commit 84740c5d960ab622f4c08b971dc59192bc27ef74 +Subproject commit d186eb95388710a7a91f6fd353169b457ebbb9db