Vous ne le saviez peut-être pas, mais l'éclairage public ne dispose que rarement de câbles prévus pour les commander quartier par quartier. Du coup, on trouve des capteurs pour allumer automatiquement dès que la lumière ambiante devient faible (quand c'est pas une bête horloge systématiquement mal réglée), mais également des systèmes de télécommande utilisant les fréquences radio. Outre EPAR, on trouve aussi un système nommé Xylos (toujours de la même boîte) qui transmet en modulation "audio" (en FM) et qui peut donc être enregistré avec n'importe quel récepteur radio à partir du moment qu'il soit capable de discriminer la FM et qu'il soit calé sur la bonne fréquence (qui dépend de la ville, mais entre 31 et 32 MHz à priori). On m'a donné un enregistrement un peu pourri d'une journée et d'une nuit, en automatique (mise en route de l'enregistrement quand il y a du son à l'entrée). Une trame vue dans un éditeur audio, ressemble à ça :

off1_rawsamples
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 :

off1_delays_raw
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;
	}
	...

off1_delays_filtered
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]++;
	}}

freq_stats
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
CCIR tone frequencies (Source: Wikipedia)

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).

off1_F_D1_D2_D3
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.99s
Les 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/0E0E

Maintenant, il reste a comprendre la signification de ces codes, mais ça sera pour une autre fois lors d'une mise à jour future !