forked from platypush/platypush
Support for mixes of multiple sounds
This commit is contained in:
parent
d89184358a
commit
2fb5e5abc6
1 changed files with 105 additions and 15 deletions
|
@ -1,6 +1,14 @@
|
|||
"""
|
||||
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
|
||||
|
||||
class Sound(object):
|
||||
"""
|
||||
Class model a synthetic sound that can be played through the audio device
|
||||
Models a basic synthetic sound that can be played through an audio device
|
||||
"""
|
||||
|
||||
STANDARD_A_FREQUENCY = 440.0
|
||||
|
@ -11,11 +19,12 @@ class Sound(object):
|
|||
|
||||
midi_note = None
|
||||
frequency = None
|
||||
phase = 0.0
|
||||
gain = 1.0
|
||||
duration = None
|
||||
|
||||
def __init__(self, midi_note=midi_note, frequency=None, gain=gain,
|
||||
duration=duration, A_frequency=STANDARD_A_FREQUENCY):
|
||||
def __init__(self, midi_note=midi_note, frequency=None, phase=phase,
|
||||
gain=gain, duration=duration, A_frequency=STANDARD_A_FREQUENCY):
|
||||
"""
|
||||
You can construct a sound either from a MIDI note or a base frequency
|
||||
|
||||
|
@ -26,6 +35,9 @@ class Sound(object):
|
|||
:param frequency: Sound base frequency in Hz
|
||||
:type frequency: float
|
||||
|
||||
:param phase: Wave phase shift as a multiple of pi (default: 0.0)
|
||||
:type phase: float
|
||||
|
||||
:param gain: Note gain/volume between 0.0 and 1.0 (default: 1.0)
|
||||
:type gain: float
|
||||
|
||||
|
@ -53,6 +65,7 @@ class Sound(object):
|
|||
raise RuntimeError('Please specify either a MIDI note or a base ' +
|
||||
'frequency')
|
||||
|
||||
self.phase = phase
|
||||
self.gain = gain
|
||||
self.duration = duration
|
||||
|
||||
|
@ -92,10 +105,10 @@ class Sound(object):
|
|||
"""
|
||||
Get the wave binary data associated to this sound
|
||||
|
||||
:param t_start: Start offset for the sine wave in seconds. Default: 0
|
||||
:param t_start: Start offset for the wave in seconds. Default: 0
|
||||
:type t_start: float
|
||||
|
||||
:param t_end: End offset for the sine wave in seconds. Default: 0
|
||||
:param t_end: End offset for the wave in seconds. Default: 0
|
||||
:type t_end: float
|
||||
|
||||
:param samplerate: Audio sample rate. Default: 44100 Hz
|
||||
|
@ -108,18 +121,17 @@ class Sound(object):
|
|||
x = np.linspace(t_start, t_end, int((t_end-t_start)*samplerate))
|
||||
|
||||
x = x.reshape(len(x), 1)
|
||||
return self.gain * np.sin(2 * np.pi * self.frequency * x)
|
||||
return self.gain * np.sin((2*np.pi*self.frequency*x) + np.pi*self.phase)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
for attr in ['midi_note', 'frequency', 'gain', 'duration']:
|
||||
yield (attr, getattr(self, attr))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
import json
|
||||
return json.dumps(dict(self))
|
||||
|
||||
return json.dumps({
|
||||
'midi_note': midi_note,
|
||||
'frequency': frequency,
|
||||
'gain': gain,
|
||||
'duration': duration,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def build(cls, *args, **kwargs):
|
||||
|
@ -128,8 +140,6 @@ class Sound(object):
|
|||
key-value representation
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
if args:
|
||||
if isinstance(args[0], cls):
|
||||
return args[0]
|
||||
|
@ -143,4 +153,84 @@ class Sound(object):
|
|||
raise RuntimeError('Usage: {}'.format(__doc__))
|
||||
|
||||
|
||||
class Mix(object):
|
||||
"""
|
||||
This class models a set of mixed :class:`Sound` instances that can be played
|
||||
through an audio stream to an audio device
|
||||
"""
|
||||
|
||||
_sounds = []
|
||||
|
||||
def __init__(self, *sounds):
|
||||
for sound in sounds:
|
||||
self.add(sound)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
for sound in self._sounds:
|
||||
yield dict(sound)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps(list(self))
|
||||
|
||||
|
||||
def add(self, sound):
|
||||
self._sounds.append(Sound.build(sound))
|
||||
|
||||
|
||||
def get_wave(self, t_start=0., t_end=0., normalize_range=(-1.0, 1.0),
|
||||
on_clip='scale', samplerate=Sound._DEFAULT_SAMPLERATE):
|
||||
"""
|
||||
Get the wave binary data associated to this mix
|
||||
|
||||
:param t_start: Start offset for the wave in seconds. Default: 0
|
||||
:type t_start: float
|
||||
|
||||
:param t_end: End offset for the wave in seconds. Default: 0
|
||||
:type t_end: float
|
||||
|
||||
:param normalize_range: Normalization range. If set the gain values of the
|
||||
wave will be normalized to fit into the specified range if it
|
||||
"clips" above or below. Default: ``(-1.0, 1.0)``
|
||||
:type normalize_range: list[float]
|
||||
|
||||
:param on_clip: Action to take on wave clipping if ``normalize_range``
|
||||
is set. Possible values: "``scale``" (scale down the frame to remove
|
||||
the clipping) or "``clip``" (saturate the values above/below range).
|
||||
Default: "``scale``".
|
||||
:param samplerate: Audio sample rate. Default: 44100 Hz
|
||||
:type samplerate: int
|
||||
|
||||
:returns: A numpy.ndarray[n,1] with the raw float values
|
||||
"""
|
||||
|
||||
wave = None
|
||||
|
||||
for sound in self._sounds:
|
||||
sound_wave = sound.get_wave(t_start=t_start, t_end=t_end,
|
||||
samplerate=samplerate)
|
||||
|
||||
if wave is None:
|
||||
wave = sound_wave
|
||||
else:
|
||||
wave += sound_wave
|
||||
|
||||
if normalize_range:
|
||||
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"')
|
||||
|
||||
return wave
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
Loading…
Reference in a new issue