Page 1 sur 1

Bibliothèque son avec callback qu'on peut modifier avec ajout facile de filtres pour un exécutable indépendant

Publié : ven. 25/juil./2025 9:30
par det_uio
Objectif : ne pas importer de librairie C qui rend l'application dépendante d'une lib externe et pouvoir générer un exécutable non dépendant en respect de la philosophie Purebasic.

Faisabilité : créer dans Purebasic une bibliothèque avec son callback (comme portaudio et le temps réel) qu'on puisse modifier avec la possibilité d'y mettre des filtres sur mesures (passe haut, etc).

Inspiration pour la faisabilité: Voici un exemple qui fonctionne


Il s'agit d'un filtre passe bas IIR. Il faut modifier le chemin du wav à la ligne 253.

Code : Tout sélectionner

EnableExplicit

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; sndfile ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; --- Constantes libsndfile ---
#SFM_READ = $10 ; Mode lecture pour sf_open (0x10)
#SFM_WRITE = $20
#SFM_RDWR = $30
; --- Importation des fonctions libsndfile ---
ImportC "/opt/homebrew/Cellar/libsndfile/1.2.2_1/lib/libsndfile.1.0.37.dylib"
  sf_open(filename.p-utf8, mode.l, *sfinfo)
  ;   sf_open(filename.s, mode.l, *sfinfo)  ; filename.s = PureBasic String
  sf_strerror(*sndfile)
  sf_close(*sndfile)
  sf_read_float(*sndfile, *ptr_float, frames.q)
  sf_readf_float(*sndfile, *ptr_float, frames.q)
EndImport

