Playing a simple tone with PureBasic and PortAudio

Just starting out? Need help? Post your questions and find answers here.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Playing a simple tone with PureBasic and PortAudio

Post by wilbert »

Here's another modification

Code: Select all

XIncludeFile "PortAudio.pb"

#MinFreq = 100
#MaxFreq = 10000
#SampleRate = 44100

#PI2 = 6.28318530717958

Enumeration
  #WIN_MAIN
  #PlayPauseButton
  #SelectedWave
  #FrequenceText
  #FrequenceString
  #FrequenceTrack
EndEnumeration

Enumeration
  #Sine
  #Saw
  #Square
EndEnumeration

Procedure Error(err)
  Debug Pa_GetErrorText(err)
  MessageRequester("", PeekS(Pa_GetErrorText(err)))
  End
EndProcedure


Prototype.d WaveFormFnProto(Phh.d)

Global Stream, Streaming, WaveFormFn.WaveFormFnProto
Global Phase.d, PhaseAdd.d


Procedure.d Sine(Phh.d)
  ProcedureReturn Phh
EndProcedure


Procedure.d Saw(Phh.d)
  
  If Phh < 0.5
    ProcedureReturn 0.35 * Sin(Phh)
  Else
    ProcedureReturn (0.35 * Sin(1 - Phh)) + 0.5
  EndIf
  
EndProcedure


Procedure.d Sq(Phh.d)
  
  If Phh < 0.5
    ProcedureReturn 0.25
  Else
    ProcedureReturn 0.75
  EndIf
  
