\\ Home : Articoli : Stampa
DirectSound - Cattura Audio
Di RobyDx (del 16/12/2007 @ 12:22:49, in DirectX9, linkato 5522 volte)

Prima di iniziare faccio una critica a microsoft che nonostante abbia inventato un sistema eccellente per registrare i wav poteva automatizzare meglio e soprattutto degnarsi di realizzare un tutorial chiaro ed esaustivo. Ora iniziamo.
Per molte attività la possibilità di registrare sorgenti audio e video è fondamentale. DirectSound permette di registrare l'audio in formato wave tramite la scheda audio. Che il suono provenga da microfono o sia interna al sistema non fa differenza(ad esempio registrare un sorgente midi che sta girando nel pc). Il miglioramento rispetto al sistema presente in directX8 si nota soprattutto nel fatto che stavolta i dati vengono scritti durante la registrazione rendendo possibile registrare file wav di qualsiasi lunghezza e volendo registrare e sentire direttamente il suono (con appena un pò di differita). Nota negativa è che dovremo gestire il processo di salvataggio in modo diretto quindi un pò più complicato del solito.

Formati audio

I file audio wave si differenziano per 3 caratteristiche:

  • frequenza: l'orecchio umano è in grado di ascoltare un certo intervallo di frequenze. DirectSound è in grado di usare frequenze fra 8000hz e 48000Hz (oltre il campo udibile). Maggiore frequenza significa migliore qualità ma naturalmente maggiore impiego di memoria
  • channel: un suono può essere mono (emesso da una sola sorgente) o stereo (emesso da 2 sorgenti). Occupando 2 canali i wave stereo occupano il doppio
  • dimensione campione: sono i bits usati per il suono, 8 o 16.

    Dim cBuff As CaptureBuffer 'buffer di registrazione

    Questa variabile è il buffer di registrazione. Le altre variabili che ci serviranno sono

    Dim th As New Threading.Thread(AddressOf checkD) 'thread che salva ad intervalli regolari i dati nel file
    Public PositionNotify(16) As BufferPositionNotify 'posizioni di notifica
    Public appNotify As Notify 'notificatore
    Public NotificationEvent As AutoResetEvent = Nothing 'gestore delle attese nel thread
    Dim captureBufferSize As Integer 'dimensione del buffer
    Dim notifySize As Integer 'dimensione della notifica
    Public NextCaptureOffset As Integer = 0 'prossimo intervallo di cattura
    Private WaveFile As FileStream = Nothing 'file wave
    Private Writer As BinaryWriter = Nothing 'scrittore del file
    Private SampleCount As Integer = 0 'bit catturati
    Private isRecording As Boolean = False
    Const nNotification As Integer = 16

    verranno spiegate man mano che vengono adoperate
    Per avviare la registrazione occorre creare il buffer ed impostare le variabili che ci serviranno

    'descrizione del capBuffer
    Dim capD As CaptureBufferDescription
    Dim captureF As New Capture(guida)
    'formato
    Dim format As WaveFormat
    format.BitsPerSample = bitS
    format.Channels = canali
    format.SamplesPerSecond = frequenza
    format.BlockAlign = CShort(format.Channels * (format.BitsPerSample / 8))
    format.AverageBytesPerSecond = format.BlockAlign * format.SamplesPerSecond
    format.FormatTag = WaveFormatTag.Pcm
    capD.Format = format
    'creazione del fiff
    CreateRIFF(FileName, format)
    notifySize = IIf(1024 > format.AverageBytesPerSecond / 8, 1024, format.AverageBytesPerSecond / 8)
    notifySize -= notifySize Mod format.BlockAlign
    ' imposta la dimensione del buffer
    captureBufferSize = notifySize * nNotification
    ' Crea il capture buffer
    capD.BufferBytes = captureBufferSize
    cBuff = New CaptureBuffer(capD, captureF)
    NotificationEvent = New AutoResetEvent(False)
    'creazione notifica
    Dim i As Integer
    For i = 0 To nNotification - 1
    PositionNotify(i).Offset = notifySize * i + notifySize - 1
    PositionNotify(i).EventNotifyHandle = NotificationEvent.Handle
    Next
    appNotify = New Notify(cBuff)
    'imposta il notificatore affinchè generi l'evento nel thread
    appNotify.SetNotificationPositions(PositionNotify, nNotification)

    impostato il formato creiamo l'intestazione del file RIFF (che spieghiamo qui sotto). Il notifysize è la dimensione del buffer di notifica per la cattura. In pratica se registriamo 200 mega di wave questi non saranno accumulati dal capture buffer ma scaricati piano piano nel file. Il notifysize è appunto il calcolo di ogni quanto scaricare i dati nel file. Guida è il numero univoco che descrive la scheda audio da usare (spesso c'è ne sono diverse). da leggere con un processo di enumerazione

    Dim devices As New CaptureDevicesCollection
    Dim info As DeviceInformation
    For Each info In devices
    info.DriverGuid
    Next info

    info contiene tutte le caratteristiche. DriverGuid è la guida da usare
    Le altre caratteristiche vengono calcolate come mostrato nel codice
    per avviare la registrazione si avvia il thread e il capture buffer

    cBuff.Start(True)
    th.Start() 'inizia il thread
    Sub checkD()
    'thread che scarica sul disco i dati
    While isRecording
    NotificationEvent.WaitOne(Timeout.Infinite, True)
    saveRecord()
    End While
    End Sub

    Questa sub chiamata nel thread crea un attesa nel notificationEvent (che è collegato al buffer di cattura). Quando il capturebuffer è pronto per scaricare un pò di dati sblocca l'evento e da li chiamo la routine di registrazione.

    File Wave

    Come tutti i file esistenti il file wave è formato da un certo numero di descrittori e dai dati. I descrittori si trovano in cima al file e devono venire scritti prima di iniziare il processo di scrittura. Questa ruotine presa dall'esempio microsoft e modificata da me per adattarla al programma imposta il filestream ed il binaryWriter per poter scrivere dati binari nel file "in corsa" (ossia dinamicamente).

    Sub CreateRIFF(ByVal filename As String, ByVal inputFormat As WaveFormat)
    '*************************************************************************
    '
    '
    ' Here is where the file will be created. A
    '
    ' wave file is a RIFF file, which has chunks
    '
    ' of data that describe what the file contains.
    '
    ' A wave RIFF file is put together like this:
    '
    '
    '
    ' The 12 byte RIFF chunk is constructed like this:
    '
    ' Bytes(0 - 3) 'R' 'I' 'F' 'F'
    '
    ' Bytes 4 - 7 : Length of file, minus the first 8 bytes of the RIFF description.
    '
    ' (4 bytes for "WAVE" + 24 bytes for format chunk length +
    '
    ' 8 bytes for data chunk description + actual sample data size.)
    '
    ' Bytes(8 - 11) 'W' 'A' 'V' 'E'
    '
    '
    '
    ' The 24 byte FORMAT chunk is constructed like this:
    '
    ' Bytes(0 - 3) 'f' 'm' 't' ' '
    '
    ' Bytes 4 - 7 : The format chunk length. This is always 16.
    '
    ' Bytes 8 - 9 : File padding. Always 1.
    '
    ' Bytes 10- 11: Number of channels. Either 1 for mono, or 2 for stereo.
    '
    ' Bytes 12- 15: Sample rate.
    '
    ' Bytes 16- 19: Number of bytes per second.
    '
    ' Bytes 20- 21: Bytes per sample. 1 for 8 bit mono, 2 for 8 bit stereo or
    '
    ' 16 bit mono, 4 for 16 bit stereo.
    '
    ' Bytes 22- 23: Number of bits per sample.
    '
    '
    '
    ' The DATA chunk is constructed like this:
    '
    ' Bytes(0 - 3) 'd' 'a' 't' 'a'
    '
    ' Bytes 4 - 7 : Length of data, in bytes.
    '
    ' Bytes 8 -...: Actual sample data.
    '
    '
    '
    '**************************************************************************
    ' Open up the wave file for writing.
    WaveFile = New FileStream(filename, FileMode.Create)
    Writer = New BinaryWriter(WaveFile)
    ' Set up file with RIFF chunk info.
    Dim ChunkRiff As Char() = {"R", "I", "F", "F"}
    Dim ChunkType As Char() = {"W", "A", "V", "E"}
    Dim ChunkFmt As Char() = {"f", "m", "t", " "}
    Dim ChunkData As Char() = {"d", "a", "t", "a"}
    Dim shPad As Short = 1 ' File padding
    Dim nFormatChunkLength As Integer = &H10 ' Format chunk length.
    Dim nLength As Integer = 0 ' File length, minus first 8 bytes of RIFF description. This will be filled in later.
    Dim shBytesPerSample As Short = 0 ' Bytes per sample.
    ' Figure out how many bytes there will be per sample.
    If 8 = inputFormat.BitsPerSample And 1 = inputFormat.Channels Then
    shBytesPerSample = 1
    ElseIf 8 = inputFormat.BitsPerSample And 2 = inputFormat.Channels Or (16 = inputFormat.BitsPerSample And 1 = inputFormat.Channels) Then
    shBytesPerSample = 2
    ElseIf 16 = inputFormat.BitsPerSample And 2 = inputFormat.Channels Then
    shBytesPerSample = 4
    End If
    ' Fill in the riff info for the wave file.
    Writer.Write(ChunkRiff)
    Writer.Write(nLength)
    Writer.Write(ChunkType)
    ' Fill in the format info for the wave file.
    Writer.Write(ChunkFmt)
    Writer.Write(nFormatChunkLength)
    Writer.Write(shPad)
    Writer.Write(inputFormat.Channels)
    Writer.Write(inputFormat.SamplesPerSecond)
    Writer.Write(inputFormat.AverageBytesPerSecond)
    Writer.Write(shBytesPerSample)
    Writer.Write(inputFormat.BitsPerSample)
    ' Now fill in the data chunk.
    Writer.Write(ChunkData)
    Writer.Write(CInt(0)) ' The sample length will be written in later.
    End Sub 'CreateRIFF

    La descrizione di come è fatto un file wave può essere approfondita in numerosi siti ma in definitiva questo è tutto. Ora dobbiamo scrivere i dati.

    Sub saveRecord()
    Dim CaptureData As Byte() = Nothing
    Dim ReadPos As Integer
    Dim CapturePos As Integer
    Dim LockSize As Integer
    cBuff.GetCurrentPosition(CapturePos, ReadPos)
    LockSize = ReadPos - NextCaptureOffset
    If LockSize < 0 Then
    LockSize += captureBufferSize
    End If
    LockSize -= LockSize Mod notifySize
    If 0 = LockSize Then
    Return
    End If
    ' legge i dati
    CaptureData = CType(cBuff.Read(NextCaptureOffset, GetType(Byte), LockFlag.None, LockSize), Byte())
    'scrivi sul file
    Writer.Write(CaptureData, 0, CaptureData.Length)
    SampleCount += CaptureData.Length
    NextCaptureOffset += CaptureData.Length
    NextCaptureOffset = NextCaptureOffset Mod captureBufferSize
    End Sub

    Come vedete vengono letti i dati dal cBuff e scritti sul file. Le altre variabile spostano in avanti il prossimo intervallo di lettura.
    Per fermare la registrazione

    cBuff.Stop()
    isRecording = False
    saveRecord()
    Writer.Seek(4, SeekOrigin.Begin) 'porta il writer al descrittore di lunghezza del file RIFF
    Writer.Write(CInt(SampleCount + 36)) ' scrive la lunghezza del file meno i primi 8 bytes del descrittore RIFF
    Writer.Seek(40, SeekOrigin.Begin) ' porta il writer al descrittore della lunghezza dei dati
    Writer.Write(SampleCount) ' scrive la lunghezza dei dati
    Writer.Close() ' chiude il writer e tutto il resto
    Writer = Nothing
    WaveFile = Nothing
    NotificationEvent.Set() 'termina il processo di notifica

    Il writer scrive gli ultimi dati e riempie alcune zone all'inizio del file lasciate in sospeso (ossia la lunghezza del file e dei dati). Il file è ora chiuso e pronto ad essere ascoltato.
    Ultime note:

  • Non importa quanto è grande il file, le uniche risorse usate sono il disco fisso e la ram per far girare il programma che non sarà riempita con i dati se non nel buffer (cosa che però è fatta in genere dalla scheda audio).
  • Occhio ai formati di frequenza usati
  • Attenzione in fase di programmazione. La presenza di un thread significa che se non state attenti potreste chiudere il programma ma lasciare il thread a girare (e quindi a registrare). Siate sicuri quindi che il programma sia chiuso altrimenti potreste continuare a registrare dati.
    Vi lascio all'esempio che spero chiarirà tutto.
  • Esempio VB.Net