You are currently viewing Enregistrer sa voix au format WAV avec Arduino et le SPH0645
Enregistrer sa voix au format .WAV avec Arduino et le SPH0645

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:
         1 -> PCM,
         3 -> PCM à virgule flottante,
         6 -> ITU-T G.711 A-Law,
         7 -> ITU-T G.711 μ-Lawet et
65534 ->  Wave-Format extensible

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.
Donné par la formule: {Nombre de bits par échantillon} * {Nombre de canaux} / 8.

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:

Enregistrement de sa voix au format WAV avec Arduino et le SPH0645

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 ! 😉

S’abonner
Notification pour
guest
0 Commentaires
Commentaires en ligne
Afficher tous les commentaires