Compare commits

..

No commits in common. "093a341dd22cd2d4cba2d937b2b1125e10af651f" and "4104126c6e72bf4649e9c98cfc9cc8fb460ea6cb" have entirely different histories.

8 changed files with 43 additions and 62 deletions

View File

@ -1,26 +1,7 @@
<!-- vim: set spelllang=fr: -->
# À la recherche des notes originelles
# Utilisation dun sonagramme pour retrouver les notes dun enregistrement
À partir de lenregistrement numérique dune 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 sapparente à celle où lon dispose dune image matricielle rendue à partir dune image vectorielle et où lon souhaite retrouver les vecteurs dorigine dont on ne dispose plus — à la différence que nous traitons ici dun signal 1D au lieu de 2D, et dun signal sonore plutôt que visuel qui plus est.
![](fig/synth-single.png)
## Excursion sinusoïdale
Simplifions dabord le problème en considérant [un signal composé uniquement de sinusoïdes pures](sounds/synth.wav) (produit par le synthétiseur de fortune quest [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 nont quune 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 lobtenir consiste à découper le signal en courtes fenêtres de temps et dappliquer la transformation de Fourier sur les morceaux obtenus : cest [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 quen est-il dun signal plus complexe ?
![](fig/synth-shorttime.png)

View File

@ -17,29 +17,24 @@ output_file = sys.argv[2]
# Calcul du STFT
signal = soundbox.load_signal(source_file)
freq, time, vecs = sig.stft(signal, soundbox.samp_rate, nperseg=soundbox.samp_rate * 0.5)
values = np.absolute(vecs)
freq, time, fts = sig.stft(signal, soundbox.samp_rate, nperseg=soundbox.samp_rate * 0.5)
# Génération du graphe
plt.rcParams.update({
'figure.figsize': (10, 8),
'figure.frameon': True,
'font.size': 20,
'figure.figsize': (8, 8),
'font.size': 16,
'font.family': 'Concourse T4',
})
fig, ax = plt.subplots(frameon=True)
fig, ax = plt.subplots()
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(
time, freq[freq_filter], values[freq_filter],
cmap='Greys',
time, freq,
np.abs(fts),
cmap='plasma',
shading='gouraud')
# Configuration des axes
def time_format(value, pos):
return f'{value:.0f} s'
@ -48,16 +43,14 @@ def freq_format(value, pos):
return f'{value:.0f} Hz'
ax.set_xlabel('Temps', labelpad=10)
ax.set_xlabel('Temps')
ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(time_format))
ax.set_ylabel('Fréquence', labelpad=10)
ax.set_yscale('log', base=2)
ax.yaxis.set_major_locator(plt.MultipleLocator(100))
ax.set_ylabel('Fréquence')
ax.set_ylim(0, 800)
ax.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(freq_format))
ax.grid(alpha=.3)
# Rendu du résultat
# Rend le résultat
if output_file == '-':
plt.show()
else:

View File

@ -17,26 +17,21 @@ output_file = sys.argv[2]
# Calcul du FFT
signal = soundbox.load_signal(source_file)
vecs = np.fft.fft(signal)[:soundbox.samp_rate // 2]
values = np.absolute(vecs) / np.max(np.absolute(vecs))
freqs = np.fft.fft(signal)
# Génération du graphe
plt.style.use('ggplot')
ampl_scale = 1 / np.max(np.absolute(freqs))
freq_scale = soundbox.samp_rate / len(signal)
plt.rcParams.update({
'figure.figsize': (10, 5),
'figure.figsize': (8, 4),
'font.size': 16,
'font.family': 'Concourse T4',
})
fig, ax = plt.subplots()
ax.tick_params(axis='both', which='major', labelsize=12)
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)
ax.plot(np.absolute(freqs))
def freq_format(value, pos):
@ -44,19 +39,19 @@ def freq_format(value, pos):
def ampl_format(value, pos):
return f'{value:.1f}'
return f'{value * ampl_scale:.1f}'
ax.set_xlabel('Fréquence', labelpad=10)
ax.set_xscale('log', base=2)
ax.set_xlabel('Fréquence')
ax.set_xlim(0 / freq_scale, 800 / freq_scale)
ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(freq_format))
ax.xaxis.set_major_locator(plt.MultipleLocator(100 / freq_scale))
ax.set_ylabel('Amplitude relative', labelpad=10)
ax.set_ylabel('Amplitude')
ax.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(ampl_format))
ax.yaxis.set_major_locator(plt.MultipleLocator(.2))
ax.yaxis.set_major_locator(plt.MultipleLocator(.2 / ampl_scale))
# Rendu du résultat
# Rend le résultat
if output_file == '-':
plt.show()
else:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -16,7 +16,13 @@ def sine(dur, freq, value=1):
signal=soundbox.sine(dur, freq, value))
signal = soundbox.silence(10)
def square(dur, freq, value=1):
return soundbox.envelope(
attack=.2, decay=.2, release=.2,
signal=soundbox.square(dur, freq, value))
signal = soundbox.silence(9)
chords_l = (
(('do', 2),),
@ -40,9 +46,9 @@ for shift in (.5, 4.5):
for i in range(len(chords_l)):
soundbox.add_signal(signal, start=i / 2 + shift,
source=soundbox.chord(
instr=sine, dur=.8 if shift == 4.5 and i == 7 else .4,
instr=square, dur=.4,
freqs=soundbox.note_freqs(chords_l[i]),
value=.4
value=.04
))
for i in range(len(chords_r)):
@ -50,7 +56,7 @@ for shift in (.5, 4.5):
source=soundbox.chord(
instr=sine, dur=1.1,
freqs=soundbox.note_freqs(chords_r[i]),
value=.4
value=.5
))
soundbox.save_signal(output_file, signal)

View File

@ -1,5 +1,6 @@
import wave
import numpy as np
import scipy.signal as sig
import math
# Nombre doctets par échantillon
@ -25,6 +26,11 @@ def sine(dur, freq, value=1):
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):
total = len(signal)
attack = int(attack * total)

Binary file not shown.