soundbox/soundbox.py

202 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
def silence(dur):
"""
Génère un signal silencieux.
Paramètres:
dur (float): Durée en secondes
Retourne:
(ndarray): Signal généré
"""
return np.zeros((int(samp_rate * dur),))
def sine(dur, freq, value=1):
"""
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é
"""
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):
"""
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é
"""
total = len(signal)
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')
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),
))
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
notes = {
'si#': 0, 'do': 0,
'reb': 1, 'do#': 1,
're': 2,
'mib': 3, 're#': 3,
'fab': 4, 'mi': 4,
'mi#': 5, 'fa': 5,
'solb': 6, 'fa#': 6,
'sol': 7,
'lab': 8, 'sol#': 8,
'la': 9,
'sib': 10, 'la#': 10,
'dob': 11, 'si': 11,
}
rev_notes = dict(zip(*reversed(list(zip(*notes.items())))))
def note_freq(note, octave):
"""
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
"""
return (440
* (2 ** (octave - 3))
* math.pow(2, (notes[note] - 9) / 12))
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)
def note_freqs(notes):
"""Calcule la fréquence correspondant à un ensemble de notes."""
return list(map(lambda info: note_freq(*info), notes))
def save_signal(out_name, signal):
"""
Écrit un signal dans un fichier au format WAV.
Paramètres:
out_name (str): Chemin vers le fichier
signal (ndarray): Signal à enregistrer
"""
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):
"""
Charge un signal depuis un fichier au format WAV.
Paramètres:
in_name (str): Chemin vers le fichier
Retourne:
(ndarray): Signal décodé
"""
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))