AudiooPy 0.4

https://audioopy.sourceforge.io

Module audioopy

Class AudioFrames

Description

A utility class for audio frames.

Initially based on audioop (2011-2023), this class is self-implemented in 02-2024 due to PEP 594 (dead batteries). Actually, 'audioop' is one of the 19 removed libraries with no proposed alternative.

Example
 >>> frames = b''
 >>> a = AudioFrames(frames, sampwidth=2, nchannels=1)
 >>> a.rms()
 > 3
 >>> a.minmax()
 > (1,5)

Supported sample width is only either 1 (8bits) or 2 (16bits) or 4 (32bits).

Note that operations such as rms() or mul() make no distinction between mono and stereo fragments, i.e. all samples are treated equal. If this is a problem the stereo fragment should be split into two mono fragments first and recombined later.

Constructor

Create an instance.

Parameters
  • frames: (str) input frames
  • sampwidth: (int) sample width of the frames (1, 2 or 4)
  • nchannels: (int) number of channels in the samples
View Source
def __init__(self, frames=b'', sampwidth=2, nchannels=1):
    """Create an instance.

    :param frames: (str) input frames
    :param sampwidth: (int) sample width of the frames (1, 2 or 4)
    :param nchannels: (int) number of channels in the samples

    """
    sampwidth = int(sampwidth)
    if sampwidth not in [1, 2, 4]:
        raise SampleWidthError(sampwidth)
    nchannels = int(nchannels)
    if nchannels < 1:
        raise ChannelIndexError(nchannels)
    bytes_per_frame = sampwidth * nchannels
    if len(frames) % bytes_per_frame != 0:
        raise NumberFramesError(len(frames), sampwidth, nchannels)
    self._frames = frames
    self._sampwidth = sampwidth
    self._nchannels = nchannels

Public functions

get_minval

Return the min value for a given sampwidth.

Parameters
  • size: (int) the sampwidth
  • signed: (bool) if the values will be signed or not
Raises
  • SampleWidthError: Invalid given size.
Returns
  • (int) the min value
View Source
@staticmethod
def get_minval(size, signed=True):
    """Return the min value for a given sampwidth.

        :param size: (int) the sampwidth
        :param signed: (bool) if the values will be signed or not
        :raise: SampleWidthError: Invalid given size.
        :return: (int) the min value

        """
    if not signed:
        return 0
    elif size == 1:
        return -128
    elif size == 2:
        return -32768
    elif size == 4:
        return -2147483648
    raise SampleWidthError(size)

get_maxval

Return the max value for a given sampwidth.

Parameters
  • size: (int) the sampwidth
  • signed: (bool) if the values will be signed or not
Raises
  • SampleWidthError: Invalid given size.
Returns
  • (int) the max value
View Source
@staticmethod
def get_maxval(size, signed=True):
    """Return the max value for a given sampwidth.

        :param size: (int) the sampwidth
        :param signed: (bool) if the values will be signed or not
        :raise: SampleWidthError: Invalid given size.
        :return: (int) the max value

        """
    if signed and size == 1:
        return 127
    elif size == 1:
        return 255
    elif signed and size == 2:
        return 32767
    elif size == 2:
        return 65535
    elif signed and size == 4:
        return 2147483647
    elif size == 4:
        return 4294967295
    raise SampleWidthError(size)

get_nchannels

Return the number of channels in the frames.

View Source
def get_nchannels(self):
    """Return the number of channels in the frames."""
    return self._nchannels

get_sampwidth

Return the size of the frames in (1, 2, 4).

View Source
def get_sampwidth(self):
    """Return the size of the frames in (1, 2, 4)."""
    return self._sampwidth

get_sample

Return the value of given sample index.

Parameters
  • i: (int) index of the sample to get value
Returns
  • (int) value
View Source
def get_sample(self, i):
    """Return the value of given sample index.

        :param i: (int) index of the sample to get value
        :return: (int) value

        """
    start = i * self._sampwidth
    return self.__frame_to_sample(self._frames[start:start + self._sampwidth])

minmax

Return the minimum and maximum values of all samples in the frames.

Returns
  • (int, int) Min and max amplitude values, or (0,0) if empty frames.
