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 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
|
STANDARD_A_FREQUENCY = 440.0
|
||||||
|
@ -11,11 +19,12 @@ class Sound(object):
|
||||||
|
|
||||||
midi_note = None
|
midi_note = None
|
||||||
frequency = None
|
frequency = None
|
||||||
|
phase = 0.0
|
||||||
gain = 1.0
|
gain = 1.0
|
||||||
duration = None
|
duration = None
|
||||||
|
|
||||||
def __init__(self, midi_note=midi_note, frequency=None, gain=gain,
|
def __init__(self, midi_note=midi_note, frequency=None, phase=phase,
|
||||||
duration=duration, A_frequency=STANDARD_A_FREQUENCY):
|
gain=gain, duration=duration, A_frequency=STANDARD_A_FREQUENCY):
|
||||||
"""
|
"""
|
||||||
You can construct a sound either from a MIDI note or a base 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
|
:param frequency: Sound base frequency in Hz
|
||||||
:type frequency: float
|
: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)
|
:param gain: Note gain/volume between 0.0 and 1.0 (default: 1.0)
|
||||||
:type gain: float
|
:type gain: float
|
||||||
|
|
||||||
|
@ -53,6 +65,7 @@ class Sound(object):
|
||||||
raise RuntimeError('Please specify either a MIDI note or a base ' +
|
raise RuntimeError('Please specify either a MIDI note or a base ' +
|
||||||
'frequency')
|
'frequency')
|
||||||
|
|
||||||
|
self.phase = phase
|
||||||
self.gain = gain
|
self.gain = gain
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
|
|
||||||
|
@ -92,10 +105,10 @@ class Sound(object):
|
||||||
"""
|
"""
|
||||||
Get the wave binary data associated to this sound
|
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
|
: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
|
:type t_end: float
|
||||||
|
|
||||||
:param samplerate: Audio sample rate. Default: 44100 Hz
|
: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 = np.linspace(t_start, t_end, int((t_end-t_start)*samplerate))
|
||||||
|
|
||||||
x = x.reshape(len(x), 1)
|
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):
|
def __str__(self):
|
||||||
import json
|
return json.dumps(dict(self))
|
||||||
|
|
||||||
return json.dumps({
|
|
||||||
'midi_note': midi_note,
|
|
||||||
'frequency': frequency,
|
|
||||||
'gain': gain,
|
|
||||||
'duration': duration,
|
|
||||||
})
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, *args, **kwargs):
|
def build(cls, *args, **kwargs):
|
||||||
|
@ -128,8 +140,6 @@ class Sound(object):
|
||||||
key-value representation
|
key-value representation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
if args:
|
if args:
|
||||||
if isinstance(args[0], cls):
|
if isinstance(args[0], cls):
|
||||||
return args[0]
|
return args[0]
|
||||||
|
@ -143,4 +153,84 @@ class Sound(object):
|
||||||
raise RuntimeError('Usage: {}'.format(__doc__))
|
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:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
Loading…
Reference in a new issue