Linux - PulseAudio - audio level capture module

Applications, Games, Tools, User libs and useful stuff coded in PureBasic
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Linux - PulseAudio - audio level capture module

Post by Kukulkan »

Hi,

I just did this module for myself and maybe it is of use for someone else. No idea if everything is correctly declared or done, but for me it works fine here. I thought this also might be a good starting point for someone else to enhance this...

Some test and example code is on the bottom of the code.

Restrictions:
* Only for getting the level, no WAV recording or playback implemented
* Returns only RMS, PEAK and DB levels for left and right signal
* Always acquiring stereo, 44100Khz and 16 bit values
* Only Linux
* Only tested on Kubuntu 14.04 and Ubuntu 14.04. If it works for you on other systems, please mention it.

Changes:
26 July 2016: Added DB level calculation

Code:

Code: Select all

; Small module to simply calculate the level of signals
; on a specific audio device.
;
; PB 5.24 LTS or newer
;
; Find the user documentation here:
; https://freedesktop.org/software/pulseaudio/doxygen/index.html#simple_sec
;
; (w) 2016 / V. Schmid

DeclareModule pulse
  
  ;{ PulseAudio Stuff
  Enumeration pa_sample_format
    #PA_SAMPLE_U8
    #PA_SAMPLE_ALAW
    #PA_SAMPLE_ULAW
    #PA_SAMPLE_S16LE
    #PA_SAMPLE_S16BE
    #PA_SAMPLE_FLOAT32LE
    #PA_SAMPLE_FLOAT32BE
    #PA_SAMPLE_S32LE
    #PA_SAMPLE_S32BE
    #PA_SAMPLE_S24LE
    #PA_SAMPLE_S24BE
    #PA_SAMPLE_S24_32LE
    #PA_SAMPLE_S24_32BE
    #PA_SAMPLE_MAX
    #PA_SAMPLE_INVALID = -1
  EndEnumeration
  
  Enumeration pa_stream_direction
    #PA_STREAM_NODIRECTION 	
    #PA_STREAM_PLAYBACK 	
    #PA_STREAM_RECORD 	
    #PA_STREAM_UPLOAD 	
  EndEnumeration
  
  Enumeration pa_channel_position
    #PA_CHANNEL_POSITION_INVALID = -1
    #PA_CHANNEL_POSITION_MONO = 0
    #PA_CHANNEL_POSITION_FRONT_LEFT
    #PA_CHANNEL_POSITION_FRONT_RIGHT
    #PA_CHANNEL_POSITION_FRONT_CENTER
    #PA_CHANNEL_POSITION_LEFT = #PA_CHANNEL_POSITION_FRONT_LEFT
    #PA_CHANNEL_POSITION_RIGHT = #PA_CHANNEL_POSITION_FRONT_RIGHT
    #PA_CHANNEL_POSITION_CENTER = #PA_CHANNEL_POSITION_FRONT_CENTER
    #PA_CHANNEL_POSITION_REAR_CENTER
    #PA_CHANNEL_POSITION_REAR_LEFT
    #PA_CHANNEL_POSITION_REAR_RIGHT
    #PA_CHANNEL_POSITION_LFE
    #PA_CHANNEL_POSITION_SUBWOOFER = #PA_CHANNEL_POSITION_LFE
    #PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER
    #PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER
    #PA_CHANNEL_POSITION_SIDE_LEFT
    #PA_CHANNEL_POSITION_SIDE_RIGHT
    #PA_CHANNEL_POSITION_AUX0
    #PA_CHANNEL_POSITION_AUX1
    #PA_CHANNEL_POSITION_AUX2
    #PA_CHANNEL_POSITION_AUX3
    #PA_CHANNEL_POSITION_AUX4
    #PA_CHANNEL_POSITION_AUX5
    #PA_CHANNEL_POSITION_AUX6
    #PA_CHANNEL_POSITION_AUX7
    #PA_CHANNEL_POSITION_AUX8
    #PA_CHANNEL_POSITION_AUX9
    #PA_CHANNEL_POSITION_AUX10
    #PA_CHANNEL_POSITION_AUX11
    #PA_CHANNEL_POSITION_AUX12
    #PA_CHANNEL_POSITION_AUX13
    #PA_CHANNEL_POSITION_AUX14
    #PA_CHANNEL_POSITION_AUX15
    #PA_CHANNEL_POSITION_AUX16
    #PA_CHANNEL_POSITION_AUX17
    #PA_CHANNEL_POSITION_AUX18
    #PA_CHANNEL_POSITION_AUX19
    #PA_CHANNEL_POSITION_AUX20
    #PA_CHANNEL_POSITION_AUX21
    #PA_CHANNEL_POSITION_AUX22
    #PA_CHANNEL_POSITION_AUX23
    #PA_CHANNEL_POSITION_AUX24
    #PA_CHANNEL_POSITION_AUX25
    #PA_CHANNEL_POSITION_AUX26
    #PA_CHANNEL_POSITION_AUX27
    #PA_CHANNEL_POSITION_AUX28
    #PA_CHANNEL_POSITION_AUX29
    #PA_CHANNEL_POSITION_AUX30
    #PA_CHANNEL_POSITION_AUX31
    #PA_CHANNEL_POSITION_TOP_CENTER
    #PA_CHANNEL_POSITION_TOP_FRONT_LEFT
    #PA_CHANNEL_POSITION_TOP_FRONT_RIGHT
    #PA_CHANNEL_POSITION_TOP_FRONT_CENTER
    #PA_CHANNEL_POSITION_TOP_REAR_LEFT
    #PA_CHANNEL_POSITION_TOP_REAR_RIGHT
    #PA_CHANNEL_POSITION_TOP_REAR_CENTER
    #PA_CHANNEL_POSITION_MA
  EndEnumeration
  ;}
  
  Structure vu_result
    peakLeft.f
    peakRight.f
    rmsLeft.f
    rmsRight.f
    dbLeft.f
    dbRight.f
  EndStructure
  
  #PA_BUFSIZE = 1024 ; rec buffer / turned out to be best here / about 160 calls a second
  
  Declare.i initLibrary()
  Declare shutdown()
  Declare.s getVersion()
  Declare.i closeStream()
  Declare.i initStream(appName.s, streamName.s = "", deviceName.s = "")
  Declare.s errToStr(error.i)
  Declare.i getVU(*values.vu_result)
