Initialisation du repository de Beta

This commit is contained in:
Mathis 2026-02-06 22:23:20 +01:00
commit 14985f6dbb
9469 changed files with 1903273 additions and 0 deletions

View file

@ -0,0 +1,2 @@
from .frame import AudioFrame
from .stream import AudioStream

View file

@ -0,0 +1,16 @@
from typing import Literal
from .frame import AudioFrame
from .stream import AudioStream
_AudioCodecName = Literal[
"aac",
"libopus",
"mp2",
"mp3",
"pcm_alaw",
"pcm_mulaw",
"pcm_s16le",
]
__all__ = ("AudioFrame", "AudioStream")

View file

@ -0,0 +1,11 @@
from av.audio.frame cimport AudioFrame
from av.audio.resampler cimport AudioResampler
from av.codec.context cimport CodecContext
cdef class AudioCodecContext(CodecContext):
# Hold onto the frames that we will decode until we have a full one.
cdef AudioFrame next_frame
# For encoding.
cdef AudioResampler resampler

View file

@ -0,0 +1,105 @@
import cython
from cython.cimports import libav as lib
from cython.cimports.av.audio.format import AudioFormat, get_audio_format
from cython.cimports.av.audio.frame import AudioFrame, alloc_audio_frame
from cython.cimports.av.audio.layout import AudioLayout, get_audio_layout
from cython.cimports.av.frame import Frame
from cython.cimports.av.packet import Packet
@cython.cclass
class AudioCodecContext(CodecContext):
@cython.cfunc
def _prepare_frames_for_encode(self, input_frame: Frame | None):
frame: AudioFrame | None = input_frame
allow_var_frame_size: cython.bint = (
self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE
)
# Note that the resampler will simply return an input frame if there is
# no resampling to be done. The control flow was just a little easier this way.
if not self.resampler:
self.resampler = AudioResampler(
format=self.format,
layout=self.layout,
rate=self.ptr.sample_rate,
frame_size=None if allow_var_frame_size else self.ptr.frame_size,
)
frames = self.resampler.resample(frame)
if input_frame is None:
frames.append(None) # flush if input frame is None
return frames
@cython.cfunc
def _alloc_next_frame(self) -> Frame:
return alloc_audio_frame()
@cython.cfunc
def _setup_decoded_frame(self, frame: Frame, packet: Packet):
CodecContext._setup_decoded_frame(self, frame, packet)
aframe: AudioFrame = frame
aframe._init_user_attributes()
@property
def frame_size(self):
"""
Number of samples per channel in an audio frame.
:type: int
"""
return self.ptr.frame_size
@property
def sample_rate(self):
"""
Sample rate of the audio data, in samples per second.
:type: int
"""
return self.ptr.sample_rate
@sample_rate.setter
def sample_rate(self, value: cython.int):
self.ptr.sample_rate = value
@property
def rate(self):
"""Another name for :attr:`sample_rate`."""
return self.sample_rate
@rate.setter
def rate(self, value):
self.sample_rate = value
@property
def channels(self):
return self.layout.nb_channels
@property
def layout(self):
"""
The audio channel layout.
:type: AudioLayout
"""
return get_audio_layout(self.ptr.ch_layout)
@layout.setter
def layout(self, value):
layout: AudioLayout = AudioLayout(value)
self.ptr.ch_layout = layout.layout
@property
def format(self):
"""
The audio sample format.
:type: AudioFormat
"""
return get_audio_format(self.ptr.sample_fmt)
@format.setter
def format(self, value):
format: AudioFormat = AudioFormat(value)
self.ptr.sample_fmt = format.sample_fmt

View file

@ -0,0 +1,29 @@
from typing import Iterator, Literal
from av.codec.context import CodecContext
from av.packet import Packet
from .format import AudioFormat
from .frame import AudioFrame
from .layout import AudioLayout
class _Format:
def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ...
def __set__(self, instance: object, value: AudioFormat | str) -> None: ...
class _Layout:
def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ...
def __set__(self, instance: object, value: AudioLayout | str) -> None: ...
class AudioCodecContext(CodecContext):
frame_size: int
sample_rate: int
rate: int
type: Literal["audio"]
format: _Format
layout: _Layout
@property
def channels(self) -> int: ...
def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ...
def encode_lazy(self, frame: AudioFrame | None = None) -> Iterator[Packet]: ...
def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ...

