PyAV-Org / PyAV

Pythonic bindings for FFmpeg's libraries.
https://pyav.basswood-io.com/
BSD 3-Clause "New" or "Revised" License
2.43k stars 359 forks source link

Resource temporary unavailable issue when retiming audio #1369

Closed diksham-flwls closed 4 months ago

diksham-flwls commented 4 months ago

Overview

I am using pyav to load an retime an audio. I am using the following piece of code:

import av
from av.filter.context import FilterContext

import os
import tempfile

def link_nodes(*nodes: FilterContext) -> None:
    for c, n in zip(nodes, nodes[1:]):
        c.link_to(n)

def main() -> None:
    audio_in_container = av.open("test.wav", mode="r", format="wav")
    audio_in_stream = audio_in_container.streams.audio[0]

    stretched_audio = tempfile.NamedTemporaryFile()
    stretched_aud_path = f"{stretched_audio.name}.wav"
    if os.path.exists(stretched_aud_path):
        os.remove(stretched_aud_path)

    audio_out_container = av.open(stretched_aud_path, mode="w", format="wav")

    audio_codec = audio_in_container.streams.audio[0].codec_context.name

    output_stream = audio_out_container.add_stream(codec_name="mp3", rate=4800)

    # Setup filter graph
    filter_graph = av.filter.Graph()
    link_nodes(
        filter_graph.add_abuffer(template=audio_in_stream),
        filter_graph.add("atempo", "2.0"),
        filter_graph.add("abuffersink"),
    )
    filter_graph.configure()

    # Render audio
    for idx, audio_frame in enumerate(audio_in_container.decode(audio_in_stream)):
        filter_graph.push(audio_frame)
        retimed_audio_frame = filter_graph.pull()
        packet = output_stream.encode(retimed_audio_frame)
        audio_out_container.mux(packet)

    audio_out_container.close()

if __name__ == "__main__":
    main()

Expected behavior

I expected the function to work for all packets seamlessly. The len(output audio) should be len(input_audio) / speed_factor. Eg: input audio: sample_rate: 22050 length: 74420 time: 3.375s

speed factor: 0.617

output audio: sample_rate: 22050 length: 120540 time: 5.4667s

Actual behavior

I face the following error for few packets at line filter_graph.pull() av.error.BlockingIOError: [Errno 11] Resource temporarily unavailable

Due to missing these packets the actual length of the audio: 117789, time 5.3419s

Traceback:

av.error.BlockingIOError: [Errno 11] Resource temporarily unavailable

Versions

Research

I have done the following:

WyattBlue commented 4 months ago

For filters like atempo, you need to push multiple frames before the filter starts making output frames. See my example for how you would do that in code:

import av
from av.filter.context import FilterContext

import os
import time
import tempfile

def link_nodes(*nodes: FilterContext) -> None:
    for c, n in zip(nodes, nodes[1:]):
        c.link_to(n)

def main() -> None:
    stretched_aud_path = "out.mp3"

    audio_in_container = av.open("test.wav", mode="r", format="wav")
    audio_in_stream = audio_in_container.streams.audio[0]

    if os.path.exists(stretched_aud_path):
        os.remove(stretched_aud_path)

    audio_out_container = av.open(stretched_aud_path, mode="w", format="wav")
    output_stream = audio_out_container.add_stream(codec_name="mp3", rate=48000)

    # Setup filter graph
    filter_graph = av.filter.Graph()
    link_nodes(
        filter_graph.add_abuffer(template=audio_in_stream),
        filter_graph.add("atempo", "2.0"),
        filter_graph.add("abuffersink"),
    )
    filter_graph.configure()

    # Render audio
    for frame in audio_in_container.decode(audio=0):
        filter_graph.push(frame)
        while True:
            try:
                output_frame = filter_graph.pull()
                for packet in output_stream.encode(output_frame):
                    audio_out_container.mux(packet)
            except av.BlockingIOError:
                break

    # After pushing all frames, flush the filter graph
    for packet in output_stream.encode(None):
        audio_out_container.mux(packet)

    audio_out_container.close()
    audio_in_container.close()

if __name__ == "__main__":
    main()