Pagina personale di:
Carlo Vecchio
appunti di C#, R, SQL Server, ASP.NET, algoritmi, numeri
Vai ai contenuti

C# - Audio - File WAV

C#
Audio - I File WAV
Struttura
  • I file audio wave (con estensione 'wav') contengono forme d'onda sonore, campionate e quantizzate.
  • Campionate: significa che un certo numero di volte ogni secondo, viene misurato il segnale audio. La quantità di misurazioni al secondo si chiama frequenza di campionamento (si misura in Hertz) e il valore misurato è appunto un campione. Per esempio un audio di qualità CD ha una frequenza di campionamento di 44100 Hz.
  • Quantizzate: significa che il campione preso, non può assumere qualsiasi valore, ma solo alcuni, per esempio 256 valori distinti. Volendo infatti memorizzare in campione, esso non può assumere qualsiasi valore, ma solo i valori permessi dall'area che gli si mette a disposizione. Se si utilizza un byte a campione, esso può assumere solo 256 valori distinti; se si utilizzano invece due byte, i valori distinti sono 65536.
  • I file wave fanno parte dei file RIFF; hanno quindi la stessa struttura gerarchica di chunk descritta in I File RIFF.
  • Il chunk principale di tipo 'RIFF' contiene diversi sub-chunk, alcuni sempre presenti, altri opzionali.

Chunk sempre presenti
  • Tipo 'fmt ' (con lo spazio): è lungo 16 byte e contiene le informazioni sul formato di registrazione dell'audio.
  • Tipo 'data': è di lunghezza variabile e contiene tutti i campioni del segnale audio.

Chunk opzionali
  • Tipo 'fact': tipicamente è lungo 4 byte ed è richiesto per audio memorizzati in formato diverso dal formato PCM.
  • Tipo 'slnt': è lungo 4 byte indica un periodo di silenzio.
  • Tipo 'cue ' (con lo spazio): è di lunghezza variabile e consente di memorizzare gli offset di alcuni punti notevoli. A questi punti notevoli, si possono associare i chunk 'plst' che specificano l'ordine di esecuzione), i chunk 'list' che specificano del testo, i chunk 'labl' che specificano delle label, i chunk 'note' che specificano delle note e i chunk 'ltxt' che specificano sempre del testo.
  • Tipo 'smpl' e 'inst' associati ai suoni MIDI.
  • Tipo 'wavl': ha lunghezza variabile e non è molto supportato dai player.
  • Tipo 'CDif': ho trovato questi chunk (anche più di uno nel file) di 68 byte in alcuni file in Windows 10, ma non ho trovato nessun documento che ne spieghi il significato.
  • Nei link seguenti, ho trovato le informazioni appena descritte.

Il chunk fmt
  • Il chunk 'fmt ' contiene informazioni su come il segnale audio è registrato nella sezione dati. Il chunk è lungo generalmente 16 byte (può avere qualche byte in più per formati non-PCM) e il significato dei byte è il seguente. Si noti che ogni blocco di byte è sempre rappresentato in formato little-endian, quindi leggendo i byte nello stream, essi sono ordinati dal meno significativo al più significativo.
  • 2 byte: Tipo di codifica o compressione.
    • 0000h -> Sconosciuto.
    • 0001h -> Formato PCM, non compresso.
    • 0002h -> Microsoft ADPCM.
    • 0006h -> ITU G.711 a-law
    • 0007h -> ITU G.711 µ-law
    • 0011h -> IMA ADPCM
    • 0016h -> ITU G.723 ADPCM (Yamaha)
    • 0031h -> GSM 6.10
    • 0040h -> ITU G.721 ADPCM
    • 0050h -> MPEG
    • FFFFh -> Experimental
  • 2 byte: Numero di canali.
    • 0001h -> Mono.
    • 0002h -> Stereo.
  • 4 byte: Frequenza di campionamento.
    • Valori tipici: 8000, 22050, 44100.
  • 4 byte: Byte per secondo.
    • Numero di byte necessari per memorizzare ogni secondo di audio.
    • Il valore si calcola con la formula: Frequenza di campionamento * Block Align oppure Frequenza di campionamento *  Numero di canali * Bits per campione / 8.
  • 2 byte: Block Align.
    • Numero di byte per ogni campione, inclusi tutti i canali.
    • Il valore si calcola con la formula: Numero di canali * Bits per campione / 8.
  • 2 byte: Bit per campione.
    • Numero di bit necessari per ogni campione.
    • È generalmente 8, 16, 24 o 32.
  • Altri byte:
    • Possono esserci altri byte significativi per formati non-PCM.