View file

@ -0,0 +1,19 @@
cimport libav as lib
from libc.stdint cimport int64_t, uint64_t
from av.audio.frame cimport AudioFrame
cdef class AudioFifo:
cdef lib.AVAudioFifo *ptr
cdef AudioFrame template
cdef readonly uint64_t samples_written
cdef readonly uint64_t samples_read
cdef readonly double pts_per_sample
cpdef write(self, AudioFrame frame)
cpdef read(self, int samples=*, bint partial=*)
cpdef read_many(self, int samples, bint partial=*)

View file

@ -0,0 +1,22 @@
from .format import AudioFormat
from .frame import AudioFrame
from .layout import AudioLayout
class AudioFifo:
def write(self, frame: AudioFrame) -> None: ...
def read(self, samples: int = 0, partial: bool = False) -> AudioFrame | None: ...
def read_many(self, samples: int, partial: bool = False) -> list[AudioFrame]: ...
@property
def format(self) -> AudioFormat: ...
@property
def layout(self) -> AudioLayout: ...
@property
def sample_rate(self) -> int: ...
@property
def samples(self) -> int: ...
@property
def samples_written(self) -> int: ...
@property
def samples_read(self) -> int: ...
@property
def pts_per_sample(self) -> float: ...

View file

@ -0,0 +1,197 @@
from av.audio.frame cimport alloc_audio_frame
from av.error cimport err_check
cdef class AudioFifo:
"""A simple audio sample FIFO (First In First Out) buffer."""
def __repr__(self):
try:
result = (
f"<av.{self.__class__.__name__} {self.samples} samples of "
f"{self.sample_rate}hz {self.layout} {self.format} at 0x{id(self):x}>"
)
except AttributeError:
result = (
f"<av.{self.__class__.__name__} uninitialized, use fifo.write(frame),"
f" at 0x{id(self):x}>"
)
return result
def __dealloc__(self):
if self.ptr:
lib.av_audio_fifo_free(self.ptr)
cpdef write(self, AudioFrame frame):
"""write(frame)
Push a frame of samples into the queue.
:param AudioFrame frame: The frame of samples to push.
The FIFO will remember the attributes from the first frame, and use those
to populate all output frames.
If there is a :attr:`~.Frame.pts` and :attr:`~.Frame.time_base` and
:attr:`~.AudioFrame.sample_rate`, then the FIFO will assert that the incoming
timestamps are continuous.
"""
if frame is None:
raise TypeError("AudioFifo must be given an AudioFrame.")
if not frame.ptr.nb_samples:
return
if not self.ptr:
# Hold onto a copy of the attributes of the first frame to populate
# output frames with.
self.template = alloc_audio_frame()
self.template._copy_internal_attributes(frame)
self.template._init_user_attributes()
# Figure out our "time_base".
if frame._time_base.num and frame.ptr.sample_rate:
self.pts_per_sample = frame._time_base.den / float(frame._time_base.num)
self.pts_per_sample /= frame.ptr.sample_rate
else:
self.pts_per_sample = 0
self.ptr = lib.av_audio_fifo_alloc(
<lib.AVSampleFormat>frame.ptr.format,
frame.layout.nb_channels,
frame.ptr.nb_samples * 2, # Just a default number of samples; it will adjust.
)
if not self.ptr:
raise RuntimeError("Could not allocate AVAudioFifo.")
# Make sure nothing changed.
elif (
frame.ptr.format != self.template.ptr.format or
# TODO: frame.ptr.ch_layout != self.template.ptr.ch_layout or
frame.ptr.sample_rate != self.template.ptr.sample_rate or
(frame._time_base.num and self.template._time_base.num and (
frame._time_base.num != self.template._time_base.num or
frame._time_base.den != self.template._time_base.den
))
):
raise ValueError("Frame does not match AudioFifo parameters.")
# Assert that the PTS are what we expect.
cdef int64_t expected_pts
if self.pts_per_sample and frame.ptr.pts != lib.AV_NOPTS_VALUE:
expected_pts = <int64_t>(self.pts_per_sample * self.samples_written)
if frame.ptr.pts != expected_pts:
raise ValueError(
"Frame.pts (%d) != expected (%d); fix or set to None." % (frame.ptr.pts, expected_pts)
)
err_check(lib.av_audio_fifo_write(
self.ptr,
<void **>frame.ptr.extended_data,
frame.ptr.nb_samples,
))
self.samples_written += frame.ptr.nb_samples
cpdef read(self, int samples=0, bint partial=False):
"""read(samples=0, partial=False)
Read samples from the queue.
:param int samples: The number of samples to pull; 0 gets all.
:param bool partial: Allow returning less than requested.
:returns: New :class:`AudioFrame` or ``None`` (if empty).
If the incoming frames had valid a :attr:`~.Frame.time_base`,
:attr:`~.AudioFrame.sample_rate` and :attr:`~.Frame.pts`, the returned frames
will have accurate timing.
"""
if not self.ptr:
return
cdef int buffered_samples = lib.av_audio_fifo_size(self.ptr)
if buffered_samples < 1:
return
samples = samples or buffered_samples
if buffered_samples < samples:
if partial:
samples = buffered_samples
else:
return
cdef AudioFrame frame = alloc_audio_frame()
frame._copy_internal_attributes(self.template)
frame._init(
<lib.AVSampleFormat>self.template.ptr.format,
<lib.AVChannelLayout>self.template.ptr.ch_layout,
samples,
1, # Align?
)
err_check(lib.av_audio_fifo_read(
self.ptr,
<void **>frame.ptr.extended_data,
samples,
))
if self.pts_per_sample:
frame.ptr.pts = <uint64_t>(self.pts_per_sample * self.samples_read)
else:
frame.ptr.pts = lib.AV_NOPTS_VALUE
self.samples_read += samples
return frame
cpdef read_many(self, int samples, bint partial=False):
"""read_many(samples, partial=False)
Read as many frames as we can.
:param int samples: How large for the frames to be.
:param bool partial: If we should return a partial frame.
:returns: A ``list`` of :class:`AudioFrame`.
"""
cdef AudioFrame frame
frames = []
while True:
frame = self.read(samples, partial=partial)
if frame is not None:
frames.append(frame)
else:
break
return frames
@property
def format(self):
"""The :class:`.AudioFormat` of this FIFO."""
if not self.ptr:
raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'format'")
return self.template.format
@property
def layout(self):
"""The :class:`.AudioLayout` of this FIFO."""
if not self.ptr:
raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'layout'")
return self.template.layout
@property
def sample_rate(self):
if not self.ptr:
raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'")
return self.template.sample_rate
@property
def samples(self):
"""Number of audio samples (per channel) in the buffer."""
return lib.av_audio_fifo_size(self.ptr) if self.ptr else 0

