From a676d89b16aa76a80abe7dd1588c8591e66fd3d2 Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:59:58 +0200 Subject: [PATCH 1/3] Give an unspecified decoder channel layout its default layout A plain mono or stereo WAV leaves its channel order unspecified. swr_init normalises the resampler input to the default layout, so swr_convert_frame rejected every decoded frame with AVERROR_INPUT_CHANGED. Set the default layout before the decoder opens, as the ffmpeg CLI does, in extract_audio and transcode. The passthrough check no longer needs its unspecified-order branch. --- native/exmpeg_native/src/audio.rs | 15 ++++++++++++++- native/exmpeg_native/src/extract_audio.rs | 14 +++++--------- native/exmpeg_native/src/transcode.rs | 1 + 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/native/exmpeg_native/src/audio.rs b/native/exmpeg_native/src/audio.rs index d9a96e9..99be454 100644 --- a/native/exmpeg_native/src/audio.rs +++ b/native/exmpeg_native/src/audio.rs @@ -1,8 +1,9 @@ //! Shared helpers for the audio re-encode paths (`extract_audio`, //! `transcode`). -use rsmpeg::avcodec::AVCodecRef; +use rsmpeg::avcodec::{AVCodecContext, AVCodecRef}; use rsmpeg::avutil::{AVChannelLayout, AVFrame}; +use rsmpeg::ffi; use crate::errors::NativeError; @@ -35,6 +36,18 @@ pub(crate) fn resolve_channels(requested: Option, src: i32) -> Result, src: i32) -> i32 { if let Some(fmts) = codec.sample_fmts() { if fmts.contains(&src) { diff --git a/native/exmpeg_native/src/extract_audio.rs b/native/exmpeg_native/src/extract_audio.rs index 6c39599..f005567 100644 --- a/native/exmpeg_native/src/extract_audio.rs +++ b/native/exmpeg_native/src/extract_audio.rs @@ -78,6 +78,7 @@ pub(crate) fn extract_audio>( let codecpar = input.streams()[audio_index].codecpar(); decoder.apply_codecpar(&codecpar)?; } + crate::audio::default_unspecified_layout(&mut decoder); decoder.set_time_base(stream_time_base); decoder.open(None)?; @@ -109,18 +110,13 @@ pub(crate) fn extract_audio>( // The layout must match for this to be safe: a 2-channel source with // a *specific* non-stereo mask (e.g. WAVEFORMATEXTENSIBLE FL+LFE) // sent unremapped under the canonical target layout the output - // stream advertises would mislabel/misorder its channels. An - // *unspecified* source order with the matching channel count has no - // order to preserve, so passthrough under the canonical target is - // fine (and avoids the resampler for the common WAV case, which - // reports its layout as unspecified). - let layout_ok = ffi_helpers::channel_layouts_equal(&decoder.ch_layout, &target_layout) - || (decoder.ch_layout.order == ffi::AV_CHANNEL_ORDER_UNSPEC - && decoder.ch_layout.nb_channels == target_channels); + // stream advertises would mislabel/misorder its channels. A plain + // WAV with an unspecified order already has the default layout from + // `default_unspecified_layout`, so it matches here. let passthrough = encoder.frame_size == 0 && decoder.sample_fmt == target_fmt && decoder.sample_rate == target_rate - && layout_ok; + && ffi_helpers::channel_layouts_equal(&decoder.ch_layout, &target_layout); let mut swr = if passthrough { None diff --git a/native/exmpeg_native/src/transcode.rs b/native/exmpeg_native/src/transcode.rs index 592da08..2670465 100644 --- a/native/exmpeg_native/src/transcode.rs +++ b/native/exmpeg_native/src/transcode.rs @@ -645,6 +645,7 @@ fn build_audio_pipeline( })?; let mut decoder = AVCodecContext::new(&decoder_codec); decoder.apply_codecpar(codecpar)?; + crate::audio::default_unspecified_layout(&mut decoder); // The decoder moves the timestamps of a frame it trims (Opus and AAC // priming) only when it knows the packet time_base. decoder.set_pkt_timebase(in_tb); From a6bc33ff6d1135570eaad33ac3a6a25a5082fc10 Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:59:58 +0200 Subject: [PATCH 2/3] Test the re-encode of a plain mono WAV in extract_audio and transcode --- test/integration_test.exs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/integration_test.exs b/test/integration_test.exs index ecb7515..5b1581a 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -971,6 +971,35 @@ defmodule Exmpeg.IntegrationTest do end end + # The audio-only fixture is a plain mono WAV, so its channel layout is + # unspecified. The next three tests re-encode it. + test "extract_audio re-encodes a mono 44.1 kHz WAV to mp3" do + wav = TestFixtures.ensure_audio_only_clip!() + out = Path.join(System.tmp_dir!(), "exmpeg_wav_mp3_#{System.unique_integer([:positive])}.mp3") + on_exit(fn -> File.rm(out) end) + + assert {:ok, _stats} = Exmpeg.extract_audio(wav, out) + assert {:ok, %MediaInfo{streams: [audio]}} = Exmpeg.probe(out) + assert audio.codec in ["mp3", "mp3float"] + assert %{sample_rate: 44_100, channels: 1} = audio.audio + end + + test "extract_audio resamples a mono 44.1 kHz WAV to 48 kHz opus" do + wav = TestFixtures.ensure_audio_only_clip!() + out = Path.join(System.tmp_dir!(), "exmpeg_wav_opus_#{System.unique_integer([:positive])}.opus") + on_exit(fn -> File.rm(out) end) + + assert {:ok, %{sample_rate: 48_000}} = Exmpeg.extract_audio(wav, out) + end + + test "transcode re-encodes a mono 44.1 kHz WAV to aac in m4a" do + wav = TestFixtures.ensure_audio_only_clip!() + out = Path.join(System.tmp_dir!(), "exmpeg_wav_aac_#{System.unique_integer([:positive])}.m4a") + on_exit(fn -> File.rm(out) end) + + assert {:ok, %{streams_reencoded: 1}} = Exmpeg.transcode(wav, out, audio_codec: "aac") + end + test "concat rejects inputs with mismatched stream layouts", %{clip: clip} do video_only = TestFixtures.ensure_video_only_clip!() out = Path.join(System.tmp_dir!(), "exmpeg_concat_bad_#{System.unique_integer([:positive])}.mp4") From 017e887582d859de995f8c6a61f2bd87b9ab1498 Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:59:58 +0200 Subject: [PATCH 3/3] Note the WAV re-encode fix in the changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75760d1..86dd19a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased + +### Fixed + +- `extract_audio/3` and `transcode/3` re-encode a plain mono or stereo + WAV again. Before, every call that resampled or re-encoded such a file, + for example to `.mp3`, `.m4a`, or `.opus`, returned + `{:error, %Exmpeg.Error{reason: :io_error}}` with the message + ``AVERROR(-1668179713): `Input changed` ``. The WAV leaves its channel + layout unspecified, and the decoder now gets the default layout for its + channel count, as in the `ffmpeg` CLI. + ## 0.6.0 - 2026-09-23 ### Changed