Esempio 1
  • Si considera il file 'chimes.wav' contenuto in Windows 10.
  • La figura seguente mostra il file aperto con un editor esadecimale e la parte selezionata è il chunk 'fmt '.



  • 4 Byte: 66h 6dh 74h 20h -> stringa 'fmt ', ID del chunk.
  • 4 Byte: 10h 00h 00h 00h -> Dimensione del chunk, 16 byte.
  • 2 Byte: 01h 00h -> Tipo di codifica, 1, PCM non compresso.
  • 2 Byte: 02h 00h -> Numero di canali, 2.
  • 4 Byte: 44h ACh 00h 00h -> Frequenza di campionamento: 44100 Hz.
  • 4 Byte: 10h B1h 02h 00h -> Byte per secondo: 176400
  • 2 Byte: 04h 00h -> Block Align, 4 byte per ogni campione (totale per i 2 canali).
  • 2 Byte: 10h 00h -> Bits per campione e per canale: 16.

Il chunk data
  • Nel chunk 'data' sono memorizzati tutti i campioni. Ogni campione è rappresentato da uno o più byte e se sono presenti più canali, per ogni campione sono presenti i byte di ogni canale.
  • Il Block Align presenta il totale dei byte da leggere per ogni campione e per il totale dei canali.
  • Un segnale audio ha valori positivi e negativi essendo un valore che rappresenta una pressione istantanea (il suono è una variazione di pressione che giunge alle orecchie). Poiché invece ogni byte rappresenta un valore tra 0 e 255, occorre impostare una codifica che permetta di convertire valori positivi e negativi, in byte.
  • La codifica PCM prevede:
    • Se il campione necessita di un byte, il livello zero è 80h = 128. Valori superiori sono per la forma d'onda positiva, valori inferiori per quella negativa.
    • Se il campione necessita di due byte, allora il valore è memorizzato in complemento a 2 con un range compreso tra -32768 a +32767.