View file

@ -0,0 +1,7 @@
cimport libav as lib
cdef class AudioFormat:
cdef lib.AVSampleFormat sample_fmt
cdef AudioFormat get_audio_format(lib.AVSampleFormat format)

View file

@ -0,0 +1,142 @@
import sys
import cython
container_format_postfix: str = "le" if sys.byteorder == "little" else "be"
_cinit_bypass_sentinel = object()
@cython.cfunc
def get_audio_format(c_format: lib.AVSampleFormat) -> AudioFormat:
"""Get an AudioFormat without going through a string."""
if c_format < 0:
return None
format: AudioFormat = AudioFormat(_cinit_bypass_sentinel)
format.sample_fmt = c_format
return format
@cython.cclass
class AudioFormat:
"""Descriptor of audio formats."""
def __cinit__(self, name):
if name is _cinit_bypass_sentinel:
return
sample_fmt: lib.AVSampleFormat
if isinstance(name, AudioFormat):
sample_fmt = cython.cast(AudioFormat, name).sample_fmt
else:
sample_fmt = lib.av_get_sample_fmt(name)
if sample_fmt < 0:
raise ValueError(f"Not a sample format: {name!r}")
self.sample_fmt = sample_fmt
def __repr__(self):
return f"<av.AudioFormat {self.name}>"
@property
def name(self):
"""Canonical name of the sample format.
>>> AudioFormat('s16p').name
's16p'
"""
return lib.av_get_sample_fmt_name(self.sample_fmt)
@property
def bytes(self):
"""Number of bytes per sample.
>>> AudioFormat('s16p').bytes
2
"""
return lib.av_get_bytes_per_sample(self.sample_fmt)
@property
def bits(self):
"""Number of bits per sample.
>>> AudioFormat('s16p').bits
16
"""
return lib.av_get_bytes_per_sample(self.sample_fmt) << 3
@property
def is_planar(self):
"""Is this a planar format?
Strictly opposite of :attr:`is_packed`.
"""
return bool(lib.av_sample_fmt_is_planar(self.sample_fmt))
@property
def is_packed(self):
"""Is this a packed format?
Strictly opposite of :attr:`is_planar`.
"""
return not lib.av_sample_fmt_is_planar(self.sample_fmt)
@property
def planar(self):
"""The planar variant of this format.
Is itself when planar:
>>> fmt = AudioFormat('s16p')
>>> fmt.planar is fmt
True
"""
if self.is_planar:
return self
return get_audio_format(lib.av_get_planar_sample_fmt(self.sample_fmt))
@property
def packed(self):
"""The packed variant of this format.
Is itself when packed:
>>> fmt = AudioFormat('s16')
>>> fmt.packed is fmt
True
"""
if self.is_packed:
return self
return get_audio_format(lib.av_get_packed_sample_fmt(self.sample_fmt))
@property
def container_name(self):
"""The name of a :class:`ContainerFormat` which directly accepts this data.
:raises ValueError: when planar, since there are no such containers.
"""
if self.is_planar:
raise ValueError("no planar container formats")
if self.sample_fmt == lib.AV_SAMPLE_FMT_U8:
return "u8"
elif self.sample_fmt == lib.AV_SAMPLE_FMT_S16:
return "s16" + container_format_postfix
elif self.sample_fmt == lib.AV_SAMPLE_FMT_S32:
return "s32" + container_format_postfix
elif self.sample_fmt == lib.AV_SAMPLE_FMT_FLT:
return "f32" + container_format_postfix
elif self.sample_fmt == lib.AV_SAMPLE_FMT_DBL:
return "f64" + container_format_postfix
raise ValueError("unknown layout")

