2023-06-27 13:31:38 +02:00
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
from typing import List, Tuple, Union
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
from ...._utils import convert_nd_array
|
|
|
|
from ._base import SoundBase
|
|
|
|
from ._sound import Sound
|
|
|
|
|
|
|
|
|
|
|
|
class Mix(SoundBase):
|
|
|
|
"""
|
|
|
|
This class models a set of mixed :class:`._sound.Sound` instances that can be played
|
|
|
|
through an audio stream to an audio device
|
|
|
|
"""
|
|
|
|
|
2023-10-23 22:24:49 +02:00
|
|
|
def __init__(self, *sounds, channels: int, dtype, **kwargs):
|
2023-06-27 13:31:38 +02:00
|
|
|
super().__init__(**kwargs)
|
|
|
|
self._sounds: List[Sound] = []
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
self.channels = channels
|
|
|
|
self.dtype = np.dtype(dtype)
|
|
|
|
|
|
|
|
for sound in sounds:
|
|
|
|
self.add(sound)
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
"""
|
|
|
|
Iterate over the object's attributes and return key-pair values.
|
|
|
|
"""
|
|
|
|
for sound in self._sounds:
|
|
|
|
yield dict(sound)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
"""
|
|
|
|
Return a JSON string representation of the object.
|
|
|
|
"""
|
|
|
|
return json.dumps(list(self))
|
|
|
|
|
|
|
|
def add(self, *sounds: Union[Sound, dict]):
|
|
|
|
"""
|
|
|
|
Add one or more sounds to the mix.
|
|
|
|
"""
|
|
|
|
self._sounds += [Sound.build(sound) for sound in sounds]
|
|
|
|
|
|
|
|
def remove(self, *sound_indices: int):
|
|
|
|
"""
|
|
|
|
Remove one or more sounds from the mix.
|
|
|
|
"""
|
|
|
|
assert self._sounds and all(
|
|
|
|
0 <= sound_index < len(sound_indices) for sound_index in sound_indices
|
|
|
|
), f'Sound indices must be between 0 and {len(self._sounds) - 1}'
|
|
|
|
|
|
|
|
for sound_index in sound_indices[::-1]:
|
|
|
|
self._sounds.pop(sound_index)
|
|
|
|
|
|
|
|
def get_wave(
|
|
|
|
self,
|
|
|
|
sample_rate: float,
|
|
|
|
t_start: float = 0,
|
|
|
|
t_end: float = 0,
|
|
|
|
normalize_range: Tuple[float, float] = (-1.0, 1.0),
|
|
|
|
on_clip: str = 'scale',
|
|
|
|
**_,
|
2023-10-23 22:24:49 +02:00
|
|
|
): # -> NDArray[np.number]:
|
2023-06-27 13:31:38 +02:00
|
|
|
wave = None
|
|
|
|
|
|
|
|
for sound in self._sounds:
|
|
|
|
sound_wave = sound.get_wave(
|
|
|
|
t_start=t_start, t_end=t_end, sample_rate=sample_rate
|
|
|
|
)
|
|
|
|
|
|
|
|
if wave is None:
|
|
|
|
wave = sound_wave
|
|
|
|
else:
|
|
|
|
wave += sound_wave
|
|
|
|
|
|
|
|
if wave is not None and len(wave):
|
|
|
|
scale_factor = (normalize_range[1] - normalize_range[0]) / (
|
|
|
|
wave.max() - wave.min()
|
|
|
|
)
|
|
|
|
|
|
|
|
if scale_factor < 1.0: # Wave clipping
|
|
|
|
if on_clip == 'scale':
|
|
|
|
wave = scale_factor * wave
|
|
|
|
elif on_clip == 'clip':
|
|
|
|
wave[wave < normalize_range[0]] = normalize_range[0]
|
|
|
|
wave[wave > normalize_range[1]] = normalize_range[1]
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
'Supported values for "on_clip": ' + '"scale" or "clip"'
|
|
|
|
)
|
|
|
|
|
|
|
|
assert wave is not None
|
|
|
|
return convert_nd_array(self.gain * wave, dtype=self.dtype)
|
|
|
|
|
|
|
|
def duration(self):
|
|
|
|
"""
|
|
|
|
:returns: The duration of the mix in seconds as duration of its longest
|
|
|
|
sample, or None if the mixed sample have no duration set
|
|
|
|
"""
|
|
|
|
|
|
|
|
# If any sound has no duration specified, then the resulting mix will
|
|
|
|
# have no duration as well.
|
|
|
|
if any(sound.duration is None for sound in self._sounds):
|
|
|
|
return None
|
|
|
|
|
|
|
|
return max(((sound.duration or 0) + sound.delay for sound in self._sounds))
|
|
|
|
|
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|