Esempio 2
  • Si considera ancora il file 'chimes.wav' contenuto in Windows 10.
  • La figura seguente mostra il file aperto con un editor esadecimale e la parte selezionata è il chunk 'data', fino al secondo campione.



  • 4 Byte: 64h 61h 74h 61h -> Stringa 'data', ID del chunk.
  • 4 Byte: 00h 4Dh 03h 00h -> Dimensione del chunk, 216.320 byte.
  • 4 Byte: 00h 00h FFh FFh -> 1° campione, canale 1: valore 00h 00h = 0, canale 2: valore FFh FFh = -1.
  • 4 Byte: FFh FFh 00h 00h -> 2° campione, canale 1: valore FFh FFh = -1, canale 2: valore 00h 00h = 0.
  • Essendo l'inizio della forma d'onda, il segnale è ancora basso, per questo i due campioni sono 0 e -1 (il range va da -32768 a +32767.
 
Audio - Decodificare i file WAV
Obiettivo
  • Il progetto seguente è la continuazione del progetto di decodifica del file RIFF. Vai alla pagina Audio - File RIFF.
  • Riassumendo, con i progetto di decodifica del file RIFF, si sono letti tutti i chunk del file fornito in input al programma. Se il file in esame è un file WAV (che è uno dei formati RIFF) ci si aspetta di trovare un chunk di tipo 'fmt ' e un chunk di tipo 'data'. Nei chunk 'fmt ' sono codificate tutte le informazioni sul formato di codifica.
  • Lo scopo del progetto è quello di mostrare le informazioni del chunk 'fmt ' in modo leggibile.

Organizzazione del progetto
  • Come spiegato nel paragrafo precedente, si dà per assodata la presenza del codice di decodifica del file RIFF.
  • Al form principale (form1) si aggiungono i seguenti oggetti:
    • Un Button che lancia la decodifica, btnWaveData.
    • Una ListBox dove si scrive l'esito della decodifica, lstWaveData. In questa ListBox si è impostato un Font monospace (per esempio Courier New) al fine di allineare i messaggi e renderli più leggibili.

Classi e metodi utili
  • La conversione da un array di byte in formato little endian a un valore intero, riceve in input un array di byte di lunghezza 4. Poiché si necessita della conversione anche per array di lunghezza 2, si fa la seguente modifica (si dovrebbe fare una modifica più strutturata e generica...).

   int GetLittleEndianIntegerFromByteArray(byte[] data, int startIndex)
   {
       if (data.Length == 2)
       {
           return (data[startIndex + 1] << 8)
               | data[startIndex];
       }
       else   // if (data.Length == 4)
       {
          return (data[startIndex + 3] << 24)
               | (data[startIndex + 2] << 16)
               | (data[startIndex + 1] << 8)
               | data[startIndex];
       }
   }

  • L'inserimento nella lista lstWaveData, necessita della funzione seguente.

   private void AddMessageToListWaveData(string msg)
   {
       lstWaveData.Items.Add(msg);
       lstWaveData.SelectedIndex = lstWaveData.Items.Count - 1;
   }

L'evento associato al Click del Button
  • Il codice è il seguente:

   private void btnWaveData_Click(object sender, EventArgs e)
   {
       // Lista Chunks non vuota.
       if (lstChunks.Count > 0)
       {
           // Il chunk principale è RIFF di tipo wave.
           if (lstChunks.FirstOrDefault(p => p.Progressive == 1 && p.ID == "RIFF" && p.FormatType == "WAVE") != null)
           {
               // Esiste un chunk di tipo fmt.
               Chunks c = lstChunks.FirstOrDefault(p => p.ID == "fmt ");
               if (c != null)
               {
                   int firstByte = c.FirstByteData;
                   int size = c.Size;
                   ShowWaveInfo(txtFile.Text, firstByte, size);
               }
           }
       }
   }

  • Si noti in sequenza:
    • La condizione che il file da elaborare sia un RIFF di tipo WAVE.
    • La condizione che esista un chunk 'fmt '.
    • La chiamata al metodo ShowWaveInfo(), descritto nel prossimo paragrafo.

Il metodo ShowWaveInfo()
  • Il codice è il seguente.

   private void ShowWaveInfo(string fileName, int firstByte, int size)
   {
       using (FileStream fsRead = new FileStream(fileName, FileMode.Open))
       {
           byte[] buffer = new byte[0];      // Buffer di lettura.
           byte[] subBuffer = new byte[0];   // Buffer di lettura.
           int read = 0;                     // Numero di byte letti.

           // Legge i 16 byte dati del chunk 'fmt '.
           fsRead.Seek(firstByte, SeekOrigin.Begin);
           buffer = new byte[16];
           read = fsRead.Read(buffer, 0, 16);

           // Tipo di codifica o compressione.
           subBuffer = new byte[2];
           Array.Copy(buffer, 0, subBuffer, 0, 2);
           int codType = GetLittleEndianIntegerFromByteArray(subBuffer, 0);
 
           switch (codType)
           {
               case 0: AddMessageToListWaveData("Tipo di codifica: Sconosciuto");
                   break;
               case 1: AddMessageToListWaveData("Tipo di codifica: PCM non compresso");
                   break;
               case 2: AddMessageToListWaveData("Tipo di codifica: Microsoft ADPCM");
                   break;
               case 6: AddMessageToListWaveData("Tipo di codifica: ITU G.711 a-law");
                   break;
               case 7: AddMessageToListWaveData("Tipo di codifica: ITU G.711 µ-law");
                   break;
               case 17: AddMessageToListWaveData("Tipo di codifica: IMA ADPCM");
                   break;
               case 22: AddMessageToListWaveData("Tipo di codifica: ITU G.723 ADPCM (Yamaha)");
                   break;
               case 49: AddMessageToListWaveData("Tipo di codifica: GSM 6.10");
                   break;
               case 64: AddMessageToListWaveData("Tipo di codifica: ITU G.721 ADPCM");
                   break;
               case 80: AddMessageToListWaveData("Tipo di codifica: MPEG");
                   break;
               case 255: AddMessageToListWaveData("Tipo di codifica: Experimental");
                   break;
               default: AddMessageToListWaveData("Tipo di codifica: Sconosciuto");
                   break;
           }

           // Numero di canali.
           subBuffer = new byte[2];
           Array.Copy(buffer, 2, subBuffer, 0, 2);
           int numChannels = GetLittleEndianIntegerFromByteArray(subBuffer, 0);
           AddMessageToListWaveData("Numero di canali: " + numChannels.ToString());

           // Frequenza di campionamento.
           subBuffer = new byte[4];
           Array.Copy(buffer, 4, subBuffer, 0, 4);
           int sampleFrequency = GetLittleEndianIntegerFromByteArray(subBuffer, 0);
           AddMessageToListWaveData("Frequenza di campionamento: " + sampleFrequency.ToString());

           // Bytes per secondo.
           subBuffer = new byte[4];
           Array.Copy(buffer, 8, subBuffer, 0, 4);
           int bytesPerSecond = GetLittleEndianIntegerFromByteArray(subBuffer, 0);
           AddMessageToListWaveData("Bytes per secondo: " + bytesPerSecond.ToString());

           // Block Align.
           subBuffer = new byte[2];
           Array.Copy(buffer, 12, subBuffer, 0, 2);
           int blockAlign = GetLittleEndianIntegerFromByteArray(subBuffer, 0);
           AddMessageToListWaveData("Block Align: " + blockAlign.ToString());

           // Bit per campione.
           subBuffer = new byte[2];
           Array.Copy(buffer, 14, subBuffer, 0, 2);
           int bitPerSample = GetLittleEndianIntegerFromByteArray(subBuffer, 0);
           AddMessageToListWaveData("Bit per campione: " + bitPerSample.ToString());
       }
   }

  • Si noti in sequenza:
    • L'apertura del file da analizzare.
    • La lettura dei 16 byte del chunk 'fmt '.
    • La decodifica dei 16 byte.

© 2022 Carlo Vecchio
Torna ai contenuti