541 lines
19 KiB
Python
541 lines
19 KiB
Python
# ----------------------------------------------------------------------------
|
|
# pyglet
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
|
# Copyright (c) 2008-2021 pyglet contributors
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in
|
|
# the documentation and/or other materials provided with the
|
|
# distribution.
|
|
# * Neither the name of pyglet nor the names of its
|
|
# contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written
|
|
# permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
# ----------------------------------------------------------------------------
|
|
|
|
import os
|
|
import math
|
|
import struct
|
|
import random
|
|
import ctypes
|
|
|
|
from .codecs.base import Source, AudioFormat, AudioData
|
|
|
|
from collections import deque
|
|
|
|
|
|
class Envelope:
|
|
"""Base class for SynthesisSource amplitude envelopes."""
|
|
def get_generator(self, sample_rate, duration):
|
|
raise NotImplementedError
|
|
|
|
|
|
class FlatEnvelope(Envelope):
|
|
"""A flat envelope, providing basic amplitude setting.
|
|
|
|
:Parameters:
|
|
`amplitude` : float
|
|
The amplitude (volume) of the wave, from 0.0 to 1.0.
|
|
Values outside of this range will be clamped.
|
|
"""
|
|
def __init__(self, amplitude=0.5):
|
|
self.amplitude = max(min(1.0, amplitude), 0)
|
|
|
|
def get_generator(self, sample_rate, duration):
|
|
amplitude = self.amplitude
|
|
while True:
|
|
yield amplitude
|
|
|
|
|
|
class LinearDecayEnvelope(Envelope):
|
|
"""A linearly decaying envelope.
|
|
|
|
This envelope linearly decays the amplitude from the peak value
|
|
to 0, over the length of the waveform.
|
|
|
|
:Parameters:
|
|
`peak` : float
|
|
The Initial peak value of the envelope, from 0.0 to 1.0.
|
|
Values outside of this range will be clamped.
|
|
"""
|
|
def __init__(self, peak=1.0):
|
|
self.peak = max(min(1.0, peak), 0)
|
|
|
|
def get_generator(self, sample_rate, duration):
|
|
peak = self.peak
|
|
total_bytes = int(sample_rate * duration)
|
|
for i in range(total_bytes):
|
|
yield (total_bytes - i) / total_bytes * peak
|
|
|
|
|
|
class ADSREnvelope(Envelope):
|
|
"""A four part Attack, Decay, Suspend, Release envelope.
|
|
|
|
This is a four part ADSR envelope. The attack, decay, and release
|
|
parameters should be provided in seconds. For example, a value of
|
|
0.1 would be 100ms. The sustain_amplitude parameter affects the
|
|
sustain volume. This defaults to a value of 0.5, but can be provided
|
|
on a scale from 0.0 to 1.0.
|
|
|
|
:Parameters:
|
|
`attack` : float
|
|
The attack time, in seconds.
|
|
`decay` : float
|
|
The decay time, in seconds.
|
|
`release` : float
|
|
The release time, in seconds.
|
|
`sustain_amplitude` : float
|
|
The sustain amplitude (volume), from 0.0 to 1.0.
|
|
"""
|
|
def __init__(self, attack, decay, release, sustain_amplitude=0.5):
|
|
self.attack = attack
|
|
self.decay = decay
|
|
self.release = release
|
|
self.sustain_amplitude = max(min(1.0, sustain_amplitude), 0)
|
|
|
|
def get_generator(self, sample_rate, duration):
|
|
sustain_amplitude = self.sustain_amplitude
|
|
total_bytes = int(sample_rate * duration)
|
|
attack_bytes = int(sample_rate * self.attack)
|
|
decay_bytes = int(sample_rate * self.decay)
|
|
release_bytes = int(sample_rate * self.release)
|
|
sustain_bytes = total_bytes - attack_bytes - decay_bytes - release_bytes
|
|
decay_step = (1 - sustain_amplitude) / decay_bytes
|
|
release_step = sustain_amplitude / release_bytes
|
|
for i in range(1, attack_bytes + 1):
|
|
yield i / attack_bytes
|
|
for i in range(1, decay_bytes + 1):
|
|
yield 1 - (i * decay_step)
|
|
for i in range(1, sustain_bytes + 1):
|
|
yield sustain_amplitude
|
|
for i in range(1, release_bytes + 1):
|
|
yield sustain_amplitude - (i * release_step)
|
|
|
|
|
|
class TremoloEnvelope(Envelope):
|
|
"""A tremolo envelope, for modulation amplitude.
|
|
|
|
A tremolo envelope that modulates the amplitude of the
|
|
waveform with a sinusoidal pattern. The depth and rate
|
|
of modulation can be specified. Depth is calculated as
|
|
a percentage of the maximum amplitude. For example:
|
|
a depth of 0.2 and amplitude of 0.5 will fluctuate
|
|
the amplitude between 0.4 an 0.5.
|
|
|
|
:Parameters:
|
|
`depth` : float
|
|
The amount of fluctuation, from 0.0 to 1.0.
|
|
`rate` : float
|
|
The fluctuation frequency, in seconds.
|
|
`amplitude` : float
|
|
The peak amplitude (volume), from 0.0 to 1.0.
|
|
"""
|
|
def __init__(self, depth, rate, amplitude=0.5):
|
|
self.depth = max(min(1.0, depth), 0)
|
|
self.rate = rate
|
|
self.amplitude = max(min(1.0, amplitude), 0)
|
|
|
|
def get_generator(self, sample_rate, duration):
|
|
total_bytes = int(sample_rate * duration)
|
|
period = total_bytes / duration
|
|
max_amplitude = self.amplitude
|
|
min_amplitude = max(0.0, (1.0 - self.depth) * self.amplitude)
|
|
step = (math.pi * 2) / period / self.rate
|
|
for i in range(total_bytes):
|
|
value = math.sin(step * i)
|
|
yield value * (max_amplitude - min_amplitude) + min_amplitude
|
|
|
|
|
|
class SynthesisSource(Source):
|
|
"""Base class for synthesized waveforms.
|
|
|
|
:Parameters:
|
|
`duration` : float
|
|
The length, in seconds, of audio that you wish to generate.
|
|
`sample_rate` : int
|
|
Audio samples per second. (CD quality is 44100).
|
|
`sample_size` : int
|
|
The bit precision. Must be either 8 or 16.
|
|
"""
|
|
def __init__(self, duration, sample_rate=44800, sample_size=16, envelope=None):
|
|
|
|
self._duration = float(duration)
|
|
self.audio_format = AudioFormat(
|
|
channels=1,
|
|
sample_size=sample_size,
|
|
sample_rate=sample_rate)
|
|
|
|
self._offset = 0
|
|
self._sample_rate = sample_rate
|
|
self._sample_size = sample_size
|
|
self._bytes_per_sample = sample_size >> 3
|
|
self._bytes_per_second = self._bytes_per_sample * sample_rate
|
|
self._max_offset = int(self._bytes_per_second * self._duration)
|
|
self.envelope = envelope or FlatEnvelope(amplitude=1.0)
|
|
self._envelope_generator = self.envelope.get_generator(sample_rate, duration)
|
|
|
|
if self._bytes_per_sample == 2:
|
|
self._max_offset &= 0xfffffffe
|
|
|
|
def get_audio_data(self, num_bytes, compensation_time=0.0):
|
|
"""Return `num_bytes` bytes of audio data."""
|
|
num_bytes = min(num_bytes, self._max_offset - self._offset)
|
|
if num_bytes <= 0:
|
|
return None
|
|
|
|
timestamp = float(self._offset) / self._bytes_per_second
|
|
duration = float(num_bytes) / self._bytes_per_second
|
|
data = self._generate_data(num_bytes)
|
|
self._offset += num_bytes
|
|
|
|
return AudioData(data, num_bytes, timestamp, duration, [])
|
|
|
|
def _generate_data(self, num_bytes):
|
|
"""Generate `num_bytes` bytes of data.
|
|
|
|
Return data as ctypes array or string.
|
|
"""
|
|
raise NotImplementedError('abstract')
|
|
|
|
def seek(self, timestamp):
|
|
self._offset = int(timestamp * self._bytes_per_second)
|
|
|
|
# Bound within duration
|
|
self._offset = min(max(self._offset, 0), self._max_offset)
|
|
|
|
# Align to sample
|
|
if self._bytes_per_sample == 2:
|
|
self._offset &= 0xfffffffe
|
|
|
|
self._envelope_generator = self.envelope.get_generator(self._sample_rate, self._duration)
|
|
|
|
def save(self, filename):
|
|
"""Save the audio to disk as a standard RIFF Wave.
|
|
|
|
A standard RIFF wave header will be added to the raw PCM
|
|
audio data when it is saved to disk.
|
|
|
|
:Parameters:
|
|
`filename` : str
|
|
The file name to save as.
|
|
|
|
"""
|
|
self.seek(0)
|
|
data = self.get_audio_data(self._max_offset).get_string_data()
|
|
header = struct.pack('<4sI8sIHHIIHH4sI',
|
|
b"RIFF",
|
|
len(data) + 44 - 8,
|
|
b"WAVEfmt ",
|
|
16, # Default for PCM
|
|
1, # Default for PCM
|
|
1, # Number of channels
|
|
self._sample_rate,
|
|
self._bytes_per_second,
|
|
self._bytes_per_sample,
|
|
self._sample_size,
|
|
b"data",
|
|
len(data))
|
|
|
|
with open(filename, "wb") as f:
|
|
f.write(header)
|
|
f.write(data)
|
|
|
|
|
|
class Silence(SynthesisSource):
|
|
"""A silent waveform."""
|
|
|
|
def _generate_data(self, num_bytes):
|
|
if self._bytes_per_sample == 1:
|
|
return b'\127' * num_bytes
|
|
else:
|
|
return b'\0' * num_bytes
|
|
|
|
|
|
class WhiteNoise(SynthesisSource):
|
|
"""A white noise, random waveform."""
|
|
|
|
def _generate_data(self, num_bytes):
|
|
return os.urandom(num_bytes)
|
|
|
|
|
|
class Sine(SynthesisSource):
|
|
"""A sinusoid (sine) waveform.
|
|
|
|
:Parameters:
|
|
`duration` : float
|
|
The length, in seconds, of audio that you wish to generate.
|
|
`frequency` : int
|
|
The frequency, in Hz of the waveform you wish to produce.
|
|
`sample_rate` : int
|
|
Audio samples per second. (CD quality is 44100).
|
|
`sample_size` : int
|
|
The bit precision. Must be either 8 or 16.
|
|
"""
|
|
def __init__(self, duration, frequency=440, **kwargs):
|
|
super(Sine, self).__init__(duration, **kwargs)
|
|
self.frequency = frequency
|
|
|
|
def _generate_data(self, num_bytes):
|
|
if self._bytes_per_sample == 1:
|
|
samples = num_bytes
|
|
bias = 127
|
|
amplitude = 127
|
|
data = (ctypes.c_ubyte * samples)()
|
|
else:
|
|
samples = num_bytes >> 1
|
|
bias = 0
|
|
amplitude = 32767
|
|
data = (ctypes.c_short * samples)()
|
|
step = self.frequency * (math.pi * 2) / self.audio_format.sample_rate
|
|
envelope = self._envelope_generator
|
|
for i in range(samples):
|
|
data[i] = int(math.sin(step * i) * amplitude * next(envelope) + bias)
|
|
return bytes(data)
|
|
|
|
|
|
class Triangle(SynthesisSource):
|
|
"""A triangle waveform.
|
|
|
|
:Parameters:
|
|
`duration` : float
|
|
The length, in seconds, of audio that you wish to generate.
|
|
`frequency` : int
|
|
The frequency, in Hz of the waveform you wish to produce.
|
|
`sample_rate` : int
|
|
Audio samples per second. (CD quality is 44100).
|
|
`sample_size` : int
|
|
The bit precision. Must be either 8 or 16.
|
|
"""
|
|
def __init__(self, duration, frequency=440, **kwargs):
|
|
super(Triangle, self).__init__(duration, **kwargs)
|
|
self.frequency = frequency
|
|
|
|
def _generate_data(self, num_bytes):
|
|
if self._bytes_per_sample == 1:
|
|
samples = num_bytes
|
|
value = 127
|
|
maximum = 255
|
|
minimum = 0
|
|
data = (ctypes.c_ubyte * samples)()
|
|
else:
|
|
samples = num_bytes >> 1
|
|
value = 0
|
|
maximum = 32767
|
|
minimum = -32768
|
|
data = (ctypes.c_short * samples)()
|
|
step = (maximum - minimum) * 2 * self.frequency / self.audio_format.sample_rate
|
|
envelope = self._envelope_generator
|
|
for i in range(samples):
|
|
value += step
|
|
if value > maximum:
|
|
value = maximum - (value - maximum)
|
|
step = -step
|
|
if value < minimum:
|
|
value = minimum - (value - minimum)
|
|
step = -step
|
|
data[i] = int(value * next(envelope))
|
|
return bytes(data)
|
|
|
|
|
|
class Sawtooth(SynthesisSource):
|
|
"""A sawtooth waveform.
|
|
|
|
:Parameters:
|
|
`duration` : float
|
|
The length, in seconds, of audio that you wish to generate.
|
|
`frequency` : int
|
|
The frequency, in Hz of the waveform you wish to produce.
|
|
`sample_rate` : int
|
|
Audio samples per second. (CD quality is 44100).
|
|
`sample_size` : int
|
|
The bit precision. Must be either 8 or 16.
|
|
"""
|
|
def __init__(self, duration, frequency=440, **kwargs):
|
|
super(Sawtooth, self).__init__(duration, **kwargs)
|
|
self.frequency = frequency
|
|
|
|
def _generate_data(self, num_bytes):
|
|
if self._bytes_per_sample == 1:
|
|
samples = num_bytes
|
|
value = 127
|
|
maximum = 255
|
|
minimum = 0
|
|
data = (ctypes.c_ubyte * samples)()
|
|
else:
|
|
samples = num_bytes >> 1
|
|
value = 0
|
|
maximum = 32767
|
|
minimum = -32768
|
|
data = (ctypes.c_short * samples)()
|
|
step = (maximum - minimum) * self.frequency / self._sample_rate
|
|
envelope = self._envelope_generator
|
|
for i in range(samples):
|
|
value += step
|
|
if value > maximum:
|
|
value = minimum + (value % maximum)
|
|
data[i] = int(value * next(envelope))
|
|
return bytes(data)
|
|
|
|
|
|
class Square(SynthesisSource):
|
|
"""A square (pulse) waveform.
|
|
|
|
:Parameters:
|
|
`duration` : float
|
|
The length, in seconds, of audio that you wish to generate.
|
|
`frequency` : int
|
|
The frequency, in Hz of the waveform you wish to produce.
|
|
`sample_rate` : int
|
|
Audio samples per second. (CD quality is 44100).
|
|
`sample_size` : int
|
|
The bit precision. Must be either 8 or 16.
|
|
"""
|
|
def __init__(self, duration, frequency=440, **kwargs):
|
|
|
|
super(Square, self).__init__(duration, **kwargs)
|
|
self.frequency = frequency
|
|
|
|
def _generate_data(self, num_bytes):
|
|
if self._bytes_per_sample == 1:
|
|
samples = num_bytes
|
|
bias = 127
|
|
amplitude = 127
|
|
data = (ctypes.c_ubyte * samples)()
|
|
else:
|
|
samples = num_bytes >> 1
|
|
bias = 0
|
|
amplitude = 32767
|
|
data = (ctypes.c_short * samples)()
|
|
half_period = self.audio_format.sample_rate / self.frequency / 2
|
|
envelope = self._envelope_generator
|
|
value = 1
|
|
count = 0
|
|
for i in range(samples):
|
|
if count >= half_period:
|
|
value = -value
|
|
count %= half_period
|
|
count += 1
|
|
data[i] = int(value * amplitude * next(envelope) + bias)
|
|
return bytes(data)
|
|
|
|
|
|
class FM(SynthesisSource):
|
|
"""A simple FM waveform.
|
|
|
|
This is a simplistic frequency modulated waveform, based on the
|
|
concepts by John Chowning. Basic sine waves are used for both
|
|
frequency carrier and modulator inputs, of which the frequencies can
|
|
be provided. The modulation index, or amplitude, can also be adjusted.
|
|
|
|
:Parameters:
|
|
`duration` : float
|
|
The length, in seconds, of audio that you wish to generate.
|
|
`carrier` : int
|
|
The carrier frequency, in Hz.
|
|
`modulator` : int
|
|
The modulator frequency, in Hz.
|
|
`mod_index` : int
|
|
The modulation index.
|
|
`sample_rate` : int
|
|
Audio samples per second. (CD quality is 44100).
|
|
`sample_size` : int
|
|
The bit precision. Must be either 8 or 16.
|
|
"""
|
|
def __init__(self, duration, carrier=440, modulator=440, mod_index=1, **kwargs):
|
|
super(FM, self).__init__(duration, **kwargs)
|
|
self.carrier = carrier
|
|
self.modulator = modulator
|
|
self.mod_index = mod_index
|
|
|
|
def _generate_data(self, num_bytes):
|
|
if self._bytes_per_sample == 1:
|
|
samples = num_bytes
|
|
bias = 127
|
|
amplitude = 127
|
|
data = (ctypes.c_ubyte * samples)()
|
|
else:
|
|
samples = num_bytes >> 1
|
|
bias = 0
|
|
amplitude = 32767
|
|
data = (ctypes.c_short * samples)()
|
|
car_step = 2 * math.pi * self.carrier
|
|
mod_step = 2 * math.pi * self.modulator
|
|
mod_index = self.mod_index
|
|
sample_rate = self._sample_rate
|
|
envelope = self._envelope_generator
|
|
sin = math.sin
|
|
# FM equation: sin((2 * pi * carrier) + sin(2 * pi * modulator))
|
|
for i in range(samples):
|
|
increment = i / sample_rate
|
|
data[i] = int(sin(car_step * increment + mod_index * sin(mod_step * increment))
|
|
* amplitude * next(envelope) + bias)
|
|
return bytes(data)
|
|
|
|
|
|
class Digitar(SynthesisSource):
|
|
"""A guitar-like waveform.
|
|
|
|
A guitar-like waveform, based on the Karplus-Strong algorithm.
|
|
The sound is similar to a plucked guitar string. The resulting
|
|
sound decays over time, and so the actual length will vary
|
|
depending on the frequency. Lower frequencies require a longer
|
|
`length` parameter to prevent cutting off abruptly.
|
|
|
|
:Parameters:
|
|
`duration` : float
|
|
The length, in seconds, of audio that you wish to generate.
|
|
`frequency` : int
|
|
The frequency, in Hz of the waveform you wish to produce.
|
|
`decay` : float
|
|
The decay rate of the effect. Defaults to 0.996.
|
|
`sample_rate` : int
|
|
Audio samples per second. (CD quality is 44100).
|
|
`sample_size` : int
|
|
The bit precision. Must be either 8 or 16.
|
|
"""
|
|
def __init__(self, duration, frequency=440, decay=0.996, **kwargs):
|
|
super(Digitar, self).__init__(duration, **kwargs)
|
|
self.frequency = frequency
|
|
self.decay = decay
|
|
self.period = int(self._sample_rate / self.frequency)
|
|
|
|
def _generate_data(self, num_bytes):
|
|
if self._bytes_per_sample == 1:
|
|
samples = num_bytes
|
|
bias = 127
|
|
amplitude = 127
|
|
data = (ctypes.c_ubyte * samples)()
|
|
else:
|
|
samples = num_bytes >> 1
|
|
bias = 0
|
|
amplitude = 32767
|
|
data = (ctypes.c_short * samples)()
|
|
random.seed(10)
|
|
period = self.period
|
|
ring_buffer = deque([random.uniform(-1, 1) for _ in range(period)], maxlen=period)
|
|
decay = self.decay
|
|
for i in range(samples):
|
|
data[i] = int(ring_buffer[0] * amplitude + bias)
|
|
ring_buffer.append(decay * (ring_buffer[0] + ring_buffer[1]) / 2)
|
|
return bytes(data)
|