2026-06-16 17:09:34 +00:00
|
|
|
import cython
|
|
|
|
|
from cython.cimports.av.audio.frame import alloc_audio_frame
|
|
|
|
|
from cython.cimports.av.error import err_check
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@cython.final
|
|
|
|
|
@cython.cclass
|
|
|
|
|
class AudioFifo:
|
2026-02-06 22:23:20 +01:00
|
|
|
"""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)
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@cython.ccall
|
|
|
|
|
def write(self, frame: AudioFrame | None):
|
2026-02-06 22:23:20 +01:00
|
|
|
"""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(
|
2026-06-16 17:09:34 +00:00
|
|
|
cython.cast(lib.AVSampleFormat, frame.ptr.format),
|
2026-02-06 22:23:20 +01:00
|
|
|
frame.layout.nb_channels,
|
2026-06-16 17:09:34 +00:00
|
|
|
frame.ptr.nb_samples
|
|
|
|
|
* 2, # Just a default number of samples; it will adjust.
|
2026-02-06 22:23:20 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not self.ptr:
|
|
|
|
|
raise RuntimeError("Could not allocate AVAudioFifo.")
|
|
|
|
|
|
|
|
|
|
# Make sure nothing changed.
|
|
|
|
|
elif (
|
2026-06-16 17:09:34 +00:00
|
|
|
frame.ptr.format != self.template.ptr.format
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
):
|
|
|
|
|
raise ValueError("Frame does not match AudioFifo parameters.")
|
|
|
|
|
|
|
|
|
|
# Assert that the PTS are what we expect.
|
2026-06-16 17:09:34 +00:00
|
|
|
expected_pts = cython.declare(int64_t)
|
2026-02-06 22:23:20 +01:00
|
|
|
if self.pts_per_sample and frame.ptr.pts != lib.AV_NOPTS_VALUE:
|
2026-06-16 17:09:34 +00:00
|
|
|
expected_pts = cython.cast(
|
|
|
|
|
int64_t, self.pts_per_sample * self.samples_written
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
if frame.ptr.pts != expected_pts:
|
|
|
|
|
raise ValueError(
|
2026-06-16 17:09:34 +00:00
|
|
|
"Frame.pts (%d) != expected (%d); fix or set to None."
|
|
|
|
|
% (frame.ptr.pts, expected_pts)
|
2026-02-06 22:23:20 +01:00
|
|
|
)
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
err_check(
|
|
|
|
|
lib.av_audio_fifo_write(
|
|
|
|
|
self.ptr,
|
|
|
|
|
cython.cast(cython.pointer[cython.p_void], frame.ptr.extended_data),
|
|
|
|
|
frame.ptr.nb_samples,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
self.samples_written += frame.ptr.nb_samples
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@cython.ccall
|
|
|
|
|
def read(self, samples: cython.int = 0, partial: cython.bint = False):
|
2026-02-06 22:23:20 +01:00
|
|
|
"""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
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
buffered_samples: cython.int = lib.av_audio_fifo_size(self.ptr)
|
2026-02-06 22:23:20 +01:00
|
|
|
if buffered_samples < 1:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
samples = samples or buffered_samples
|
|
|
|
|
|
|
|
|
|
if buffered_samples < samples:
|
|
|
|
|
if partial:
|
|
|
|
|
samples = buffered_samples
|
|
|
|
|
else:
|
|
|
|
|
return
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
frame: AudioFrame = alloc_audio_frame()
|
2026-02-06 22:23:20 +01:00
|
|
|
frame._copy_internal_attributes(self.template)
|
|
|
|
|
frame._init(
|
2026-06-16 17:09:34 +00:00
|
|
|
cython.cast(lib.AVSampleFormat, self.template.ptr.format),
|
|
|
|
|
cython.cast(lib.AVChannelLayout, self.template.ptr.ch_layout),
|
2026-02-06 22:23:20 +01:00
|
|
|
samples,
|
|
|
|
|
1, # Align?
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
err_check(
|
|
|
|
|
lib.av_audio_fifo_read(
|
|
|
|
|
self.ptr,
|
|
|
|
|
cython.cast(cython.pointer[cython.p_void], frame.ptr.extended_data),
|
|
|
|
|
samples,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
if self.pts_per_sample:
|
2026-06-16 17:09:34 +00:00
|
|
|
frame.ptr.pts = cython.cast(
|
|
|
|
|
uint64_t, self.pts_per_sample * self.samples_read
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
else:
|
|
|
|
|
frame.ptr.pts = lib.AV_NOPTS_VALUE
|
|
|
|
|
|
|
|
|
|
self.samples_read += samples
|
|
|
|
|
return frame
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@cython.ccall
|
|
|
|
|
def read_many(self, samples: cython.int, partial: cython.bint = False):
|
2026-02-06 22:23:20 +01:00
|
|
|
"""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`.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
frame: AudioFrame
|
|
|
|
|
frames: list = []
|
2026-02-06 22:23:20 +01:00
|
|
|
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:
|
2026-06-16 17:09:34 +00:00
|
|
|
raise AttributeError(
|
|
|
|
|
f"'{__name__}.AudioFifo' object has no attribute 'format'"
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
return self.template.format
|
2026-06-16 17:09:34 +00:00
|
|
|
|
2026-02-06 22:23:20 +01:00
|
|
|
@property
|
|
|
|
|
def layout(self):
|
|
|
|
|
"""The :class:`.AudioLayout` of this FIFO."""
|
|
|
|
|
if not self.ptr:
|
2026-06-16 17:09:34 +00:00
|
|
|
raise AttributeError(
|
|
|
|
|
f"'{__name__}.AudioFifo' object has no attribute 'layout'"
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
return self.template.layout
|
2026-06-16 17:09:34 +00:00
|
|
|
|
2026-02-06 22:23:20 +01:00
|
|
|
@property
|
|
|
|
|
def sample_rate(self):
|
|
|
|
|
if not self.ptr:
|
2026-06-16 17:09:34 +00:00
|
|
|
raise AttributeError(
|
|
|
|
|
f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'"
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
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
|