EndProcedure


ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData)
  
  While frameCount
    *output\f = Sin(#PI2 * WaveFormFn(Phase)) 
    Phase + PhaseAdd
    If Phase > 1 : Phase = Phase - 1 : EndIf
    *output + 4
    frameCount - 1  
  Wend
  
EndProcedure




err = Pa_Initialize()
If err <> #paNoError : Error(err) : EndIf

op.PaStreamParameters
op\channelCount = 1
op\device = Pa_GetDefaultOutputDevice()
op\sampleFormat = #paFloat32
op\suggestedLatency = 150/1000

err = Pa_OpenStream(@Stream, #Null, @op, #SampleRate, 0, #paNoFlag, @PaStreamCallback(), 0)
If err <> #paNoError : Error(err) : EndIf

If OpenWindow(#WIN_MAIN, 0, 0, 200, 180, "Interaction", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
  ComboBoxGadget(#SelectedWave, 40, 20, 140, 20)
  AddGadgetItem(#SelectedWave, -1, "Sine")
  AddGadgetItem(#SelectedWave, -1, "Saw")
  AddGadgetItem(#SelectedWave, -1, "Square")
  SetGadgetState(#SelectedWave,0)
  WaveFormFn = @Sine()
  
  TrackBarGadget(#FrequenceTrack, 10, 10, 20, 110, #MinFreq, #MaxFreq, #PB_TrackBar_Vertical)
  TextGadget(#FrequenceText, 40, 70, 80, 20, "Frequency:")
  StringGadget(#FrequenceString, 40, 90, 60, 20, "200",#PB_String_Numeric)
  SetGadgetState(#FrequenceTrack, 200)
  
  ButtonGadget(#PlayPauseButton, 10, 130, 180, 30, "Play")
  
  Quit = #False
  Repeat
    Event = WaitWindowEvent()
    Select Event
      Case #PB_Event_Gadget
        Select EventGadget()
            
          Case #SelectedWave  
            Select GetGadgetState(#SelectedWave)
              Case #Sine
                WaveFormFn = @Sine()
              Case #Saw
                WaveFormFn = @Saw()
              Case #Square
                WaveFormFn = @Sq()
            EndSelect
            
          Case #FrequenceTrack
            SetGadgetText(#FrequenceString, Str(GetGadgetState(#FrequenceTrack)))
          Case #FrequenceString
            Select EventType()
              Case #PB_EventType_Change
                Value = Val(GetGadgetText(#FrequenceString))
                If Value < #MinFreq : Value = #MinFreq
                  ElseIf Value > #MaxFreq : Value = #MaxFreq : EndIf
                SetGadgetState(#FrequenceTrack, Value)
              Case #PB_EventType_LostFocus
                SetGadgetText(#FrequenceString, Str(GetGadgetState(#FrequenceTrack)))
            EndSelect
            
          Case #PlayPauseButton
            Streaming = ~Streaming
            If Streaming
              Pa_StartStream(Stream)
              SetGadgetText(#PlayPauseButton, "Pause")
            Else
              Pa_StopStream(Stream)
              SetGadgetText(#PlayPauseButton, "Play")
            EndIf
            
        EndSelect
        
        Freq = Val(GetGadgetText(#FrequenceString))
        PhaseAdd = Freq / #SampleRate
        
      Case #PB_Event_CloseWindow
        Quit = #True
    EndSelect
  Until Quit
  
EndIf

Pa_Terminate()
infratec
Always Here
Always Here
Posts: 7662
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Playing a simple tone with PureBasic and PortAudio

Post by infratec »

Best of both worlds

Code: Select all

XIncludeFile "PortAudio.pb"

#MinFreq = 100
#MaxFreq = 10000
#SampleRate = 44100

#PI2 = 6.28318530717958

Enumeration
  #WIN_MAIN
  #PlayButton
  #SelectedWave
  #FrequenceText
  #FrequenceString
  #FrequenceTrack
EndEnumeration


Enumeration
  #Sine
  #Saw
  #Square
EndEnumeration


Prototype.d WaveFormFnProto(Phh.d)


Structure UserDataStr
  WaveFormFn.WaveFormFnProto
  PhaseAdd.d
EndStructure

Global UserData.UserDataStr


Procedure Error(err)
  Debug Pa_GetErrorText(err)
  MessageRequester("", PeekS(Pa_GetErrorText(err)))
  End
EndProcedure


Procedure.d Sine(Phh.d)
  ProcedureReturn Phh
EndProcedure


Procedure.d Saw(Phh.d)
  If Phh < 0.5
   ProcedureReturn 0.35 * Sin(Phh)
  Else
   ProcedureReturn (0.35 * Sin(1 - Phh)) + 0.5
  EndIf
EndProcedure


Procedure.d Sq(Phh.d)
  If Phh < 0.5
    ProcedureReturn 0.25
  Else
    ProcedureReturn 0.75
  EndIf
EndProcedure


ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData.UserDataStr)
  
  Static Phase.d = 0.0
  
  For i = 1 To frameCount
    ;PokeF(*output, Sin(#PI2 * *UserData\WaveFormFn(Phase)))
    *output\f = Sin(#PI2 * *UserData\WaveFormFn(Phase))
    Phase = Phase + *userData\PhaseAdd
    If Phase > 1 : Phase = Phase - 1 : EndIf
    *output + 4
  Next i

EndProcedure


Procedure.l PlayWave(WaveForm, SampleRate.d, Freq)
  
  Protected stream.l
  
  err = Pa_Initialize()
  If err <> #paNoError : Error(err) : EndIf
  
  op.PaStreamParameters
  op\channelCount = 1
  ;op\device = 7
  op\device = Pa_GetDefaultOutputDevice()
  op\sampleFormat = #paFloat32
  op\suggestedLatency = 150/1000
  
  err = Pa_OpenStream(@stream, #Null, @op, SampleRate, 0, #paNoFlag, @PaStreamCallback(), @UserData)
  If err <> #paNoError : Error(err) : EndIf
  
  Pa_StartStream(stream)
  
  ProcedureReturn stream
  
EndProcedure


Procedure StopWave(stream.l)
  Pa_StopStream(stream)
  Pa_Terminate()
EndProcedure





If OpenWindow(#WIN_MAIN, 0, 0, 140, 180, "Interaction", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
  ComboBoxGadget(#SelectedWave, 40, 20, 60, 20)
  AddGadgetItem(#SelectedWave, -1, "Sine")
  AddGadgetItem(#SelectedWave, -1, "Saw")
  AddGadgetItem(#SelectedWave, -1, "Square")
  SetGadgetState(#SelectedWave,0)
  
  TrackBarGadget(#FrequenceTrack, 10, 10, 20, 110, #MinFreq, #MaxFreq, #PB_TrackBar_Vertical)
  TextGadget(#FrequenceText, 40, 70, 80, 20, "Frequency:")
  StringGadget(#FrequenceString, 40, 90, 60, 20, "200",#PB_String_Numeric)
  SetGadgetState(#FrequenceTrack, 200)
    
  ButtonGadget(#PlayButton, 10, 130, 120, 30, "Play")
  
  UserData\WaveFormFn = @Sine()
  UserData\PhaseAdd = GetGadgetState(#FrequenceTrack) / #SampleRate
  
  Quit = #False
  Repeat
    Event = WaitWindowEvent()
    Select Event
      Case #PB_Event_Gadget
        Select EventGadget()
          Case #SelectedWave
            If EventType() = #PB_EventType_Change
              Select GetGadgetState(#SelectedWave)
                Case #Sine : UserData\WaveFormFn = @Sine()
                Case #Saw : UserData\WaveFormFn = @Saw()
                Case #Square : UserData\WaveFormFn = @Sq()
              EndSelect
            EndIf
          Case #FrequenceTrack
            SetGadgetText(#FrequenceString, Str(GetGadgetState(#FrequenceTrack)))
            UserData\PhaseAdd = GetGadgetState(#FrequenceTrack) / #SampleRate
          Case #FrequenceString
            Select EventType()
              Case #PB_EventType_Change
                Value = Val(GetGadgetText(#FrequenceString))
                If Value < #MinFreq : Value = #MinFreq
                ElseIf Value > #MaxFreq : Value = #MaxFreq : EndIf
                SetGadgetState(#FrequenceTrack, Value)
                UserData\PhaseAdd = GetGadgetState(#FrequenceTrack) / #SampleRate
              Case #PB_EventType_LostFocus
                SetGadgetText(#FrequenceString, Str(GetGadgetState(#FrequenceTrack)))
                UserData\PhaseAdd = GetGadgetState(#FrequenceTrack) / #SampleRate
            EndSelect
          Case #PlayButton
            If GetGadgetText(#PlayButton) = "Play"
              SetGadgetText(#PlayButton, "Stop")
              Stream.l = PlayWave(GetGadgetState(#SelectedWave), #SampleRate, Val(GetGadgetText(#FrequenceString)))
            Else
              StopWave(Stream)
              SetGadgetText(#PlayButton, "Play")
            EndIf
        EndSelect
      Case #PB_Event_CloseWindow
        Quit = #True
    EndSelect
  Until Quit

EndIf
Bernd
Last edited by infratec on Sun Oct 30, 2011 11:03 am, edited 1 time in total.
infratec
Always Here
Always Here
Posts: 7662
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Playing a simple tone with PureBasic and PortAudio

Post by infratec »

Hi, hi,

if you make

Code: Select all

Global UserData.UserDataStr
and put

Code: Select all

UserData\PhaseAdd = GetGadgetState(#FrequenceTrack) / #SampleRate
after

Code: Select all

SetGadgetText(#FrequenceString, Str(GetGadgetState(#FrequenceTrack)))
than you can change the frequency 'on the fly'

Bernd
doctornash
Enthusiast
Enthusiast
Posts: 130
Joined: Thu Oct 20, 2011 7:22 am

Re: Playing a simple tone with PureBasic and PortAudio

Post by doctornash »

thanks infratec and wilbert! before i graduate to understanding some of the 'advanced' concepts in the 'best of both worlds' code (which works great) such as prototyping, infratec when i run your original suggestion, it appears to continue to play the sine even when one of the other waveforms is chosen from the combo box...minor alteration needed?
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Playing a simple tone with PureBasic and PortAudio

Post by wilbert »

A prototype allows you to use a variable as if it were a function. You can assign the pointer to the function at runtime. It is usually used for external libraries but can also be used to point to another function inside your program.

Two little remarks ...
1. PokeF makes a function call. Using *output\f should be faster.
2. It might be best to use op\device = Pa_GetDefaultOutputDevice() instead of assigning a number directly. I'm using OS X (so it is cross platform) and the device number isn't the same as on Windows.
infratec
Always Here
Always Here
Posts: 7662
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Playing a simple tone with PureBasic and PortAudio

Post by infratec »

Hi doctornash,

I replaced the code in 'Best of both worlds'.

It includes now the latest suggestions from wilbert and I also added the possibility to change the
waveform 'on the fly'.

But: it was only for playing a bit.
I don't think that it is phase correct.

Have fun.

Bernd
doctornash
Enthusiast
Enthusiast
Posts: 130
Joined: Thu Oct 20, 2011 7:22 am

Re: Playing a simple tone with PureBasic and PortAudio

Post by doctornash »

something a bit weird: I have purebasic+portaudio running on two laptops: an acer Win7 netbook, and a dell latitude d600 WinXP. my original code and wilbert's code suggestion run fine on both machines. the 'best of both worlds' runs fine on the acer, but on the dell the app continues to play the sine even when saw or square are selected from the combo-box :?
doctornash
Enthusiast
Enthusiast
Posts: 130
Joined: Thu Oct 20, 2011 7:22 am

Re: Playing a simple tone with PureBasic and PortAudio

Post by doctornash »

How would one go about playing a pre-filled array of waveform data through the portaudio callback? For example, if I do this:

Code: Select all

Global Fase.d
Global Fase_Add.d
Fase_Add = 200/44100
Global Dim QArr.d(88200)
For i = 0 To 88200
  Qarr(i) = Sin(Pi2*Sine(Fase))
  Fase = Fase + Fase_Add
    If Fase > 1
    Fase = Fase - 1
    EndIf 
Next
I've just filled an array with 2 seconds worth of 200Hz sine wave data sampled at 44.1kHz, right?
Now when I try to stream this through the portaudio callback like so:

Code: Select all

ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData)
  For I = 1 To frameCount
    *output\f  = QArr(I)
    *output+4
  Next
EndProcedure
I hear a tone, but it is distorted. What am I doing wrong?
infratec
Always Here
Always Here
Posts: 7662
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Playing a simple tone with PureBasic and PortAudio

Post by infratec »

Hi,

try something like this

Code: Select all

ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData.UserDataStr)
  Static n = 0

  For i = 1 To frameCount
    *output\f = QArr(n)
    *output+4
    n + 1
    If n = 88200 : n = 0 : EndIf
  Next
EndProcedure
And don't forget to tell PlayWave the right frequency.

Bernd
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Playing a simple tone with PureBasic and PortAudio

Post by wilbert »

You could use Pa_WriteStream

Code: Select all

XIncludeFile "PortAudio.pb"

#PI2 = 6.28318530717958

#SampleRate = 44100
#NumSamples = #SampleRate * 2
#LastSample = #NumSamples - 1

Dim SampleArr.f(#LastSample)


Procedure ErrorCheck(err)
  If err <> #paNoError
    MessageRequester("", PeekS(Pa_GetErrorText(err), -1, #PB_Ascii))
    End
  EndIf
EndProcedure


Procedure.d Sine(Phh.d)
  ProcedureReturn Phh
EndProcedure


ErrorCheck(Pa_Initialize()) 

op.PaStreamParameters
op\channelCount = 1
op\device = Pa_GetDefaultOutputDevice()
op\sampleFormat = #paFloat32
op\suggestedLatency = 150 / 1000

ErrorCheck(Pa_OpenStream(@Stream, #Null, @op, #SampleRate, 0, 0, 0, 0))

Fase.d = 0
Fase_Add.d = 200 / #SampleRate
For i = 0 To #LastSample
  SampleArr(i) = Sin(#PI2 * Sine(Fase))
  Fase = Mod(Fase + Fase_Add, 1)
Next

ErrorCheck(Pa_StartStream(Stream))
ErrorCheck(Pa_WriteStream(Stream, @SampleArr(0), #NumSamples))

Pa_Terminate()
Here's an example of a 5 sec gliding tone

Code: Select all

XIncludeFile "PortAudio.pb"

#PI2 = 6.28318530717958

#SampleRate = 44100
#NumSamples = #SampleRate * 5
#LastSample = #NumSamples - 1

Dim SampleArr.f(#LastSample)


Procedure ErrorCheck(err)
  If err <> #paNoError
    MessageRequester("", PeekS(Pa_GetErrorText(err), -1, #PB_Ascii))
    End
  EndIf
EndProcedure


Procedure.d Sine(Phh.d)
  ProcedureReturn Phh
EndProcedure


ErrorCheck(Pa_Initialize()) 

op.PaStreamParameters
op\channelCount = 1
op\device = Pa_GetDefaultOutputDevice()
op\sampleFormat = #paFloat32
op\suggestedLatency = 150 / 1000

ErrorCheck(Pa_OpenStream(@Stream, #Null, @op, #SampleRate, 0, 0, 0, 0))

StartFreq.d = 100
DeltaFreq.d = 400

Fase.d = 0

For i = 0 To #LastSample
  
  Freq.d = StartFreq + DeltaFreq * i / #LastSample 
  Fase_Add.d = Freq / #SampleRate
  
  SampleArr(i) = Sin(#PI2 * Sine(Fase))
  Fase = Mod(Fase + Fase_Add, 1)
    
Next

ErrorCheck(Pa_StartStream(Stream))
ErrorCheck(Pa_WriteStream(Stream, @SampleArr(0), #NumSamples))

Pa_Terminate()
doctornash
Enthusiast
Enthusiast
Posts: 130
Joined: Thu Oct 20, 2011 7:22 am

Re: Playing a simple tone with PureBasic and PortAudio

Post by doctornash »

Brilliant! Infratec, your suggestion worked like charm, and wilbert the pa_writestream method yields fine results too :D

btw, when i code directsound directly with vb6, I have to specify the bits per sample when creating the soundbuffer (eg 16 bits in which case i'd multiply the waveform data by 32767 before buffering); how is bit depth being set here?

Now that we have the basics of playing waveform data from an array and control from a simple GUI sorted out, I'd like to verify how the synthesized audio from an application I previously created, sounds through purebasic->portaudio (once I've done that, I will proceed to build and enhance that app in purebasic itself). So, to verify playback I have output a textfile of samples in the range [-1,1] from that application. Now, I can import this file into a string array like so:

Code: Select all

Global x.l
Global T.l
Dim MyArr.s(0)
x = -1
If ReadFile(1, "TestAudio.txt")
  While Eof(1) = #False
    x = x+1
    ReDim myarr(x)
    myarr(x) = ReadString(1)
  Wend
  CloseFile(1)
EndIf
but how to convert the strings representing each value to an array of doubles (or floats), in order to play the file?
doctornash
Enthusiast
Enthusiast
Posts: 130
Joined: Thu Oct 20, 2011 7:22 am

Re: Playing a simple tone with PureBasic and PortAudio

Post by doctornash »

ahh...answered my own question: use ValD :oops:

Dim MyArr.d(0)
MyArr(x) = ValD(ReadString(1))
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Playing a simple tone with PureBasic and PortAudio

Post by wilbert »

The bit depth is specified by the sampleFormat.
op\sampleFormat = #paFloat32
Other possibilities are #paInt32, #paInt24, #paInt16, #paInt8 and #paUInt8 .

So for the 16 bit integer like you had, it would be
op\sampleFormat = #paInt16

The callback of course also would have to be changed in this case

Code: Select all

ProcedureC PaStreamCallback(*in, *output.Word, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData.UserDataStr)
  Static n = 0

  For i = 1 To frameCount
    *output\w = QArr(n) * 32767
    *output+2
    n + 1
    If n = 88200 : n = 0 : EndIf
  Next
EndProcedure
Edit:
#paFloat32 might be the best format to use.
If QArr(n) would contain a value outside of [-1, 1] it handles this rather well. When you use #paInt16 and multiply such a value with 32767, it distorts the sound.
doctornash
Enthusiast
Enthusiast
Posts: 130
Joined: Thu Oct 20, 2011 7:22 am

Re: Playing a simple tone with PureBasic and PortAudio

Post by doctornash »

thanks for the tip wilbert. i tried with #paInt16, and it works fine for my specific application case, because I apply a normalization algorithm to the audio before outputting it, thereby constraining it to within Int limits. as an aside, the test: output text file of audio data from my vb6 synthesis/sequencing application -> import the text file into purebasic array and play it, went well!
infratec
Always Here
Always Here
Posts: 7662
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Playing a simple tone with PureBasic and PortAudio

Post by infratec »

Hi doctornash,

but why you want to save the data in text files ???
Why not in binary files?

It is faster and smaller.

Bernd
Post Reply