View Source
def minmax(self):
    """Return the minimum and maximum values of all samples in the frames.

        :return: (int, int) Min and max amplitude values, or (0,0) if empty frames.

        """
    if len(self._frames) > 0:
        val_min = self.get_maxval(self._sampwidth)
        val_max = self.get_minval(self._sampwidth)
        for i in range(len(self._frames) // self._sampwidth):
            val = self.get_sample(i)
            if val > val_max:
                val_max = val
            if val < val_min:
                val_min = val
        return (val_min, val_max)
    return (0, 0)

min

Return the minimum of the values of all samples in the frames.

View Source
def min(self):
    """Return the minimum of the values of all samples in the frames."""
    return self.minmax()[0]

max

Return the maximum of the values of all samples in the frames.

View Source
def max(self):
    """Return the maximum of the values of all samples in the frames."""
    return self.minmax()[1]

absmax

Return the maximum of the *absolute value of all samples in the frames.*

View Source
def absmax(self):
    """Return the maximum of the *absolute value* of all samples in the frames."""
    val_min, val_max = self.minmax()
    return max(abs(val_min), abs(val_max))

avg

Return the average over all samples in the frames.

Returns
  • (float) Average value rounded to 2 digits.
View Source
def avg(self):
    """Return the average over all samples in the frames.

        :return: (float) Average value rounded to 2 digits.

        """
    if len(self._frames) == 0:
        return 0
    samples_sum = 0.0
    nb_samples = len(self._frames) / self._sampwidth
    for i in range(int(nb_samples)):
        samples_sum += self.get_sample(i)
    return round(samples_sum / nb_samples, 2)

rms

Return the root-mean-square of the frames.

Returns
  • (float) sqrt(sum(S_i^2) / n) rounded to 2 digits
View Source
def rms(self):
    """Return the root-mean-square of the frames.

        :return: (float) sqrt(sum(S_i^2) / n) rounded to 2 digits

        """
    if len(self._frames) == 0:
        return 0.0
    square_sum = 0.0
    nb_samples = len(self._frames) / self._sampwidth
    for i in range(int(nb_samples)):
        val = self.get_sample(i)
        square_sum += val * val
    return round(math.sqrt(square_sum / nb_samples), 2)

cross

Return the number of zero crossings in frames.

Returns
  • (int) Number of zero crossing or -1 if empty frames
View Source
def cross(self):
    """Return the number of zero crossings in frames.

        :return: (int) Number of zero crossing or -1 if empty frames

        """
    n_cross = -1
    prev_val = 17
    for i in range(len(self._frames) // self._sampwidth):
        val = self.get_sample(i) < 0
        if val != prev_val:
            n_cross += 1
        prev_val = val
    return n_cross

clip

Return the number of frames with a value higher than the one of the factor.

Parameters
  • factor: (float) All frames outside the interval are clipped.
Returns
  • (int)
View Source
def clip(self, factor=0.5):
    """Return the number of frames with a value higher than the one of the factor.

        :param factor: (float) All frames outside the interval are clipped.
        :return: (int)

        """
    max_val = int(AudioFrames.get_maxval(self._sampwidth) * float(factor))
    min_val = int(AudioFrames.get_minval(self._sampwidth) * float(factor))
    n_clip = 0
    for i in range(len(self._frames) // self._sampwidth):
        val = self.get_sample(i)
        if val >= max_val or val <= min_val:
            n_clip += 1
    return n_clip

clipping_rate

Return the clipping rate of the frames.

Percentage of samples with a value higher than the one corresponding to the factor. Factor has to be between 0 and 1.

Parameters
  • factor: (float) All frames outside the interval are clipped.
Returns
  • (float) clipping rate value. To be multiplied by 100 to get the percentage!
View Source
def clipping_rate(self, factor=0.5):
    """Return the clipping rate of the frames.

        Percentage of samples with a value higher than the one corresponding
        to the factor. Factor has to be between 0 and 1.

        :param factor: (float) All frames outside the interval are clipped.
        :return: (float) clipping rate value. To be multiplied by 100 to get the percentage!

        """
    if len(self._frames) == 0:
        return 0.0
    return float(self.clip(factor)) / (len(self._frames) // self._sampwidth)

get_frames

Return the stored frames.

View Source
def get_frames(self):
    """Return the stored frames."""
    return self._frames

byteswap

Converts big-endian samples to little-endian and vice versa.

View Source
def byteswap(self):
    """Converts big-endian samples to little-endian and vice versa."""
    try:
        import audioop
        audioop.byteswap(self._frames, self._sampwidth)
    except ModuleNotFoundError:
        raise NotImplementedError

findfactor

Return a factor F such that rms(add(mul(reference, -F))) is minimal.

Estimates the factor with which you should multiply reference to make it match as well as possible to the frames.

Parameters
  • reference
View Source
def findfactor(self, reference):
    """Return a factor F such that rms(add(mul(reference, -F))) is minimal.

        Estimates the factor with which you should multiply reference to make
        it match as well as possible to the frames.

        """
    try:
        import audioop
        audioop.findfactor(self._frames, reference)
    except ModuleNotFoundError:
        raise NotImplementedError

reverse

Reverse the samples in the frames.

View Source
def reverse(self):
    """Reverse the samples in the frames."""
    try:
        import audioop
        audioop.reverse(self._frames, self._sampwidth)
    except ModuleNotFoundError:
        raise NotImplementedError

mul

Return frames for which all samples are multiplied by factor.

All samples in the original frames are multiplied by the floating-point value factor. Samples are truncated in case of overflow.

Parameters
  • factor: (float) the factor which will be applied to each sample.
Returns
  • (bytes) converted frames
View Source
def mul(self, factor):
    """Return frames for which all samples are multiplied by factor.

        All samples in the original frames are multiplied by the floating-point
        value factor. Samples are truncated in case of overflow.

        :param factor: (float) the factor which will be applied to each sample.
        :return: (bytes) converted frames

        """
    if len(self._frames) == 0:
        return b''
    factor = float(factor)
    val_min = self.get_minval(self._sampwidth)
    val_max = self.get_maxval(self._sampwidth)
    mul_frames = self.__zero_frames()
    for i in range(len(self._frames) // self._sampwidth):
        val = float(self.get_sample(i))
        mul_val = max(val_min, min(val_max, int(val * factor)))
        mul_frames[i] = self.__sample_to_frame(mul_val)
    return b''.join(mul_frames)

bias

Return frames that is the original ones with a bias added to each sample.

All samples in the original frames are summed with the given value. Samples are truncated in case of overflow.

Parameters
  • value: (int) the bias which will be applied to each sample.
Returns
  • (bytes) converted frames
View Source
def bias(self, value):
    """Return frames that is the original ones with a bias added to each sample.

        All samples in the original frames are summed with the given value.
        Samples are truncated in case of overflow.

        :param value: (int) the bias which will be applied to each sample.
        :return: (bytes) converted frames

        """
    if len(self._frames) == 0:
        return b''
    value = int(value)
    val_min = self.get_minval(self._sampwidth)
    val_max = self.get_maxval(self._sampwidth)
    mul_frames = self.__zero_frames()
    for i in range(len(self._frames) // self._sampwidth):
        val = self.get_sample(i)
        mul_val = max(val_min, min(val_max, val + value))
        mul_frames[i] = self.__sample_to_frame(mul_val)
    return b''.join(mul_frames)

change_sampwidth

Return frames with the given number of bytes.

Parameters
  • size: (int) new sample width of the frames. (1 for 8 bits, 2 for 16 bits, 4 for 32 bits)
Returns
  • (bytes) converted frames
View Source
def change_sampwidth(self, size):
    """Return frames with the given number of bytes.

        :param size: (int) new sample width of the frames.
            (1 for 8 bits, 2 for 16 bits, 4 for 32 bits)
        :return: (bytes) converted frames

        """
    if size not in (1, 2, 4):
        raise SampleWidthError(size)
    zero_byte = b'\x00' * size
    size_frames = [zero_byte] * (len(self._frames) // self._sampwidth)
    for i in range(len(self._frames) // self._sampwidth):
        val = self.get_sample(i)
        val = val << (4 - self._sampwidth) * 8
        if size == 2:
            size_frames[i] = struct.pack('<h', val >> 16)
        elif size == 4:
            size_frames[i] = struct.pack('<l', val)
        elif size == 1:
            size_frames[i] = struct.pack('<b', val >> 24)
    return b''.join(size_frames)

resample

Return re-sampled frames with the given new framerate.

Parameters
  • in_rate: (int) The original sample rate of the audio frames
  • out_rate: (int) The desired sample rate to resample the audio frames to
Returns
  • (bytes) the resampled audio frames.
Raises
  • FrameRateError: invalid given in_rate
  • FrameRateError: invalid given out_rate
  • NotImplementedError: can't resample from inrate to outrate
View Source
def resample(self, in_rate, out_rate=16000):
    """Return re-sampled frames with the given new framerate.

        :param in_rate: (int) The original sample rate of the audio frames
        :param out_rate: (int) The desired sample rate to resample the audio frames to
        :return: (bytes) the resampled audio frames.
        :raise: FrameRateError: invalid given in_rate
        :raise: FrameRateError: invalid given out_rate
        :raise: NotImplementedError: can't resample from in_rate to out_rate

        """
    if len(self._frames) == 0:
        return b''
    in_rate = int(in_rate)
    out_rate = int(out_rate)
    if in_rate < 8000:
        raise FrameRateError(in_rate)
    if out_rate < 8000:
        raise FrameRateError(out_rate)
    try:
        frames = self._re_sample(in_rate, out_rate)
        return frames
    except NotImplementedError as e:
        logging.warning("Deprecated. Since 'audioop' will be removed of the Python standard library, re-sampling will not be fully available from Python 3.13. Any help is welcome to implement a self-implemented solution.")
        try:
            import audioop
            return audioop.ratecv(self._frames, self._sampwidth, self._nchannels, in_rate, out_rate, None)[0]
        except ModuleNotFoundError:
            raise e

Private functions

_re_sample

Do re-sampling, or not if in a not implemented condition.

Get actual frames, turn into samples, and reduce into the new expected number of samples. Implemented solution algorithm is:

  1. add "in_n" samples into a buffer from frames;
  2. reduce the buffer to "out_n" samples;
  3. Go to 1. until the end of the frames is reached.
Parameters
  • in_rate: (int) The original sample rate of the audio frames
  • out_rate: (int) The desired sample rate to resample the audio frames to
Returns
  • (bytes) the resampled audio frames.
View Source
def _re_sample(self, in_rate, out_rate):
    """Do re-sampling, or not if in a not implemented condition.

        Get actual frames, turn into samples, and reduce into the new expected
        number of samples. Implemented solution algorithm is:

        1. add "in_n" samples into a buffer from frames;
        2. reduce the buffer to "out_n" samples;
        3. Go to 1. until the end of the frames is reached.

        :param in_rate: (int) The original sample rate of the audio frames
        :param out_rate: (int) The desired sample rate to resample the audio frames to
        :return: (bytes) the resampled audio frames.

        """
    AudioFrames._re_sample_valid_rates(in_rate, out_rate)
    in_nsamples = len(self._frames) // self._sampwidth
    out_nsamples = math.ceil(float(len(self._frames)) / float(self._sampwidth) * float(out_rate) / float(in_rate))
    gcd_rates = gcd(in_rate, out_rate)
    in_n = in_rate // gcd_rates
    in_n = min(in_n, in_nsamples)
    out_n = out_rate // gcd_rates
    out_n = min(out_n, out_nsamples)
    zero_byte = b'\x00' * self._sampwidth
    size_frames = [zero_byte] * out_nsamples
    in_buffer = [0] * in_n
    cur_n = 0
    i = 0
    while i < in_nsamples:
        in_buffer[cur_n] = self.get_sample(i)
        cur_n = cur_n + 1
        i = i + 1
        if cur_n == in_n or i == in_nsamples:
            if cur_n == in_n and in_n % out_n == 0:
                out_buffer = self._down_sample(in_buffer, out_n)
            else:
                while in_n % out_n != 0:
                    in_n += 1
                    in_buffer.append(in_buffer[cur_n - 1])
                while cur_n < in_n:
                    in_buffer[cur_n] = in_buffer[cur_n - 1]
                    cur_n = cur_n + 1
                    i = i + 1
                out_buffer = self._down_sample(in_buffer, out_n)
            for x in range(len(out_buffer)):
                val = out_buffer[x]
                size_frames[i // in_n - 1 + x] = self.__sample_to_frame(val)
            cur_n = 0
    return b''.join(size_frames)

_re_sample_valid_rates

Check if given frame rates are supported for re-sampling.

Raises

NotImplementedError

Parameters
  • in_rate
  • out_rate
View Source
@staticmethod
def _re_sample_valid_rates(in_rate, out_rate):
    """Check if given frame rates are supported for re-sampling.

        :raise: NotImplementedError

        """
    if in_rate < out_rate:
        raise NotImplementedError("re-sample error: Only down-sampling is available. Can't increase framerate from {:d} to {:d}Hz".format(in_rate, out_rate))
    if out_rate != 16000:
        raise NotImplementedError("re-sample error: Only down-sampling to 16kHz is available. Can't decrease framerate from {:d} to {:d}Hz".format(in_rate, out_rate))
    if in_rate not in (192000, 96000, 48000, 32000, 16000):
        raise NotImplementedError("re-sample error: Only down-sampling from 48 or 32kHz is available. Can't decrease framerate from the exotic rate of {:d} to {:d}Hz".format(in_rate, out_rate))

_down_sample

Reduce the given samples to n items.

Down-sample is not applicable if not len(samples) % n.

Parameters
  • samples: (list) List of sample values
  • n: number of expected sample values in the returned list
Returns
  • (list) reduced list of samples
View Source
@staticmethod
def _down_sample(samples, n):
    """Reduce the given samples to n items.

        Down-sample is not applicable if not len(samples) % n.

        :param samples: (list) List of sample values
        :param n: number of expected sample values in the returned list
        :return: (list) reduced list of samples

        """
    if len(samples) % n == 0:
        down = [0] * n
        step = len(samples) // n
        buffer = list()
        cur = 0
        for i in range(len(samples) + 1):
            if len(buffer) == step:
                down[cur] = int(sum(buffer) / len(buffer))
                buffer = list()
                cur = cur + 1
            if i < len(samples):
                buffer.append(samples[i])
    else:
        raise NotImplementedError('Down-sample is not applicable {:d}%{:d} != 0.'.format(len(samples), n))
    return down

Protected functions

__frame_to_sample

Unpack a frame into its sample value.

Make no distinction between mono and stereo frames.

Parameters
  • frame: (bytes) Frames with a length equal to the sample width (1, 2 or 4).
Raises
  • SampleWidthError: if len(frame) != sample width
Returns
  • (int) value or 0
View Source
def __frame_to_sample(self, frame):
    """Unpack a frame into its sample value.

        Make no distinction between mono and stereo frames.

        :param frame: (bytes) Frames with a length equal to the sample width (1, 2 or 4).
        :raise: SampleWidthError: if len(frame) != sample width
        :return: (int) value or 0

        """
    if self._sampwidth == 2 and len(frame) == 2:
        return struct.unpack('<%uh' % 1, frame)[0]
    elif self._sampwidth == 1 and len(frame) == 1:
        return struct.unpack('<%ub' % 1, frame)[0]
    elif self._sampwidth == 4 and len(frame) == 4:
        return struct.unpack('<%ul' % 1, frame)[0]
    raise SampleWidthError(len(frame))

__sample_to_frame

Pack a sample value into a frame.

Make no distinction between mono and stereo frames.

Parameters
  • sample: Sample value
Returns
  • (bytes) Frame
View Source
def __sample_to_frame(self, sample):
    """Pack a sample value into a frame.

        Make no distinction between mono and stereo frames.

        :param sample: Sample value
        :return: (bytes) Frame

        """
    if self._sampwidth == 2:
        return struct.pack('<h', sample)
    elif self._sampwidth == 4:
        return struct.pack('<l', sample)
    elif self._sampwidth == 1:
        return struct.pack('<b', sample)
    return b''

__zero_frames

Return a list of frames with 0 bytes values.

View Source
def __zero_frames(self):
    """Return a list of frames with 0 bytes values."""
    zero_byte = b'\x00' * self._sampwidth
    return [zero_byte] * (len(self._frames) // self._sampwidth)