; --- Structure SF_INFO de libsndfile ---
Structure SF_INFO Align #PB_Structure_AlignC
  frames.i       ; sf_count_t (nombre de frames) 8
  samplerate.l   ; int (fréquence d'échantillonnage) 4
  channels.l     ; int (nombre de canaux) 4
  format.l       ; int (format du fichier, ex: WAV, FLAC, etc.) 4
  sections.l     ; int (nombre de sections) 4
  seekable.l     ; int (indique si le fichier est seekable) 4
EndStructure

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; portaudio ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ==== Constantes ====
#paContinue = 0
#SampleRate = 44100
#FramesPerBuffer = 64
#TwoPi = 2 * #PI
#paFloat32 = $1
#paInt16   = $8
#paComplete = 1


Enumeration
  #paNoError = 0
  
  #paNotInitialized = -10000
  #paUnanticipatedHostError
  #paInvalidChannelCount
  #paInvalidSampleRate
  #paInvalidDevice
  #paInvalidFlag
  #paSampleFormatNotSupported
  #paBadIODeviceCombination
  #paInsufficientMemory
  #paBufferTooBig
  #paBufferTooSmall
  #paNullCallback
  #paBadStreamPtr
  #paTimedOut
  #paInternalError
  #paDeviceUnavailable
  #paIncompatibleHostApiSpecificStreamInfo
  #paStreamIsStopped
  #paStreamIsNotStopped
  #paInputOverflowed
  #paOutputUnderflowed
  #paHostApiNotFound
  #paInvalidHostApi
  #paCanNotReadFromACallbackStream
  #paCanNotWriteToACallbackStream
  #paCanNotReadFromAnOutputOnlyStream
  #paCanNotWriteToAnInputOnlyStream
  #paIncompatibleStreamHostApi
  #paBadBufferPtr
EndEnumeration



ImportC "/opt/homebrew/Cellar/portaudio/19.7.0/lib/libportaudio.dylib"
  Pa_Initialize()
  Pa_Terminate()
  Pa_GetDeviceCount()
  Pa_GetDeviceInfo(i) ;const PaDeviceInfo * Pa_GetDeviceInfo (PaDeviceIndex device)
  Pa_GetHostApiInfo(q);const PaHostApiInfo * Pa_GetHostApiInfo (PaHostApiIndex hostApi)
  Pa_GetErrorText(i)  ;const char * Pa_GetErrorText (PaError errorCode)
  Pa_StartStream(*stream)
  Pa_StopStream(*stream)
  Pa_CloseStream(*stream)
  Pa_IsStreamActive(*stream)
  
;   typedef unsigned long PaSampleFormat;
;   PaError Pa_OpenDefaultStream (PaStream **stream, int numInputChannels, int numOutputChannels, PaSampleFormat sampleFormat, double sampleRate, unsigned long framesPerBuffer, PaStreamCallback *streamCallback, void *userData)
  Pa_OpenDefaultStream(*stream, inputChannels, outputChannels.l, sampleFormat.q, sampleRate.d, framesPerBuffer.q, *callback, *userData)

  Pa_Sleep(ms)
EndImport

Structure PaDeviceInfo Align #PB_Structure_AlignC
  structVersion.l               ;4 bytes
  *name                        ; 8 bytes
  hostApi.l                     ;4 bytes. int
  maxInputChannels.l            ;4 bytes
  maxOutputChannels.l           ;4 bytes
  defaultLowInputLatency.d      ; 8 bytes
  defaultLowOutputLatency.d     ; 8 bytes
  defaultHighInputLatency.d     ; 8 bytes
  defaultHighOutputLatency.d    ; 8 bytes
  defaultSampleRate.d           ; 8 bytes
EndStructure
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

#FRAMES_PER_BUFFER = 512

Structure CanalRealTimeProcessing
  x.d[2]; Entrées
  y.d[2]; Sorties
EndStructure  


Structure PaData
  *file
  info.SF_INFO
  stop.b
  pause.b  ; Nouveau : état de pause
  *cg.CanalRealTimeProcessing
  *cd.CanalRealTimeProcessing
EndStructure


; ---------- procédure iir ----------
Procedure.f iir (input.f, *c.CanalRealTimeProcessing)
  Protected ta.l, i.l, yn.f
  
  Static Dim coeffA.d(1)
  Static Dim coeffB.d(1)
  Static init.b = #False
  
  If Not init
    coeffA(0) = 1.0 : coeffA(1) = -0.866788439500
    coeffB(0) = 0.066605780250 : coeffB(1) = 0.066605780250
    init = #True
  EndIf
  
    
  ta = ArraySize(coeffA()) + 1; taille (+1 car purebasic)
;   Debug ta
  
  ; Décalage des entrées
  For i = ta - 1 To 1 Step -1
;       Debug i
    *c\x[i] = *c\x[i - 1]
  Next i
    
  ; Nouvel échantillon d'entrée
  *c\x[0] = input
    
  ; Calcul de la sortie
  yn = 0.0
  
  ; Partie feedforward (numérateur)
  For i = 0 To ta-1
    yn + coeffB(i) * *c\x[i]
  Next i
    
  ; Partie feedback (dénominateur)
  For i = 1 To ta-1; commence à 1
    yn - coeffA(i) * *c\y[i]
  Next i
  
  ; Décalage de la sortie
  For i = ta-1 To 1 Step -1
    *c\y[i] = *c\y[i - 1]
  Next i
  
  ; Sauvegarder la sortie
  *c\y[0] = yn
  
  ProcedureReturn yn
  
EndProcedure



; === Callback audio ===
ProcedureC.l audio_callback(*inputBuffer, *outputBuffer, framesPerBuffer.q, *timeInfo, statusFlags.q, *userData)
  Protected *audioData.PaData = *userData
  Protected *audioOut.FLOAT = *outputBuffer
  Protected num_read.q
  Protected i.l, totalSamples.l, filtre.f, input.f
  
  totalSamples = framesPerBuffer * *audioData\info\channels
  
  If *audioData\stop
    For i = 0 To totalSamples - 1
      PokeF(*audioOut + i * SizeOf(Float), 0.0)
    Next
    ProcedureReturn #paComplete
  EndIf

  ; Nouveau : si en pause, remplir de silence
  If *audioData\pause
    For i = 0 To totalSamples - 1
      PokeF(*audioOut + i * SizeOf(Float), 0.0)
    Next
    ProcedureReturn #paContinue
  EndIf
  
  totalSamples = framesPerBuffer * *audioData\info\channels
  
  num_read = sf_read_float(*audioData\file, *audioOut, totalSamples)
  
  ;   Debug Str(totalSamples) + "   " + Str(num_read)
  
  ; canal de droite à 0
;   For i = 0 To num_read - 1 Step 2
;     PokeF(*audioOut + (i + 0) * SizeOf(Float), 0)
;   Next
  
  For i = 0 To num_read - 1 Step 2
    ; canal gauche
    input = PeekF(*audioOut + (i + 0) * SizeOf(Float))
    filtre = iir(input.f, *audioData\cg)
    PokeF(*audioOut + (i + 0) * SizeOf(Float), filtre)
    
    ; canal droit
    input = PeekF(*audioOut + (i + 1) * SizeOf(Float))
    filtre = iir(input.f, *audioData\cg)
    PokeF(*audioOut + (i + 1) * SizeOf(Float), filtre)    
  Next i
  
  ; fin de fichier wav : remplir le reste par des 0
  If num_read < framesPerBuffer
    For i = num_read To totalSamples - 1
      PokeF(*audioOut + i * SizeOf(Float), 0.0)
    Next
    ProcedureReturn #paComplete
  EndIf

  ProcedureReturn #paContinue
EndProcedure


; ================== Main ==================
; Define wavfile.s = OpenFileRequester("Sélectionnez un fichier WAV", "", "Fichiers WAV (*.wav)|*.wav", 0)
; If wavfile = ""
;   End
; EndIf
; Define audioData.PaData
; audioData\info\format = 0; sndfile le recommande avant ouverture
; audioData\file = sf_open(wavfile, #SFM_READ, @audioData\info)
; If audioData\file = 0
;   MessageRequester("Erreur", "Impossible d'ouvrir le fichier.")
;   End
; EndIf


Define wavfile$="/Users/uio/Music/_.wav"
Define audioData.PaData

Define canalGauche.CanalRealTimeProcessing
Define canalDroit.CanalRealTimeProcessing
audiodata\cg = @canalGauche
audiodata\cd = @canalDroit

audioData\info\format = 0; sndfile le recommande avant ouverture
audioData\file = sf_open(wavfile$, #SFM_READ, @audioData\info)
If audioData\file = 0
  MessageRequester("Erreur", "Impossible d'ouvrir le fichier.")
  End
EndIf

If Pa_Initialize() <> 0
  MessageRequester("Erreur", "Erreur initialisation PortAudio")
  sf_close(audioData\file)
  End
EndIf

Define *stream

If Pa_OpenDefaultStream(@*stream, 0, audioData\info\channels, #paFloat32, audioData\info\samplerate, #FRAMES_PER_BUFFER, @audio_callback(), @audioData) <> 0
  MessageRequester("Erreur", "Erreur ouverture flux audio")
  Pa_Terminate()
  sf_close(audioData\file)
  End
EndIf

If Pa_StartStream(*stream) <> 0
  MessageRequester("Erreur", "Erreur démarrage flux")
  Pa_CloseStream(*stream)
  Pa_Terminate()
  sf_close(audioData\file)
  End
EndIf

; === Fenêtre avec boutons Stop et Pause ===
OpenWindow(0, 0, 0, 350, 100, "Lecteur WAV", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
ButtonGadget(0, 50, 30, 80, 30, "Arrêter")
ButtonGadget(1, 140, 30, 80, 30, "Pause")
ButtonGadget(2, 230, 30, 80, 30, "Reprendre")

; === Boucle lecture audio + GUI ===
Repeat
  Select WaitWindowEvent(10)
    Case #PB_Event_Gadget
      Select EventGadget()
        Case 0  ; Bouton Arrêter
          audioData\stop = #True
        Case 1  ; Bouton Pause
          audioData\pause = #True
        Case 2  ; Bouton Reprendre
          audioData\pause = #False
      EndSelect

    Case #PB_Event_CloseWindow
      audioData\stop = #True
      Break
  EndSelect

  If Pa_IsStreamActive(*stream) = 0
    Break
  EndIf

  Pa_Sleep(10)
ForEver

; === Nettoyage ===
Pa_StopStream(*stream)
Pa_CloseStream(*stream)
Pa_Terminate()
sf_close(audioData\file)

; MessageRequester("Info", "Lecture terminée.")