Compare commits
3 Commits
4104126c6e
...
093a341dd2
Author | SHA1 | Date |
---|---|---|
Mattéo Delabre | 093a341dd2 | |
Mattéo Delabre | 575d9b5992 | |
Mattéo Delabre | 7c3679ccaf |
25
README.md
25
README.md
|
@ -1,7 +1,26 @@
|
||||||
<!-- vim: set spelllang=fr: -->
|
<!-- vim: set spelllang=fr: -->
|
||||||
|
|
||||||
# Utilisation d’un sonagramme pour retrouver les notes d’un enregistrement
|
# À la recherche des notes originelles
|
||||||
|
|
||||||
![](fig/synth-single.png)
|
À partir de l’enregistrement numérique d’une interprétation musicale, est-il possible de reconnaître la séquence de notes qui a été jouée sur les différents instruments utilisés ?
|
||||||
|
Cette situation s’apparente à celle où l’on dispose d’une image matricielle rendue à partir d’une image vectorielle et où l’on souhaite retrouver les vecteurs d’origine dont on ne dispose plus — à la différence que nous traitons ici d’un signal 1D au lieu de 2D, et d’un signal sonore plutôt que visuel qui plus est.
|
||||||
|
|
||||||
![](fig/synth-shorttime.png)
|
## Excursion sinusoïdale
|
||||||
|
|
||||||
|
Simplifions d’abord le problème en considérant [un signal composé uniquement de sinusoïdes pures](sounds/synth.wav) (produit par le synthétiseur de fortune qu’est [ce script Python](generate.py)).
|
||||||
|
Cela facilite doublement la tâche, puisque non seulement le signal est totalement exempt de bruit, mais en plus les instruments convoqués n’ont qu’une seule harmonique.
|
||||||
|
Une première approche consiste à étudier le spectre du signal en utilisant une [transformation de Fourier](https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discrète) (générée par [ce script Python](analyze-single.py)).
|
||||||
|
|
||||||
|
![Spectre du son produit par le synthétiseur](fig/synth-single.png)
|
||||||
|
|
||||||
|
Ce spectre permet de lire les différentes fréquences qui composent le son étudié.
|
||||||
|
On y distingue, parmi les fréquences les plus représentées, un *sol₂* (192 Hz), un *do₂* (131 Hz), un *la₂* (220 Hz) et un *la₃* (440 Hz).
|
||||||
|
Une information cruciale manque, celle de l’évolution du signal dans le temps.
|
||||||
|
Une façon de l’obtenir consiste à découper le signal en courtes fenêtres de temps et d’appliquer la transformation de Fourier sur les morceaux obtenus : c’est [la transformée de Fourier à court terme](https://fr.wikipedia.org/wiki/Transform%C3%A9e_de_Fourier_%C3%A0_court_terme).
|
||||||
|
On obtient ainsi un [sonagramme](https://fr.wikipedia.org/wiki/Sonagramme) qui montre l’évolution des fréquences du signal dans le temps (produit par [ce script Python](analyze-shorttime.py)).
|
||||||
|
|
||||||
|
![Évolution dans le temps du spectre du son produit par le synthétiseur](fig/synth-shorttime.png)
|
||||||
|
|
||||||
|
Ce sonagramme permet de distinguer clairement les deux parties du morceau, celle jouée par une sinusoïde au dessus de 250 Hz et celle jouée par une onde carrée en dessous de cette fréquence.
|
||||||
|
On reconnaît également les quatre accords en do majeur joués par la sinusoïde deux fois de suite.
|
||||||
|
Sur cet exemple simple, la transformée de Fourier à court terme est donc suffisante pour extraire les notes, mais qu’en est-il d’un signal plus complexe ?
|
||||||
|
|
|
@ -17,24 +17,29 @@ output_file = sys.argv[2]
|
||||||
|
|
||||||
# Calcul du STFT
|
# Calcul du STFT
|
||||||
signal = soundbox.load_signal(source_file)
|
signal = soundbox.load_signal(source_file)
|
||||||
freq, time, fts = sig.stft(signal, soundbox.samp_rate, nperseg=soundbox.samp_rate * 0.5)
|
freq, time, vecs = sig.stft(signal, soundbox.samp_rate, nperseg=soundbox.samp_rate * 0.5)
|
||||||
|
values = np.absolute(vecs)
|
||||||
|
|
||||||
# Génération du graphe
|
# Génération du graphe
|
||||||
plt.rcParams.update({
|
plt.rcParams.update({
|
||||||
'figure.figsize': (8, 8),
|
'figure.figsize': (10, 8),
|
||||||
'font.size': 16,
|
'figure.frameon': True,
|
||||||
|
'font.size': 20,
|
||||||
'font.family': 'Concourse T4',
|
'font.family': 'Concourse T4',
|
||||||
})
|
})
|
||||||
|
|
||||||
fig, ax = plt.subplots()
|
fig, ax = plt.subplots(frameon=True)
|
||||||
ax.tick_params(axis='both', which='major', labelsize=12)
|
ax.tick_params(axis='both', which='major', labelsize=12)
|
||||||
|
|
||||||
|
freq_filter = values.max(axis=1) / np.max(values) >= 0.01
|
||||||
|
x = np.arange(len(values))
|
||||||
|
|
||||||
ax.pcolormesh(
|
ax.pcolormesh(
|
||||||
time, freq,
|
time, freq[freq_filter], values[freq_filter],
|
||||||
np.abs(fts),
|
cmap='Greys',
|
||||||
cmap='plasma',
|
|
||||||
shading='gouraud')
|
shading='gouraud')
|
||||||
|
|
||||||
|
# Configuration des axes
|
||||||
def time_format(value, pos):
|
def time_format(value, pos):
|
||||||
return f'{value:.0f} s'
|
return f'{value:.0f} s'
|
||||||
|
|
||||||
|
@ -43,14 +48,16 @@ def freq_format(value, pos):
|
||||||
return f'{value:.0f} Hz'
|
return f'{value:.0f} Hz'
|
||||||
|
|
||||||
|
|
||||||
ax.set_xlabel('Temps')
|
ax.set_xlabel('Temps', labelpad=10)
|
||||||
ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(time_format))
|
ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(time_format))
|
||||||
|
|
||||||
ax.set_ylabel('Fréquence')
|
ax.set_ylabel('Fréquence', labelpad=10)
|
||||||
ax.set_ylim(0, 800)
|
ax.set_yscale('log', base=2)
|
||||||
|
ax.yaxis.set_major_locator(plt.MultipleLocator(100))
|
||||||
ax.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(freq_format))
|
ax.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(freq_format))
|
||||||
|
ax.grid(alpha=.3)
|
||||||
|
|
||||||
# Rend le résultat
|
# Rendu du résultat
|
||||||
if output_file == '-':
|
if output_file == '-':
|
||||||
plt.show()
|
plt.show()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -17,21 +17,26 @@ output_file = sys.argv[2]
|
||||||
|
|
||||||
# Calcul du FFT
|
# Calcul du FFT
|
||||||
signal = soundbox.load_signal(source_file)
|
signal = soundbox.load_signal(source_file)
|
||||||
freqs = np.fft.fft(signal)
|
vecs = np.fft.fft(signal)[:soundbox.samp_rate // 2]
|
||||||
|
values = np.absolute(vecs) / np.max(np.absolute(vecs))
|
||||||
|
|
||||||
# Génération du graphe
|
# Génération du graphe
|
||||||
ampl_scale = 1 / np.max(np.absolute(freqs))
|
plt.style.use('ggplot')
|
||||||
freq_scale = soundbox.samp_rate / len(signal)
|
|
||||||
|
|
||||||
plt.rcParams.update({
|
plt.rcParams.update({
|
||||||
'figure.figsize': (8, 4),
|
'figure.figsize': (10, 5),
|
||||||
'font.size': 16,
|
'font.size': 16,
|
||||||
'font.family': 'Concourse T4',
|
'font.family': 'Concourse T4',
|
||||||
})
|
})
|
||||||
|
|
||||||
fig, ax = plt.subplots()
|
fig, ax = plt.subplots()
|
||||||
ax.tick_params(axis='both', which='major', labelsize=12)
|
ax.tick_params(axis='both', which='major', labelsize=12)
|
||||||
ax.plot(np.absolute(freqs))
|
|
||||||
|
freq_filter = values >= 0.01
|
||||||
|
freq = np.arange(len(values))
|
||||||
|
ax.plot(freq[freq_filter], values[freq_filter])
|
||||||
|
|
||||||
|
# Configuration des axes
|
||||||
|
freq_scale = soundbox.samp_rate / len(signal)
|
||||||
|
|
||||||
|
|
||||||
def freq_format(value, pos):
|
def freq_format(value, pos):
|
||||||
|
@ -39,19 +44,19 @@ def freq_format(value, pos):
|
||||||
|
|
||||||
|
|
||||||
def ampl_format(value, pos):
|
def ampl_format(value, pos):
|
||||||
return f'{value * ampl_scale:.1f}'
|
return f'{value:.1f}'
|
||||||
|
|
||||||
|
|
||||||
ax.set_xlabel('Fréquence')
|
ax.set_xlabel('Fréquence', labelpad=10)
|
||||||
ax.set_xlim(0 / freq_scale, 800 / freq_scale)
|
ax.set_xscale('log', base=2)
|
||||||
ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(freq_format))
|
ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(freq_format))
|
||||||
ax.xaxis.set_major_locator(plt.MultipleLocator(100 / freq_scale))
|
ax.xaxis.set_major_locator(plt.MultipleLocator(100 / freq_scale))
|
||||||
|
|
||||||
ax.set_ylabel('Amplitude')
|
ax.set_ylabel('Amplitude relative', labelpad=10)
|
||||||
ax.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(ampl_format))
|
ax.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(ampl_format))
|
||||||
ax.yaxis.set_major_locator(plt.MultipleLocator(.2 / ampl_scale))
|
ax.yaxis.set_major_locator(plt.MultipleLocator(.2))
|
||||||
|
|
||||||
# Rend le résultat
|
# Rendu du résultat
|
||||||
if output_file == '-':
|
if output_file == '-':
|
||||||
plt.show()
|
plt.show()
|
||||||
else:
|
else:
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 72 KiB |
Binary file not shown.
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 32 KiB |
14
generate.py
14
generate.py
|
@ -16,13 +16,7 @@ def sine(dur, freq, value=1):
|
||||||
signal=soundbox.sine(dur, freq, value))
|
signal=soundbox.sine(dur, freq, value))
|
||||||
|
|
||||||
|
|
||||||
def square(dur, freq, value=1):
|
signal = soundbox.silence(10)
|
||||||
return soundbox.envelope(
|
|
||||||
attack=.2, decay=.2, release=.2,
|
|
||||||
signal=soundbox.square(dur, freq, value))
|
|
||||||
|
|
||||||
|
|
||||||
signal = soundbox.silence(9)
|
|
||||||
|
|
||||||
chords_l = (
|
chords_l = (
|
||||||
(('do', 2),),
|
(('do', 2),),
|
||||||
|
@ -46,9 +40,9 @@ for shift in (.5, 4.5):
|
||||||
for i in range(len(chords_l)):
|
for i in range(len(chords_l)):
|
||||||
soundbox.add_signal(signal, start=i / 2 + shift,
|
soundbox.add_signal(signal, start=i / 2 + shift,
|
||||||
source=soundbox.chord(
|
source=soundbox.chord(
|
||||||
instr=square, dur=.4,
|
instr=sine, dur=.8 if shift == 4.5 and i == 7 else .4,
|
||||||
freqs=soundbox.note_freqs(chords_l[i]),
|
freqs=soundbox.note_freqs(chords_l[i]),
|
||||||
value=.04
|
value=.4
|
||||||
))
|
))
|
||||||
|
|
||||||
for i in range(len(chords_r)):
|
for i in range(len(chords_r)):
|
||||||
|
@ -56,7 +50,7 @@ for shift in (.5, 4.5):
|
||||||
source=soundbox.chord(
|
source=soundbox.chord(
|
||||||
instr=sine, dur=1.1,
|
instr=sine, dur=1.1,
|
||||||
freqs=soundbox.note_freqs(chords_r[i]),
|
freqs=soundbox.note_freqs(chords_r[i]),
|
||||||
value=.5
|
value=.4
|
||||||
))
|
))
|
||||||
|
|
||||||
soundbox.save_signal(output_file, signal)
|
soundbox.save_signal(output_file, signal)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import wave
|
import wave
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy.signal as sig
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
# Nombre d’octets par échantillon
|
# Nombre d’octets par échantillon
|
||||||
|
@ -26,11 +25,6 @@ def sine(dur, freq, value=1):
|
||||||
return value * max_val * np.sin(2 * np.pi * freq * x / samp_rate)
|
return value * max_val * np.sin(2 * np.pi * freq * x / samp_rate)
|
||||||
|
|
||||||
|
|
||||||
def square(dur, freq, value=1):
|
|
||||||
x = np.arange(int(samp_rate * dur))
|
|
||||||
return value * max_val * sig.square(2 * np.pi * freq * x / samp_rate)
|
|
||||||
|
|
||||||
|
|
||||||
def envelope(attack, decay, release, signal):
|
def envelope(attack, decay, release, signal):
|
||||||
total = len(signal)
|
total = len(signal)
|
||||||
attack = int(attack * total)
|
attack = int(attack * total)
|
||||||
|
|
BIN
sounds/synth.wav
BIN
sounds/synth.wav
Binary file not shown.
Loading…
Reference in New Issue