View file

@ -0,0 +1,11 @@
class AudioFormat:
name: str
bytes: int
bits: int
is_planar: bool
is_packed: bool
planar: AudioFormat
packed: AudioFormat
container_name: str
def __init__(self, name: str | AudioFormat) -> None: ...

View file

@ -0,0 +1,31 @@
cimport libav as lib
from libc.stdint cimport uint8_t, uint64_t
from av.audio.format cimport AudioFormat
from av.audio.layout cimport AudioLayout
from av.frame cimport Frame
cdef class AudioFrame(Frame):
# For raw storage of the frame's data; don't ever touch this.
cdef uint8_t *_buffer
cdef size_t _buffer_size
cdef readonly AudioLayout layout
"""
The audio channel layout.
:type: AudioLayout
"""
cdef readonly AudioFormat format
"""
The audio sample format.
:type: AudioFormat
"""
cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align)
cdef _init_user_attributes(self)
cdef AudioFrame alloc_audio_frame()

View file

@ -0,0 +1,205 @@
import cython
from cython.cimports.av.audio.format import get_audio_format
from cython.cimports.av.audio.layout import get_audio_layout
from cython.cimports.av.audio.plane import AudioPlane
from cython.cimports.av.error import err_check
from cython.cimports.av.utils import check_ndarray
_cinit_bypass_sentinel = object()
@cython.cfunc
def alloc_audio_frame() -> AudioFrame:
return AudioFrame(_cinit_bypass_sentinel)
format_dtypes = {
"dbl": "f8",
"dblp": "f8",
"flt": "f4",
"fltp": "f4",
"s16": "i2",
"s16p": "i2",
"s32": "i4",
"s32p": "i4",
"u8": "u1",
"u8p": "u1",
}
@cython.cclass
class AudioFrame(Frame):
"""A frame of audio."""
def __cinit__(self, format="s16", layout="stereo", samples=0, align=1):
if format is _cinit_bypass_sentinel:
return
cy_format: AudioFormat = AudioFormat(format)
cy_layout: AudioLayout = AudioLayout(layout)
self._init(cy_format.sample_fmt, cy_layout.layout, samples, align)
@cython.cfunc
def _init(
self,
format: lib.AVSampleFormat,
layout: lib.AVChannelLayout,
nb_samples: cython.uint,
align: cython.uint,
):
self.ptr.nb_samples = nb_samples
self.ptr.format = format
self.ptr.ch_layout = layout
# Sometimes this is called twice. Oh well.
self._init_user_attributes()
if self.layout.nb_channels != 0 and nb_samples:
# Cleanup the old buffer.
lib.av_freep(cython.address(self._buffer))
# Get a new one.
self._buffer_size = err_check(
lib.av_samples_get_buffer_size(
cython.NULL, self.layout.nb_channels, nb_samples, format, align
)
)
self._buffer = cython.cast(
cython.pointer[uint8_t], lib.av_malloc(self._buffer_size)
)
if not self._buffer:
raise MemoryError("cannot allocate AudioFrame buffer")
# Connect the data pointers to the buffer.
err_check(
lib.avcodec_fill_audio_frame(
self.ptr,
self.layout.nb_channels,
cython.cast(lib.AVSampleFormat, self.ptr.format),
self._buffer,
self._buffer_size,
align,
)
)
def __dealloc__(self):
lib.av_freep(cython.address(self._buffer))
@cython.cfunc
def _init_user_attributes(self):
self.layout = get_audio_layout(self.ptr.ch_layout)
self.format = get_audio_format(cython.cast(lib.AVSampleFormat, self.ptr.format))
def __repr__(self):
return (
f"<av.{self.__class__.__name__} pts={self.pts}, {self.samples} "
f"samples at {self.rate}Hz, {self.layout.name}, {self.format.name} at 0x{id(self):x}>"
)
@staticmethod
def from_ndarray(array, format="s16", layout="stereo"):
"""
Construct a frame from a numpy array.
"""
import numpy as np
py_format = format if isinstance(format, AudioFormat) else AudioFormat(format)
py_layout = layout if isinstance(layout, AudioLayout) else AudioLayout(layout)
format = py_format.name
# map avcodec type to numpy type
try:
dtype = np.dtype(format_dtypes[format])
except KeyError:
raise ValueError(
f"Conversion from numpy array with format `{format}` is not yet supported"
)
# check input format
nb_channels = py_layout.nb_channels
check_ndarray(array, dtype, 2)
if py_format.is_planar:
if array.shape[0] != nb_channels:
raise ValueError(
f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`"
)
samples = array.shape[1]
else:
if array.shape[0] != 1:
raise ValueError(
f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`"
)
samples = array.shape[1] // nb_channels
frame = AudioFrame(format=py_format, layout=py_layout, samples=samples)
for i, plane in enumerate(frame.planes):
plane.update(array[i, :])
return frame
@property
def planes(self):
"""
A tuple of :class:`~av.audio.plane.AudioPlane`.
:type: tuple
"""
plane_count: cython.int = 0
while self.ptr.extended_data[plane_count]:
plane_count += 1
return tuple([AudioPlane(self, i) for i in range(plane_count)])
@property
def samples(self):
"""
Number of audio samples (per channel).
:type: int
"""
return self.ptr.nb_samples
@property
def sample_rate(self):
"""
Sample rate of the audio data, in samples per second.
:type: int
"""
return self.ptr.sample_rate
@sample_rate.setter
def sample_rate(self, value):
self.ptr.sample_rate = value
@property
def rate(self):
"""Another name for :attr:`sample_rate`."""
return self.ptr.sample_rate
@rate.setter
def rate(self, value):
self.ptr.sample_rate = value
def to_ndarray(self):
"""Get a numpy array of this frame.
.. note:: Numpy must be installed.
"""
import numpy as np
try:
dtype = np.dtype(format_dtypes[self.format.name])
except KeyError:
raise ValueError(
f"Conversion from {self.format.name!r} format to numpy array is not supported."
)
if self.format.is_planar:
count = self.samples
else:
count = self.samples * self.layout.nb_channels
return np.vstack(
[np.frombuffer(x, dtype=dtype, count=count) for x in self.planes]
)

