Construction des fichiers d’enregistrements au format .WAV
La différence entre un fichier audio que nous venons de générer et un fichier au format WAV, c’est son en-tête.
L’en-tête WAV
Un fichier WAV possède un en-tête d’une longueur de 44 octets composé de 3 blocs:
1er bloc: Description RIFF |
|
---|---|
Position (en octets) | Description |
1 -> 4 | Modèle générique de format du fichier. Il s’agit d’un fichier multimédia au modèle “RIFF” (Resource Interchange File Format). |
1 -> 4 | Taille restante du fichier en octets. C’est-à-dire la taille totale du fichier moins les 8 octets correspondants à ces 2 premiers champs. |
9 -> 12 | Format du fichier. “WAVE” dans notre cas. |
2ème bloc: Description du format des informations audio |
|
---|---|
Position (en octets) | Description |
13 -> 16 | Identifiant du format de description des caractéristiques de données d’échantillons audio. En résumé, ce champ spécifie le format du reste de l’en-tête. Toujours “fmt ” pour le format .WAV. |
17 -> 20 | Nombre d’octets restant dans ce bloc. (Toujours égal à 16) |
21 -> 22 |
Format d’encodage numérique de l’onde sonore dans le fichier. Les principaux identifiants de format sont: |
23 -> 24 | Nombre de canaux. |
25 -> 28 | Fréquence d’échantillonnage en Hertz. |
29 -> 32 |
Nombre d’octets à lire par seconde.
Donné par la formule: {Fréquence d’échantillonnage} * {Nombre de bits par échantillon} * {Nombre de canaux} / 8. |
33 -> 34 |
Nombre d’octets pour un échantillon incluant tous les canaux. |
35 -> 36 | Nombre de bits utilisés pour le codage de chaque échantillon. |
3ème bloc: Description des données | |
---|---|
Position (en octets) | Description |
37 -> 40 | Marqueur du début de bloc des données: “data“. |
41 -> 44 | Taille des données en octets. Donné par la formule: {taille du fichier} – {taille de l’en-tête}. |
De ces informations, nous en déduisons la structure C d’en-tête WAV suivante pour notre projet:
typedef struct { const char modele_generique_fichier[4] = { 'R', 'I', 'F', 'F' }; int32_t taille_reste_fichier; const char format_fichier[4] = { 'W', 'A', 'V', 'E' }; const char format_description[4] = { 'f', 'm', 't', ' ' }; const int32_t taille_reste_bloc_wav = 16; const int16_t format_encodage = 1; const int16_t nombre_canaux = 1; const int32_t frequence_echantillonnage = FREQUENCE_ECHANTILLONAGE; const int32_t octets_a_lire = FREQUENCE_ECHANTILLONAGE*NOMBRE_BITS_PAR_ECHANTILLONS*NOMBRE_CANAUX/8; const int16_t octets_par_echantillon_tous_canaux = NOMBRE_BITS_PAR_ECHANTILLONS*NOMBRE_CANAUX/8; const int16_t bits_par_echantillon = NOMBRE_BITS_PAR_ECHANTILLONS; const char marqueur_bloc_donnees[4] = { 'd', 'a', 't', 'a' }; int32_t taille_donnees; } entete_wav_t;
Comme vous pouvez le remarquer, certains champs de cet en-tête ne seront connus qu’à la fin de l’enregistrement. Il s’agit des champs relatifs à la taille du fichier et à la taille des données audio.
Nous complétons donc la fonction de création de fichiers sur la carte SD en y ajoutant un saut dans le fichier afin de laisser l’espace pour l’en-tête qui sera écrit en fin d’enregistrement:
File SD_cree_fichier_wav_pour_ecriture(char *nom_fichier_p) { File fichier_l; SD.remove(nom_fichier_p); fichier_l = SD.open(nom_fichier_p, FILE_WRITE); if(!fichier_l) { sprintf(tampon_traces_g, "Erreur de creation du fichie \'%s\'.", nom_fichier_p+1); Serial.println(tampon_traces_g); } fichier_l.seek(sizeof(entete_wav_t), SeekSet); // Réserve l'espace pour l'Entête WAV return(fichier_l); }
La fonction d’écriture de l’en-tête WAV complète la structure précédemment définie, puis elle écrit cette structure en début de fichier dans l’espace laissé vide:
void SD_ecrire_entete_wav(File fichier_p) { uint32_t taille_fichier_l; entete_wav_t entete_wav_l; taille_fichier_l = fichier_p.position(); entete_wav_l.taille_reste_fichier = taille_fichier_l - 8; entete_wav_l.taille_donnees = taille_fichier_l - 44; fichier_p.seek(0, SeekSet); // Positionnement du curseur au deb if(!fichier_p.write((uint8_t*)&entete_wav_l, sizeof(entete_wav_t))) { Serial.println(F("Erreur de reecriture de l'entete du fichier son.")); } }
Le code source complet
Voici le code source complet de ce tutoriel:
/* Déclaration des librairies utilisées */ #include <SD.h> #include <driver/i2s.h> // Brochage lecteur de cartes: #define PORT_CARD_CS 5 // Brochage du micro: #define PORT_MIC_DIN 33 // Entree pour des données #define PORT_MIC_LRCL 25 // Selection du canal pour l'émission #define PORT_MIC_BCLK 32 // Signal d'horloge // Configuration I2S pour le micro #define PORT_I2S I2S_NUM_0 // Brochage des boutons: #define PORT_BOUTON_ENREGISTRER 17 // Format audio d'enregistrement #define FREQUENCE_ECHANTILLONAGE 44100 #define NOMBRE_CANAUX 1 #define NOMBRE_BITS_PAR_ECHANTILLONS 32 #define NOMBRE_OCTETS_PAR_ECHANTILLONS NOMBRE_BITS_PAR_ECHANTILLONS/8 // Performances d'enregistrement #define I2S_TAILLE_BUFFER_ENREGISTREMENT 640 // Doit être un multiple de NOMBRE_OCTETS_PAR_ECHANTILLONS #define I2S_NOMBRE_BUFFER 16 #define TAILLE_EXTRACTION_BUFFER 640 // Autres constantes #define NOM_RACINE_ENREGISTREMENT "/Enr_" /* Déclaration des fonctions */ void SD_demarre(int port_CS_p); File SD_cree_fichier_wav_pour_ecriture(char *nom_fichier_p); void donner_nom_fichier(char* nom_fichier_P, int index_P); int donner_prochain_index_enregistrement(void); void micro_configure(int nombre_buffer_p, int taille_buffer_p, int port_DIN_p, int port_WS_p, int port_BCK_p); void enregistrement_voix(File fichier_enregistrement_p, int bouton_maintien_p); size_t I2S_micro_lire(byte *donnees_micro_p, size_t taille_demande_p); void SD_ecrire_entete_wav(File fichier_p); /* Déclaration des structures */ typedef struct { const char modele_generique_fichier[4] = { 'R', 'I', 'F', 'F' }; int32_t taille_reste_fichier; const char format_fichier[4] = { 'W', 'A', 'V', 'E' }; const char format_description[4] = { 'f', 'm', 't', ' ' }; const int32_t taille_reste_bloc_wav = 16; const int16_t format_encodage = 1; const int16_t nombre_canaux = 1; const int32_t frequence_echantillonnage = FREQUENCE_ECHANTILLONAGE; const int32_t octets_a_lire = FREQUENCE_ECHANTILLONAGE*NOMBRE_BITS_PAR_ECHANTILLONS*NOMBRE_CANAUX/8; const int16_t octets_par_echantillon_tous_canaux = NOMBRE_BITS_PAR_ECHANTILLONS*NOMBRE_CANAUX/8; const int16_t bits_par_echantillon = NOMBRE_BITS_PAR_ECHANTILLONS; const char marqueur_bloc_donnees[4] = { 'd', 'a', 't', 'a' }; int32_t taille_donnees; } entete_wav_t; /* Déclaration globales */ int prochain_index_enregistrement_g; bool bouton_relache_g; char tampon_traces_g[200]; /* Fonction de démarrage, s'exécute une seule fois: */ void setup() { // Pour le debug Serial.begin(115200); Serial.println("-----\n"); // Intialisation des variables globales bouton_relache_g = true; // Initialisation des ports des boutons pinMode(PORT_BOUTON_ENREGISTRER, INPUT_PULLUP); // Initialisation de la carte SD SD_demarre(PORT_CARD_CS); // Initialisation du micro micro_configure(I2S_NOMBRE_BUFFER, I2S_TAILLE_BUFFER_ENREGISTREMENT, PORT_MIC_DIN, PORT_MIC_LRCL, PORT_MIC_BCLK); i2s_stop(PORT_I2S); // Micro coupé en attendant d'en avoir besoin // Parcourt les fichiers d'enregistrement déjà présents prochain_index_enregistrement_g = donner_prochain_index_enregistrement(); } /* Fonction principale du programme, s'exécute en boucle: */ void loop() { char nom_fichier_l[13]; File fichier_enregistrement_l; if(digitalRead(PORT_BOUTON_ENREGISTRER) == LOW) { if(bouton_relache_g) { donner_nom_fichier(nom_fichier_l, prochain_index_enregistrement_g); fichier_enregistrement_l = SD_cree_fichier_wav_pour_ecriture(nom_fichier_l); if (fichier_enregistrement_l) { enregistrement_voix(fichier_enregistrement_l, PORT_BOUTON_ENREGISTRER); SD_ecrire_entete_wav(fichier_enregistrement_l); fichier_enregistrement_l.close(); sprintf(tampon_traces_g, "Fichier son \'%s\' créé.", nom_fichier_l+1); Serial.println(tampon_traces_g); } prochain_index_enregistrement_g = donner_prochain_index_enregistrement(); bouton_relache_g = false; delay(300); // Pour éviter les rebonds parasites } } else { bouton_relache_g = true; } } void SD_demarre(int port_CS_p) { // Initialisation des ports du lecteur carte SD pinMode(port_CS_p, INPUT); // Carte SD insérée ? // Initialisation de la carte SD if (!SD.begin(port_CS_p)) { Serial.println("Carte SD absente, mal formatée ou lecteur mal câblé."); } } File SD_cree_fichier_wav_pour_ecriture(char *nom_fichier_p) { File fichier_l; SD.remove(nom_fichier_p); fichier_l = SD.open(nom_fichier_p, FILE_WRITE); if(!fichier_l) { sprintf(tampon_traces_g, "Erreur de creation du fichie \'%s\'.", nom_fichier_p+1); Serial.println(tampon_traces_g); } fichier_l.seek(sizeof(entete_wav_t), SeekSet); // Réserve l'espace pour l'Entête WAV return(fichier_l); } int donner_prochain_index_enregistrement(void) { char nom_fichier_l[13]; int index_message_l; index_message_l = 0; do { index_message_l++; donner_nom_fichier(nom_fichier_l, index_message_l); } while(SD.exists(nom_fichier_l)); return(index_message_l); } void donner_nom_fichier(char* nom_fichier_P, int index_P) { sprintf(nom_fichier_P, "%s%03d%s", NOM_RACINE_ENREGISTREMENT, index_P, ".wav"); } void micro_configure(int nombre_buffer_p, int taille_buffer_p, int port_DIN_p, int port_WS_p, int port_BCK_p) { const i2s_config_t configuration_i2s = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = FREQUENCE_ECHANTILLONAGE, .bits_per_sample = (i2s_bits_per_sample_t)NOMBRE_BITS_PAR_ECHANTILLONS, .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S | I2S_COMM_FORMAT_STAND_PCM_SHORT), .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interruption de niveau 1 .dma_buf_count = nombre_buffer_p, .dma_buf_len = taille_buffer_p, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = -1, .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT }; i2s_driver_install(PORT_I2S, &configuration_i2s, 0, NULL); const i2s_pin_config_t brochage_i2s = { .mck_io_num = I2S_PIN_NO_CHANGE, .bck_io_num = port_BCK_p, .ws_io_num = port_WS_p, .data_out_num = I2S_PIN_NO_CHANGE, .data_in_num = port_DIN_p }; i2s_set_pin(PORT_I2S, &brochage_i2s); } void enregistrement_voix(File fichier_enregistrement_p, int bouton_maintien_p) { byte donnees_recues_l[TAILLE_EXTRACTION_BUFFER]; size_t nombre_octets_recus_l; i2s_start(PORT_I2S); // Ouvre le micro do { nombre_octets_recus_l = I2S_micro_lire(donnees_recues_l, TAILLE_EXTRACTION_BUFFER); if (nombre_octets_recus_l > 0) { if(!fichier_enregistrement_p.write(donnees_recues_l, nombre_octets_recus_l)) { Serial.println(F("Erreur d'ecriture des donnees audio.")); } } } while(digitalRead(bouton_maintien_p)==LOW); i2s_stop(PORT_I2S); // Coupe le micro } size_t I2S_micro_lire(byte *donnees_micro_p, size_t taille_demande_p) { size_t nombre_octets_lus_l; /* Valeurs par defaut */ nombre_octets_lus_l = 0; i2s_read(PORT_I2S, donnees_micro_p, taille_demande_p, &nombre_octets_lus_l, portMAX_DELAY); return(nombre_octets_lus_l); } void SD_ecrire_entete_wav(File fichier_p) { uint32_t taille_fichier_l; entete_wav_t entete_wav_l; taille_fichier_l = fichier_p.position(); entete_wav_l.taille_reste_fichier = taille_fichier_l - 8; entete_wav_l.taille_donnees = taille_fichier_l - 44; fichier_p.seek(0, SeekSet); // Positionnement du curseur au deb if(!fichier_p.write((uint8_t*)&entete_wav_l, sizeof(entete_wav_t))) { Serial.println(F("Erreur de reecriture de l'entete du fichier son.")); } }
La vidéo du résultat:
Et voici les fichiers WAV écrits sur la carte Micro SD: Enregistrement 1 et Enregistrement 2.
Augmenter le volume
Un reproche régulièrement fait au micro SPH0645, est son volume d’enregistrement faible… Eh bien, amplifions le signal numérique !
Je vous propose de multiplier le volume d’enregistrement par 4:
#define FACTEUR_AMPLIFICATION 4
Les enregistrements que nous avons obtenus avec le micro SPH0645 sont de bonne qualité, aussi il n’est pas nécessaire d’utiliser de filtre de bruit. Cela simplifie l’écriture de la fonction amplifier_volume_extrait:
void amplifier_volume_extrait(uint8_t *extrait_p, int nombre_octets_p) { int ii; int32_t *echantillon_p; if((nombre_octets_p%4)!=0) { Serial.println("L'extrait n'est pas un multiple de 4 !"); } echantillon_p = (int32_t*)extrait_p; for(ii=0; ii<nombre_octets_p/4 ; ii++) { *echantillon_p=(*echantillon_p)*FACTEUR_AMPLIFICATION; echantillon_p++; } }
L’appel de cette fonction ce fait dans la fonction enregistrement_voix comme suit:
void enregistrement_voix(File fichier_enregistrement_p, int bouton_maintien_p) { byte donnees_recues_l[TAILLE_EXTRACTION_BUFFER]; size_t nombre_octets_recus_l; i2s_start(PORT_I2S); // Ouvre le micro do { nombre_octets_recus_l = I2S_micro_lire(donnees_recues_l, TAILLE_EXTRACTION_BUFFER); if (nombre_octets_recus_l > 0) { amplifier_volume_extrait(donnees_recues_l, nombre_octets_recus_l); if(!fichier_enregistrement_p.write(donnees_recues_l, nombre_octets_recus_l)) { Serial.println(F("Erreur d'ecriture des donnees audio.")); } } } while(digitalRead(bouton_maintien_p)==LOW); i2s_stop(PORT_I2S); // Coupe le micro }
Nous sommes à la fin de ce tutoriel, j’espère qu’il vous a donné plein d’idées pour vos projets ! 😉