soundbox/soundbox.py

202 lines
5.0 KiB
Python
Raw Permalink Normal View History

2020-12-17 22:08:07 +00:00
import wave
import numpy as np
import math
# Nombre doctets par échantillon
samp_width = 2
# Valeur maximale dun échantillon
max_val = 2 ** (8 * samp_width - 1) - 1
# Fréquence déchantillonnage (Hertz)
samp_rate = 44100
2020-12-19 20:04:56 +00:00
def silence(dur):
"""
Génère un signal silencieux.
2020-12-17 22:08:07 +00:00
2020-12-19 20:04:56 +00:00
Paramètres:
dur (float): Durée en secondes
2020-12-17 22:08:07 +00:00
2020-12-19 20:04:56 +00:00
Retourne:
(ndarray): Signal généré
"""
2020-12-17 22:08:07 +00:00
return np.zeros((int(samp_rate * dur),))
def sine(dur, freq, value=1):
2020-12-19 20:04:56 +00:00
"""
Génère un signal sinusoïdal.
Paramètres:
dur (float): Durée en secondes
freq (float): Fréquence de la sinusoïde en hertz
value (float): Amplitude du signal (valeur relative entre 0 et 1)
Retourne:
(ndarray): Signal généré
"""
2020-12-17 22:08:07 +00:00
x = np.arange(int(samp_rate * dur))
return value * max_val * np.sin(2 * np.pi * freq * x / samp_rate)
def envelope(attack, decay, release, signal):
2020-12-19 20:04:56 +00:00
"""
Applique une enveloppe à une note.
Paramètres:
attack (float): Temps dattaque de la note (en secondes)
decay (float): Temps de chute de la note vers la phase
de maintien (en secondes)
release (float): Temps de relâche à la fin de la note (en secondes)
signal (ndarray): Signal original
Retourne:
(ndarray): Signal généré
"""
2020-12-17 22:08:07 +00:00
total = len(signal)
2020-12-19 20:04:56 +00:00
attack = int(attack * samp_rate)
decay = int(decay * samp_rate)
release = int(release * samp_rate)
if attack + decay + release > total:
raise ValueError('Note trop courte pour lapplication de lenveloppe')
2020-12-17 22:08:07 +00:00
sustain = total - attack - decay - release
return signal * np.concatenate((
np.linspace(start=0, stop=1, num=attack, endpoint=False),
np.linspace(start=1, stop=2/3, num=decay, endpoint=False),
np.linspace(start=2/3, stop=2/3, num=sustain, endpoint=False),
np.linspace(start=2/3, stop=0, num=release, endpoint=True),
))
2020-12-19 20:04:56 +00:00
def add_signal(dest, start, source):
"""
Ajoute un signal source dans un autre signal.
Paramètres:
dest (ndarray): Signal dans lequel le signal source sera ajouté
start (float): Temps en secondes à partir duquel le signal est ajouté
source (ndarray): Signal source
Retourne: None
"""
dest[int(samp_rate * start):int(samp_rate * start) + len(source)] += source
def chord(instr, dur, freqs, value=1):
"""
Construit un accord de notes.
Paramètres:
instr (function): Instrument à utiliser pour générer les notes
dur (float): Durée de chaque note (en secondes)
freqs (list): Fréquence de chaque note
value (float): Amplitude totale partagée par les notes
Retourne:
(ndarray): Signal généré
"""
signal = np.ndarray(0)
for freq in freqs:
new_signal = instr(dur, freq, value / len(freqs))
if len(new_signal) > len(signal):
signal.resize(len(new_signal))
signal += new_signal
return signal
2020-12-17 22:08:07 +00:00
notes = {
2020-12-19 20:39:41 +00:00
'si#': 0, 'do': 0,
'reb': 1, 'do#': 1,
2020-12-17 22:08:07 +00:00
're': 2,
2020-12-19 20:39:41 +00:00
'mib': 3, 're#': 3,
'fab': 4, 'mi': 4,
'mi#': 5, 'fa': 5,
'solb': 6, 'fa#': 6,
2020-12-17 22:08:07 +00:00
'sol': 7,
2020-12-19 20:39:41 +00:00
'lab': 8, 'sol#': 8,
2020-12-17 22:08:07 +00:00
'la': 9,
2020-12-19 20:39:41 +00:00
'sib': 10, 'la#': 10,
'dob': 11, 'si': 11,
2020-12-17 22:08:07 +00:00
}
2020-12-19 20:39:41 +00:00
rev_notes = dict(zip(*reversed(list(zip(*notes.items())))))
2020-12-17 22:08:07 +00:00
def note_freq(note, octave):
2020-12-19 20:04:56 +00:00
"""
Calcule la fréquence correspondant à une note.
Paramètres:
note (str): Nom de la note
octave (int): Numéro de loctave
Retourne:
(float): Fréquence correspondante en hertz
"""
2020-12-17 22:08:07 +00:00
return (440
* (2 ** (octave - 3))
* math.pow(2, (notes[note] - 9) / 12))
2020-12-19 20:39:41 +00:00
def freq_note(freq):
"""
Retrouve la note correspondant au mieux à une fréquence.
Paramètres:
note
"""
log = math.log2(freq / note_freq('do', 3))
octave = math.floor(log) + 3
note = round(12 * log - 12 * math.floor(log))
if note == 12:
octave += 1
note = 0
return (rev_notes[note], octave)
2020-12-17 22:08:07 +00:00
def note_freqs(notes):
2020-12-19 20:04:56 +00:00
"""Calcule la fréquence correspondant à un ensemble de notes."""
2020-12-17 22:08:07 +00:00
return list(map(lambda info: note_freq(*info), notes))
def save_signal(out_name, signal):
2020-12-19 20:04:56 +00:00
"""
Écrit un signal dans un fichier au format WAV.
Paramètres:
out_name (str): Chemin vers le fichier
signal (ndarray): Signal à enregistrer
"""
2020-12-17 22:08:07 +00:00
with wave.open(out_name, 'w') as file:
file.setnchannels(1)
file.setsampwidth(samp_width)
file.setframerate(samp_rate)
file.writeframesraw(signal.astype('<h').tostring())
def load_signal(in_name):
2020-12-19 20:04:56 +00:00
"""
Charge un signal depuis un fichier au format WAV.
Paramètres:
in_name (str): Chemin vers le fichier
Retourne:
(ndarray): Signal décodé
"""
2020-12-17 22:08:07 +00:00
with wave.open(in_name, 'r') as file:
assert file.getnchannels() == 1
assert file.getsampwidth() == samp_width
assert file.getframerate() == samp_rate
size = file.getnframes()
return np.ndarray((size,), '<h', file.readframes(size))