View file

@ -0,0 +1,49 @@
from typing import Any, Union
import numpy as np
from av.frame import Frame
from .format import AudioFormat
from .layout import AudioLayout
from .plane import AudioPlane
format_dtypes: dict[str, str]
_SupportedNDarray = Union[
np.ndarray[Any, np.dtype[np.float64]], # f8
np.ndarray[Any, np.dtype[np.float32]], # f4
np.ndarray[Any, np.dtype[np.int32]], # i4
np.ndarray[Any, np.dtype[np.int16]], # i2
np.ndarray[Any, np.dtype[np.uint8]], # u1
]
class _Format:
def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ...
def __set__(self, instance: object, value: AudioFormat | str) -> None: ...
class _Layout:
def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ...
def __set__(self, instance: object, value: AudioLayout | str) -> None: ...
class AudioFrame(Frame):
planes: tuple[AudioPlane, ...]
samples: int
sample_rate: int
rate: int
format: _Format
layout: _Layout
def __init__(
self,
format: AudioFormat | str = "s16",
layout: AudioLayout | str = "stereo",
samples: int = 0,
align: int = 1,
) -> None: ...
@staticmethod
def from_ndarray(
array: _SupportedNDarray,
format: AudioFormat | str = "s16",
layout: AudioLayout | str = "stereo",
) -> AudioFrame: ...
def to_ndarray(self) -> _SupportedNDarray: ...