EndDeclareModule

Module pulse
  
  Structure pa_buffer_attr
    maxlength.l
    tlength.l
    prebuf.l
    minreq.l
    fragsize.l
  EndStructure
    
  Structure pa_sample_spec
    pa_sample_format.l
    rate.l
    channels.a
  EndStructure
  
  Structure pa_channel_map
    channels.a
    pa_channel_position.l
  EndStructure
  
  Global pulseLib.i
  Global *libBuf
  Global stream.i
  Global *ss.pa_sample_spec
  
  ; prototypes for pulseAudio
  Prototype.l pa_simple_new(*server, name.p-ascii, dir.l, *dev, stream_name.p-ascii, ss.l, mp.l, attr.l, error.l)
  Prototype.l pa_simple_read(*s, *Data, bytes.l, error.l)
  Prototype.l pa_simple_free(*s)
  Prototype.l pa_get_library_version()
  Prototype.l pa_strerror(error.l)
  Prototype.l pa_simple_flush(*s, error.l)
  
  Procedure.i initLibrary()
    ; Open libPulse library for "simple" interface
    pulseLib.i = OpenLibrary(#PB_Any, "libpulse-simple.so")
    If pulseLib.i = 0
      Debug "No pulse lib loaded. Check for 'libpulse-simple.so'."
      ProcedureReturn #False
    EndIf
    
    ; assign prototype functions
    Global pa_get_library_version.pa_get_library_version = GetFunction(pulseLib, "pa_get_library_version")
    Global pa_simple_new.pa_simple_new = GetFunction(pulseLib, "pa_simple_new")
    Global pa_strerror.pa_strerror = GetFunction(pulseLib, "pa_strerror")
    Global pa_simple_read.pa_simple_read = GetFunction(pulseLib, "pa_simple_read")
    Global pa_simple_free.pa_simple_free = GetFunction(pulseLib, "pa_simple_free")
    Global pa_simple_flush.pa_simple_flush = GetFunction(pulseLib, "pa_simple_flush")
    
    ProcedureReturn #True
  EndProcedure
  
  Procedure shutdown()
    closeStream()
    If IsLibrary(pulseLib): CloseLibrary(pulseLib): EndIf
  EndProcedure
  
  Procedure.s errToStr(error.i)
    Protected p.i = pa_strerror(error.i)
    ProcedureReturn "Error " + Str(error.i) + ": " + PeekS(p.i, -1, #PB_Ascii)
  EndProcedure
  
  Procedure.s getVersion()
    Protected p.i = pa_get_library_version()
    ProcedureReturn "V" + PeekS(p.i, -1, #PB_Ascii)
  EndProcedure
  
  Procedure.i closeStream()
    If stream.i <> 0
      pa_simple_free(stream.i)
      stream.i = 0
    EndIf
    If *libBuf <> 0
      FreeMemory(*libBuf)
      *libBuf = 0
    EndIf
    ProcedureReturn #True
  EndProcedure
  
  ; Initialize a new recording stream
  ; appName and streamName are free to choose (eg use your app name)
  ; deviceName must be a PulseAudio input devide like "alsa_input.pci-0000_00_1b.0.analog-stereo"
  ; You can get a list on your PC using this command-line: 
  ;   pactl list | grep Name
  Procedure.i initStream(appName.s, streamName.s = "", deviceName.s = "")
    If IsLibrary(pulseLib) = 0
      Debug "Need to initialize the lib first using initLibrary() function."
      ProcedureReturn 0
    EndIf
    If streamName.s = "": streamName.s = appName.s: EndIf
    
    ; close previous stream
    closeStream()
    
    Protected error.l = 0
    ; open record stream
    *ss.pa_sample_spec = AllocateMemory(SizeOf(pa_sample_spec))
    *ss\channels = 2
    *ss\rate = 44100
    *ss\pa_sample_format = #PA_SAMPLE_S16LE
    If deviceName.s = ""
      stream.i = pa_simple_new(0, appName.s, #PA_STREAM_RECORD, 0, streamName.s, *ss, 0, 0, @error.l)
    Else
      Protected p.s = Space(Len(deviceName))
      PokeS(@p.s, deviceName.s, -1, #PB_Ascii)
      stream.i = pa_simple_new(0, appName.s, #PA_STREAM_RECORD, @p, streamName.s, *ss, 0, 0, @error.l)
    EndIf
    If error.l <> 0 Or stream.i = 0
      ProcedureReturn error.l
    EndIf
    
    *libBuf = AllocateMemory(#PA_BUFSIZE)
    
    ProcedureReturn 0 ; success
    
  EndProcedure
  
  ; Get a buffer of audio data and calcumate PEAK and RMS values of it
  Procedure.i getVU(*values.vu_result)
    If IsLibrary(pulseLib) = 0
      Debug "Need to initialize the lib first using initLibrary() function."
      ProcedureReturn -1
    EndIf
    If stream.i = 0
      Debug "Need to open a stream first using initStream() function."
      ProcedureReturn -1
    EndIf
    
    Protected error.l = 0
    If pa_simple_read(stream.i, *libBuf, #PA_BUFSIZE, @error.l) < 0
      Debug "pa_simple_read() failed: " + errToStr(error.l)
      ProcedureReturn error.l
    EndIf
    
    Protected levLeft.i = 0,  levRight.i = 0
    Protected peakLeft.i = 0, peakRight.i = 0
    Protected rmsLeft.q = 0,  rmsRight.q = 0
    Protected x.i = 0
    For x = 0 To #PA_BUFSIZE - 1 Step 4
      levLeft = Abs(PeekW(*libBuf + x))
      levRight = Abs(PeekW(*libBuf + x + 2))
      
      If levLeft > peakLeft: peakLeft = levLeft: EndIf
      If levRight > peakRight: peakRight = levRight: EndIf
      rmsLeft = rmsLeft + (levLeft * levLeft)
      rmsRight = rmsRight + (levRight * levRight)
    Next
    
    Protected dbFact.i = 4096
    
    *values\rmsLeft = Sqr(rmsLeft / (#PA_BUFSIZE / 4))
    *values\rmsRight= Sqr(rmsRight / (#PA_BUFSIZE / 4))
    *values\peakLeft = peakLeft
    *values\peakRight = peakRight
    *values\dbLeft = dbFact.i * Log10(*values\rmsLeft)
    *values\dbRight = dbFact.i * Log10(*values\rmsRight)
    
    ProcedureReturn 0
  EndProcedure
  
EndModule

CompilerIf #PB_Compiler_IsMainFile
  
  ; PulseAudio test and example code
  If pulse::initLibrary()
    Debug "Initialized PulseAudio " + pulse::getVersion()
    Define err.i = pulse::initStream("PulseAudio Test", "record")
    Define values.pulse::vu_result ; define return value
    If err.i <> 0
      Debug pulse::errToStr(err.i)
    Else
      ; get signals for the next 5 seconds (this is demo only!)
      Define start.i = ElapsedMilliseconds()
      Repeat
        pulse::getVU(values)
        ; output the values
        Debug Str(values\peakLeft) + #TAB$ + 
              Str(values\peakRight) + #TAB$ + 
              Str(values\rmsLeft) + #TAB$ + 
              Str(values\rmsRight)
        
      Until ElapsedMilliseconds() > start.i + 1000
      
      pulse::closeStream()
    EndIf
    
  EndIf
  pulse::shutdown()
  
CompilerEndIf