Page 2 of 3

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Sat Oct 29, 2011 6:09 pm
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()

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Sat Oct 29, 2011 7:09 pm
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

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Sat Oct 29, 2011 9:45 pm
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

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Sun Oct 30, 2011 12:05 am
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?

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Sun Oct 30, 2011 7:42 am
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.

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Sun Oct 30, 2011 11:07 am
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

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Mon Oct 31, 2011 10:53 am
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 :?

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Tue Nov 01, 2011 9:50 pm
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?

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Tue Nov 01, 2011 10:21 pm
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

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Wed Nov 02, 2011 7:31 am
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()

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Wed Nov 02, 2011 1:20 pm
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?

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Wed Nov 02, 2011 1:32 pm
by doctornash
ahh...answered my own question: use ValD :oops:

Dim MyArr.d(0)
MyArr(x) = ValD(ReadString(1))

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Wed Nov 02, 2011 1:42 pm
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.

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Wed Nov 02, 2011 8:54 pm
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!

Re: Playing a simple tone with PureBasic and PortAudio

Posted: Wed Nov 02, 2011 9:08 pm
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