View file

@ -0,0 +1,8 @@
cimport libav as lib
cdef class AudioLayout:
cdef lib.AVChannelLayout layout
cdef _init(self, lib.AVChannelLayout layout)
cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout)

View file

@ -0,0 +1,12 @@
from dataclasses import dataclass
class AudioLayout:
name: str
nb_channels: int
channels: tuple[AudioChannel, ...]
def __init__(self, layout: str | AudioLayout): ...
@dataclass
class AudioChannel:
name: str
description: str

View file

@ -0,0 +1,82 @@
cimport libav as lib
from cpython.bytes cimport PyBytes_FromStringAndSize
from dataclasses import dataclass
@dataclass
class AudioChannel:
name: str
description: str
def __repr__(self):
return f"<av.AudioChannel '{self.name}' ({self.description})>"
cdef object _cinit_bypass_sentinel
cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout):
"""Get an AudioLayout from Cython land."""
cdef AudioLayout layout = AudioLayout.__new__(AudioLayout, _cinit_bypass_sentinel)
layout._init(c_layout)
return layout
cdef class AudioLayout:
def __init__(self, layout):
if layout is _cinit_bypass_sentinel:
return
if type(layout) is str:
ret = lib.av_channel_layout_from_string(&c_layout, layout)
if ret != 0:
raise ValueError(f"Invalid layout: {layout}")
elif isinstance(layout, AudioLayout):
c_layout = (<AudioLayout>layout).layout
else:
raise TypeError(f"layout must be of type: string | av.AudioLayout, got {type(layout)}")
self._init(c_layout)
cdef _init(self, lib.AVChannelLayout layout):
self.layout = layout
def __repr__(self):
return f"<av.{self.__class__.__name__} {self.name!r}>"
def __eq__(self, other):
return isinstance(other, AudioLayout) and self.name == other.name and self.nb_channels == other.nb_channels
@property
def nb_channels(self):
return self.layout.nb_channels
@property
def channels(self):
cdef char buf[16]
cdef char buf2[128]
results = []
for index in range(self.layout.nb_channels):
size = lib.av_channel_name(buf, sizeof(buf), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1
size2 = lib.av_channel_description(buf2, sizeof(buf2), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1
results.append(
AudioChannel(
PyBytes_FromStringAndSize(buf, size).decode("utf-8"),
PyBytes_FromStringAndSize(buf2, size2).decode("utf-8"),
)
)
return tuple(results)
@property
def name(self) -> str:
"""The canonical name of the audio layout."""
cdef char layout_name[128]
cdef int ret
ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name))
if ret < 0:
raise RuntimeError(f"Failed to get layout name: {ret}")
return layout_name

