Vue d'une trame composée de tonalités de 100ms chacune polluées avec du 50Hz, avec des bruit blanc et du silence entre chaque trame
Au début, la fonction FFT d'un éditeur de fichier sons était utilisée pour obtenir la fréquence de chaque ton, mais il fallait sélectionner chaque ton manuellement avant de lancer l'analyse, noter la fréquence, puis recommencer pour le ton suivant. Et cela pour chaque trame ! D'où l'idée d'écrire un petit décodeur simple qui directement un fichier wave "PCM" en entrée, et cela de façon automatisée.
Tout d'abord, il faut parser le header puis lire chaque échantillon du fichier wave, ce qui n'est pas très compliqué quand c'est du PCM. Ensuite, appliquer un passe haut pour virer le 50Hz. Attention, les extraits de code qui vont suivre sont un peu simplifiés, au niveau des conditions initiales et de la définitions des variables que je vais montrer au besoin (ou pas).
void analyze(double sample, double time){ static double samplelf, samplehf; ... // doing a low and highpass filter for the input signal (IIR filter) samplelf = (1.0-filterstrenght)*samplelf + filterstrenght*sample; samplehf = sample - samplelf; // got rid of that crappy 50Hz noise ! ...Ensuite on pourra appliquer un comparateur à hystérésis (deux seuils et une variable d'état statique) pour chronométrer la durée chaque période complète. Pour éviter les erreurs de fenêtrage du signal (car ce dernier n'est pas constant), il est tentant de chronométrer la durée de chaque période comme dans le code suivant, au lieu de compter le nombre de périodes pendant une durée définie tel que le ferait un fréquencemètre ordinaire.
static int trig = 0; static double lasttime; double period_duration; double h = 0.02; // hystérésis ajustable ... if (trig==-1) { if (samplehf>h){ period_duration = time-lasttime; // exploitation de la mesure ici ! ... lasttime=time; trig=1; } } else { if (samplehf<h){ trig=-1; } } ...Si on inverse la période pour obtenir la fréquence, nous sommes confrontés à la résolution temporelle de l'enregistrement, qui compte 44100 échantillons par secondes. Ça parait suffisant a priori, mais pour les fréquences un peu élevées, c'est quand même un peu génant :
C'est bien 20 tonalités de 100ms chacune, mais mesurées temporellement avec une résolution de ~23µs
Notre première idée était d'extrapoler le moment du passage du seuil compte tenu de la valeur de l'échantillon juste avant et celui après, mais c'est compliqué et inutile, il suffit de moyenner quelque peu, car l'imprécision d'une mesure affectera la mesure suivante mais dans le sens inverse. On utilise une bête moyenne glissante, par défaut periods_time_avg est réglé sur 16 mesures.
period_duration = time-lasttime; // averaging the mesured periods durations (FIR filter) { static unsigned int avg_idx = 0; int i; avg[avg_idx++] = period_duration; if (avg_idx >= periods_time_avg) { avg_idx = 0; } period_duration = 0; for (i=0; i<periods_time_avg; i++){ period_duration+=avg[i]; } period_duration/=periods_time_avg; } ...
C'est quand même plus joli comme ça.
A partir de ce moment, il est tentant d'ajouter une option pour obtenir quelques statistiques brutes sur la longueur totales des enregistrements.
// if the option is activated, doing some statistics ! so fun ! if (freq_stats){{ unsigned int freq = (unsigned int)(1.0/period_duration); freq_stats[freq]++; }}
Statistiques sur les fréquences de chaque période de toutes les trames des deux séries d'enregistrements sur une échelle log
C'est marrant, les valeurs de ces fréquences rappellent un peu celles du tableau suivant, bien qu'il nous manque encore le ~929 Hz :
Value | Frequency | Value | Frequency |
---|---|---|---|
1 | 1124 Hz | 6 | 1540 Hz |
2 | 1197 Hz | 7 | 1640 Hz |
3 | 1275 Hz | 8 | 1747 Hz |
4 | 1358 Hz | 9 | 1860 Hz |
5 | 1446 Hz | 0 | 1981 Hz |
Group (A) | 2400 Hz | Repeat (E) | 2110 Hz |
Maintenant, il s'agit de détecter automatiquement les tons "stables" afin d'en faire la liste. Pour éviter les fausses détections, ce code est plein de tests avec des nombres définis en dur, mais en gros, il faut que ça dure plus de 10ms sans bouger trop en fréquence, par rapport à la moyenne calculée sur le nombre de périodes sur le temps depuis lequel nous détectons le ton (pour une précision maximum), en excluant le nombre de périodes dues à la moyenne glissante que nous avons vu plus haut.
// trying to detecting tones. { static double tone_start=0, tone_measurement_start=0, tone_periods=0; double tone_duration, deltafreq; static double tone_avg_freq; static int reseted = 1; tone_periods+=1; tone_duration = time - tone_start; tone_avg_freq = tone_periods/tone_duration; deltafreq = 1.0/period_duration - tone_avg_freq; if ((deltafreq > freqthreshold) || (deltafreq < -freqthreshold)) { if (tone_duration > 10e-3 && tone_avg_freq > 300) { tone_avg_freq = (tone_periods-periods_time_avg)/(tone_duration-(period_duration*periods_time_avg)); tone(tone_avg_freq, tone_start, time); // début tone reseted=0; } if (tone_avg_freq < 100 && !reseted) { tone(0, tone_start, time); // ok, fin du tone reseted=1; } // reset counter tone_start = time; tone_periods=0, tone_measurement_start=0; } //tone_avg_freq = tone_periods/tone_duration; }En sortant sur un graphique le résultat de cette portion de code (courbe en "paliers" D3 en vert, un point au début, et un autre à la fin du ton, obtenus en lançant le programme avec l'option -D) en comparaison avec 1.0/period_duration en bleu clair, nous pouvons être satisfaits. Notons aussi D2 (résultats avant que nous ignorons un nombre de périodes égal au nombre de périodes prises en comptes dans la moyenne glissante) et D1 (coupures quand deltafreq prennait en compte non pas tone_avg_freq mais la valeur précédente de 1.0/period_duration, ce qui pouvait provoquer des tons détectés en double, comme en 0,65s).
Courbes montrant le fonctionnement de la portion de code chargé de détecter les tons (cliquer pour mieux voir)
Finalement, il ne reste plus qu'à lancer le programme sur l'ensemble des enregistrements pour en obtenir tous les tons :
clx@Diego:~/softwares/xilex$ ./xilex _off.wav _on.wav 0.1 1982 2108 1980 2109 1124 1747 1124 2400 2109 2400 930 1124 1981 2109 1980 930 1980 2109 1980 2108 2.2 1981 2109 1981 2109 1124 1747 1860 1358 1980 2109 930 1124 1980 2109 1981 930 1980 2108 1980 2109 4.3 1980 2109 1981 2109 1124 1747 1860 1124 1981 2109 930 1197 2109 1981 2109 930 1981 2109 1981 2109 6.5 1980 2109 1980 2108 1124 1747 1860 1446 1980 2108 930 1124 2109 1124 1980 930 1980 2109 1980 2108 8.6 1981 2109 1981 2109 1124 1747 1860 1197 1980 2109 930 1124 2109 1124 1981 930 1981 2109 1980 2109 10.7 2106 1981 2109 1124 1747 1860 1358 1981 2109 930 1123 1980 2108 1981 930 1981 2109 1980 2109 12.8 1981 2109 1981 2109 1124 1747 1124 2400 2109 2400 930 1124 1980 2109 1981 930 1980 2109 1981 2109 15.0 1981 2109 1980 2109 1124 1747 1860 1124 1980 2109 930 1197 2109 1980 2108 930 1980 2109 1981 2108 17.1 1981 2109 1980 2109 1124 1747 1860 1446 1980 2108 930 1124 2109 1124 1980 930 1981 2109 1980 2108 19.2 1980 2109 1981 2109 1124 1747 1860 1197 1981 2109 930 1124 2109 1124 1980 929 1980 2109 1981 2109 21.4 1981 2109 1980 2109 1124 1747 1124 2400 2109 2400 930 1124 1980 2109 1981 930 1980 2109 1980 2109 23.6 1979 2109 1981 2109 1124 1747 1860 1359 1980 2109 930 1124 1981 2109 1981 930 1980 2109 1981 2108 25.7 1980 2109 1981 2109 1124 1747 1860 1124 1980 2109 930 1197 2109 1981 2109 930 1981 2109 1981 2109 27.8 1981 2108 1980 2109 1124 1747 1860 1447 1980 2109 930 1124 2109 1124 1980 930 1980 2108 1981 2109 29.9 1980 2109 1981 2109 1124 1747 1860 1197 1980 2109 930 1123 2109 1124 1981 930 1981 2109 1981 2109 32.0 1981 2108 1981 2109 1124 1747 1124 2400 2109 2400 930 1124 1981 2108 1981 930 1980 2109 1981 2109 34.3 1981 2108 1981 2109 1124 1747 1860 1358 1980 2109 930 1123 1980 2109 1981 930 1981 2109 1980 2109 36.4 1980 2108 1980 2109 1124 1747 1859 1124 1980 2109 930 1197 2108 1981 2109 930 1980 2109 1981 2109 38.7 1980 2109 1981 2109 1124 1747 1860 1446 1980 2109 930 1124 2108 1124 1981 930 1980 2109 1981 2109 End of file! 3596618 bytes, 40.78s 0.2 2108 1980 2109 1124 1747 1859 1446 1980 2109 930 1124 2109 1124 1980 930 1980 2109 1980 2109 2.4 1980 2109 1981 2109 1124 1747 1124 2400 2109 2400 930 1197 1980 2109 1980 930 1980 2109 1981 2109 4.7 1981 2109 1981 2109 1124 1747 1860 1358 1980 2109 930 1197 1980 2109 1981 930 1981 2109 1981 2109 6.9 1980 2109 1981 2109 1124 1747 1860 1197 1980 2109 930 1124 2108 1124 1981 929 1981 2108 1981 2109 9.2 1980 2109 1981 2109 1124 1747 1860 1124 1981 2109 930 1197 2109 1980 2109 930 1980 2109 1980 2109 11.6 1980 2109 1980 2109 1124 1747 1860 1446 1980 2109 930 1124 2108 1124 1980 930 1981 2109 1981 2108 13.8 1980 2109 1981 2109 1124 1747 1124 2400 2109 2400 930 1197 1980 2109 1980 930 1981 2109 1981 2109 16.2 1981 2109 1981 2109 1124 1747 1860 1359 1981 2109 930 1197 1980 2109 1980 930 1980 2109 1981 2109 18.5 1980 2108 1981 2109 1124 1747 1860 1197 1980 2109 930 1123 2109 1124 1980 930 1981 2108 1981 2109 20.8 1981 2109 1980 2109 1124 1747 1860 1124 1981 2109 930 1197 2109 1980 2109 930 1980 2109 1980 2108 23.0 1980 2109 1980 2109 1124 1747 1860 1446 1980 2109 930 1123 2109 1124 1980 930 1980 2109 1981 2109 25.2 1981 2108 1981 2109 1124 1747 1124 2400 2109 2400 930 1197 1980 2109 1981 930 1980 2109 1980 2109 27.5 1980 2109 1980 2109 1124 1747 1860 1359 1980 2108 930 1197 1980 2109 1980 930 1980 2108 1981 2109 29.8 1980 2109 1981 2109 1124 1747 1860 1197 1981 2109 930 1124 2109 1124 1980 930 1981 2108 1980 2108 32.0 1981 2109 1981 2109 1124 1747 1860 1124 1981 2109 930 1197 2109 1981 2109 930 1981 2109 1980 2108 34.3 1980 2109 1981 2109 1124 1747 1859 1446 1981 2109 930 1124 2108 1124 1981 930 1981 2108 1980 2109 36.5 1980 2108 1981 2109 1124 1747 1124 2400 2109 2400 930 1197 1980 2109 1981 930 1981 2108 1981 2109 38.8 1981 2109 1981 2109 1124 1747 1860 1358 1980 2109 930 1197 1980 2109 1980 930 1980 2108 1981 2109 End of file! 3615050 bytes, 40.99sLes valeurs manquantes sont dues à un retard à la détection de présence de signal audio d'Audacity, et donc ne sont pas présentes dans l'enregistrement (on peut deviner que c'est 1981Hz) - j'ai juste ajouté des espaces pour aligner les colonnes pour faire joli. Nous constatons que les fréquences moyennes obtenues sont stables et correspondent à celles du codage CCIR ; il est très simple de coder une petite fonction pour afficher les caractères décodés plutôt que leurs fréquences.
char tonefreq2ccir(int freq){ char ccircharacters[14] = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'A', 'E', '/', '?'}; int ccirfrequencies[14] = { 1124, 1197, 1275, 1358, 1446, 1540, 1640, 1747, 1860, 1981, 2400, 2110, 930, 0}; unsigned int index = 0; for(;;){ if (ccirfrequencies[index] == 0){ break; } // tant pis if (freq >= ccirfrequencies[index]-10 && freq <= ccirfrequencies[index]+10) { break; } // ok index++; } return ccircharacters[index]; }... ce qui donne :
----- journée ------ ------- nuit ------- 0E0E181AEA/10E0/0E0E 0E0E18950E/1E10/0E0E 0E0E18940E/10E0/0E0E 0E0E181AEA/20E0/0E0E 0E0E18910E/2E0E/0E0E 0E0E18940E/20E0/0E0E 0E0E18950E/1E10/0E0E 0E0E18920E/1E10/0E0E 0E0E18920E/1E10/0E0E 0E0E18910E/2E0E/0E0E 0E0E18940E/10E0/0E0E 0E0E18950E/1E10/0E0E 0E0E181AEA/10E0/0E0E 0E0E181AEA/20E0/0E0E 0E0E18910E/2E0E/0E0E 0E0E18940E/20E0/0E0E 0E0E18950E/1E10/0E0E 0E0E18920E/1E10/0E0E 0E0E18920E/1E10/0E0E 0E0E18910E/2E0E/0E0E 0E0E181AEA/10E0/0E0E 0E0E18950E/1E10/0E0E 0E0E18940E/10E0/0E0E 0E0E181AEA/20E0/0E0E 0E0E18910E/2E0E/0E0E 0E0E18940E/20E0/0E0E 0E0E18950E/1E10/0E0E 0E0E18920E/1E10/0E0E 0E0E18920E/1E10/0E0E 0E0E18910E/2E0E/0E0E 0E0E181AEA/10E0/0E0E 0E0E18950E/1E10/0E0E 0E0E18940E/10E0/0E0E 0E0E181AEA/20E0/0E0E 0E0E18910E/2E0E/0E0E 0E0E18940E/20E0/0E0E 0E0E18950E/1E10/0E0EMaintenant, il reste a comprendre la signification de ces codes, mais ça sera pour une autre fois lors d'une mise à jour future !