View file

@ -0,0 +1,6 @@
from av.plane cimport Plane
cdef class AudioPlane(Plane):
cdef readonly size_t buffer_size
cdef size_t _buffer_size(self)

View file

@ -0,0 +1,13 @@
import cython
from cython.cimports.av.audio.frame import AudioFrame
@cython.cclass
class AudioPlane(Plane):
def __cinit__(self, frame: AudioFrame, index: cython.int):
# Only the first linesize is ever populated, but it applies to every plane.
self.buffer_size = self.frame.ptr.linesize[0]
@cython.cfunc
def _buffer_size(self) -> cython.size_t:
return self.buffer_size

View file

@ -0,0 +1,4 @@
from av.plane import Plane
class AudioPlane(Plane):
buffer_size: int

View file

@ -0,0 +1,18 @@
from av.audio.format cimport AudioFormat
from av.audio.frame cimport AudioFrame
from av.audio.layout cimport AudioLayout
from av.filter.graph cimport Graph
cdef class AudioResampler:
cdef readonly bint is_passthrough
cdef AudioFrame template
# Destination descriptors
cdef readonly AudioFormat format
cdef readonly AudioLayout layout
cdef readonly int rate
cdef readonly unsigned int frame_size
cdef Graph graph
cpdef list resample(self, AudioFrame)

View file

@ -0,0 +1,122 @@
from errno import EAGAIN
import cython
from cython.cimports.av.filter.context import FilterContext
from cython.cimports.av.filter.graph import Graph
from av.error import FFmpegError
@cython.cclass
class AudioResampler:
"""AudioResampler(format=None, layout=None, rate=None)
:param AudioFormat format: The target format, or string that parses to one
(e.g. ``"s16"``).
:param AudioLayout layout: The target layout, or an int/string that parses
to one (e.g. ``"stereo"``).
:param int rate: The target sample rate.
"""
def __cinit__(self, format=None, layout=None, rate=None, frame_size=None):
if format is not None:
self.format = (
format if isinstance(format, AudioFormat) else AudioFormat(format)
)
if layout is not None:
self.layout = AudioLayout(layout)
self.rate = int(rate) if rate else 0
self.frame_size = int(frame_size) if frame_size else 0
self.graph = None
@cython.ccall
def resample(self, frame: AudioFrame | None) -> list:
"""resample(frame)
Convert the ``sample_rate``, ``channel_layout`` and/or ``format`` of
a :class:`~.AudioFrame`.
:param AudioFrame frame: The frame to convert or `None` to flush.
:returns: A list of :class:`AudioFrame` in new parameters. If the nothing is to be done return the same frame
as a single element list.
"""
# We don't have any input, so don't bother even setting up.
if not self.graph and frame is None:
return []
# Shortcut for passthrough.
if self.is_passthrough:
return [frame]
# Take source settings from the first frame.
if not self.graph:
self.template = frame
# Set some default descriptors.
self.format = self.format or frame.format
self.layout = self.layout or frame.layout
self.rate = self.rate or frame.sample_rate
# Check if we can passthrough or if there is actually work to do.
if (
frame.format.sample_fmt == self.format.sample_fmt
and frame.layout == self.layout
and frame.sample_rate == self.rate
and self.frame_size == 0
):
self.is_passthrough = True
return [frame]
# handle resampling with aformat filter
# (similar to configure_output_audio_filter from ffmpeg)
self.graph = Graph()
extra_args = {}
if frame.time_base is not None:
extra_args["time_base"] = f"{frame.time_base}"
abuffer = self.graph.add(
"abuffer",
sample_rate=f"{frame.sample_rate}",
sample_fmt=AudioFormat(frame.format).name,
channel_layout=frame.layout.name,
**extra_args,
)
aformat = self.graph.add(
"aformat",
sample_rates=f"{self.rate}",
sample_fmts=self.format.name,
channel_layouts=self.layout.name,
)
abuffersink = self.graph.add("abuffersink")
abuffer.link_to(aformat)
aformat.link_to(abuffersink)
self.graph.configure()
if self.frame_size > 0:
self.graph.set_audio_frame_size(self.frame_size)
if frame is not None:
if (
frame.format.sample_fmt != self.template.format.sample_fmt
or frame.layout != self.template.layout
or frame.sample_rate != self.template.rate
):
raise ValueError("Frame does not match AudioResampler setup.")
self.graph.push(frame)
output: list = []
while True:
try:
output.append(self.graph.pull())
except EOFError:
break
except FFmpegError as e:
if e.errno != EAGAIN:
raise
break
return output

View file

@ -0,0 +1,20 @@
from av.filter.graph import Graph
from .format import AudioFormat
from .frame import AudioFrame
from .layout import AudioLayout
class AudioResampler:
rate: int
frame_size: int
format: AudioFormat
graph: Graph | None
def __init__(
self,
format: str | int | AudioFormat | None = None,
layout: str | int | AudioLayout | None = None,
rate: int | None = None,
frame_size: int | None = None,
) -> None: ...
def resample(self, frame: AudioFrame | None) -> list[AudioFrame]: ...

View file

@ -0,0 +1,9 @@
from av.packet cimport Packet
from av.stream cimport Stream
from .frame cimport AudioFrame
cdef class AudioStream(Stream):
cpdef encode(self, AudioFrame frame=?)
cpdef decode(self, Packet packet=?)

View file

@ -0,0 +1,46 @@
import cython
from cython.cimports.av.audio.frame import AudioFrame
from cython.cimports.av.packet import Packet
@cython.cclass
class AudioStream(Stream):
def __repr__(self):
form = self.format.name if self.format else None
return (
f"<av.AudioStream #{self.index} {self.name} at {self.rate}Hz,"
f" {self.layout.name}, {form} at 0x{id(self):x}>"
)
def __getattr__(self, name):
return getattr(self.codec_context, name)
@cython.ccall
def encode(self, frame: AudioFrame | None = None):
"""
Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`.
:rtype: list[Packet]
.. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`.
"""
packets = self.codec_context.encode(frame)
packet: Packet
for packet in packets:
packet._stream = self
packet.ptr.stream_index = self.ptr.index
return packets
@cython.ccall
def decode(self, packet: Packet | None = None):
"""
Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`.
:rtype: list[AudioFrame]
.. seealso:: This is a passthrough to :meth:`.CodecContext.decode`.
"""
return self.codec_context.decode(packet)

View file

@ -0,0 +1,32 @@
from typing import Literal
from av.packet import Packet
from av.stream import Stream
from .codeccontext import AudioCodecContext
from .format import AudioFormat
from .frame import AudioFrame
from .layout import AudioLayout
class _Format:
def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ...
def __set__(self, instance: object, value: AudioFormat | str) -> None: ...
class _Layout:
def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ...
def __set__(self, instance: object, value: AudioLayout | str) -> None: ...
class AudioStream(Stream):
codec_context: AudioCodecContext
def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ...
def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ...
# From codec context
frame_size: int
sample_rate: int
bit_rate: int
rate: int
channels: int
type: Literal["audio"]
format: _Format
layout: _Layout