Playing a simple tone with PureBasic and PortAudio

Just starting out? Need help? Post your questions and find answers here.
doctornash
Enthusiast
Enthusiast
Posts: 130
Joined: Thu Oct 20, 2011 7:22 am

Playing a simple tone with PureBasic and PortAudio

Post by doctornash »

Hi All. Over the past few years, I have built and released as freeware a very feature-rich synthesis+effects+step sequencing application developed in VB6, with which one can create sounds, process samples and turn them into complete songs. Here's the overview/help/tutorial file for the app:
http://www.scribd.com/doc/59109262/FlexiBeatzII-Help
and here's the app's site:
http://flexibeatz.weebly.com/
There are tutorial videos on youtube as well.

The app relies on low-level Directsound 8 programming, and as a result it works fine on Win 2000, XP and I've had some reports that it also works on 32 bit Vista. However, I'm not prepared to make the adaptations necessary to get it to work on Windows 7, and I no longer wish to be subject to whatever combination of programming environments, OS's and Directsound versions Microsoft chooses to support at any given time

As a result, I got me some PureBasic, since the blurb says it is cross-platform, not subject to dll-hell and is a quick n easy compile. It is installed. I also downloaded PortAudio, since the blurb says PortAudio is "a free, cross-platform, open-source, audio I/O library. Third-party language bindings make it possible to call PortAudio from other programming languages including PureBasic". If you assume that I am not really familiar with either of these yet, could someone kindly show step-by-step what I need to do to play a simple tone with a given frequency and sampling rate using PureBasic and PortAudio. If you could show me how to set up the environments and start with PureBasic equivalent of something like the following I'd be very grateful (as you know, the following code in VB6 produces an array of a sine wave that swings between +1 and -1; I am not showing the DirectSound processing needed to play this through the soundcard though). Once shown, I should be able to take it from there...

Code: Select all

Dim T as Long
Dim SR as Long
Dim Phase as Single
Dim Phase_Add as Single
Dim ToneLen as Long
Dim OutWav()
Dim Pi2 as Double

Pi2 = 6.28318530717958 '2*Pi
SR = 44100 'Sampling Rate
F = 400 'Desired Frequency of tone
ToneLen = 20000 'Desired length of tone in samples
Redim OutWav(20000)

Phase_Add = F/SR
For T = 0 to ToneLen
OutWav(T) = Sin(Pi2*Phase)
Phase = Phase + Phase_Add
If Phase>1 Then Phase = Phase-1
Next T
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Re: Playing a simple tone with PureBasic and PortAudio

Post by Trond »

I have a PortAudio version that I compiled a while ago for 32-bit Windows. I can't test it right now as I'm on 64-bit, but this is how to make a sine wave:

Code: Select all

XIncludeFile "PortAudio.pb"

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

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

Global Phase.d
Global SampleRate.d = 44100

ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData)
  For I = 1 To frameCount
    
    ; Left
    *output\f = Sin(Phase) / 6
    *output+4
    
    ; Right
    *output\f = Sin(Phase) / 6
    *output+4
    
    Phase + 0.06
  Next
EndProcedure


; err = Pa_OpenDefaultStream(@stream, 0, 2, #paFloat32, SampleRate, 4096, @PaStreamCallback(), 0)

op.PaStreamParameters
op\channelCount = 2
op\device = 8
op\sampleFormat = #paFloat32
op\suggestedLatency = 5.8/1000
err = Pa_OpenStream(@stream, 0, @op, SampleRate, 0, #paNoFlag, @PaStreamCallback(), 0)

If err <> #paNoError
  Error(err)
EndIf

Pa_StartStream(stream)
Pa_Sleep(2000)
Pa_StopStream(stream)

Pa_Terminate()
Here's all the files: http://anotherprophecy.com/system/scrip ... e=dist.zip
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 very much for the example! :)

However, when I double-click on the file "Sinewave - Tutorial.pb" and then from the PureBasic IDE go Compiler>Run, first a dialog box pops up saying that there are no products running on the system that support ASIO, followed by a pop up notification 'Invalid Device', together with a debug output dialog box showing code 268574228.

It is a WinXP laptop, and code I found elsewhere on the site to enumerate devices on the system, says the following devices are available:

There are 8 Devices
0 Source Microsoft Sound Mapper - Input (MME)
1 Source SigmaTel Audio (MME)
2 Dest Microsoft Sound Mapper - Output (MME)
3 Dest SigmaTel Audio (MME)
4 Source Primary Sound Capture Driver (Windows DirectSound)
5 Source SigmaTel Audio (Windows DirectSound)
6 Dest Primary Sound Driver (Windows DirectSound)
7 Dest SigmaTel Audio (Windows DirectSound)

Any ideas?
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Re: Playing a simple tone with PureBasic and PortAudio

Post by Trond »

On line 40 I set the device to 8. Try setting it to 6.

There is a file devices.pb which should enumerate the available devices.
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 »

On line 40 I set the device to 8. Try setting it to 6
Yep, I made the same change as you were responding, and it plays now :D

But it's a buzzy, distorted sound. Is the code actually meant to produce a pure continuous sine? In Audacity, the waveform shows as so:
Image
And is it this which is setting the length of the sound in mSec:
Pa_Sleep(2000)
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Re: Playing a simple tone with PureBasic and PortAudio

Post by Trond »

I don't know why it's not smooth. Maybe the latency is wrong. The latency is set to 5.8 ms in the code, which is ok with ASIO, but with DirectX the minimum latency is from 30 to 90 ms. With MME it could be much more.

Code: Select all

op\suggestedLatency = 150/1000 ; 150 ms.
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 »

I'm not familiar with PortAudio but I noticed the PaStreamCallbackTimeInfo structure has a variable named outputBufferDacTime.
It might help to use that value to determine what samples you need to return.
If there are some kind of parallel processes to split the working load that both call the callback routine, it's possible that the requests for sample data are not always sequential.
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 »

op\suggestedLatency = 150/1000 ; 150 ms
Brilliant! The tone now sounds and looks like a pure, unadulterated sine
Thanks very much for your help...I've boarded the train; the journey now begins :D
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 »

OK, still coming to grips with the language syntax, but I amended the code to play a sine wave, square wave and sawtooth wave, with a frequency specified by the 'Freq' variable. To play a sine, leave as Sine(Phase) in PaStreamCallback, to play saw change to Saw(Phase) and to play square change to Sq(Phase):

Code: Select all

XIncludeFile "PortAudio.pb"

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

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

Global Phase.d
Global Phase_Add.d
Global SampleRate.d = 44100
Global Pi2.d = 6.28318530717958
Global Freq.d

Freq = 200

Phase_Add = Freq/SampleRate

Procedure.d Sine(Phh.d)
  Define Q.d 
   Q = Phh
ProcedureReturn Q
EndProcedure

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

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

ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData)
  For I = 1 To frameCount
    *output\f = Sin(Pi2*Sine(Phase)) 
    Phase = Phase + Phase_Add
    If Phase > 1
    Phase = Phase - 1
    EndIf 
    *output+4
  Next
EndProcedure

op.PaStreamParameters
op\channelCount = 1
op\device = 7
op\sampleFormat = #paFloat32
op\suggestedLatency = 150/1000
err = Pa_OpenStream(@stream, 0, @op, SampleRate, 0, #paNoFlag, @PaStreamCallback(), 0)

If err <> #paNoError
  Error(err)
EndIf
*si.PaStreamInfo = Pa_GetStreamInfo(stream)
Debug *si\outputLatency*1000
MessageRequester("", "")
Pa_StartStream(stream)
Pa_Sleep(2000)
Pa_StopStream(stream)

Pa_Terminate()
this is what the output looks like in each case:
Image
Image
Image
I'd be grateful though if someone could kindly explain the following in the PaStreamCallback procedure:
a) what *output\f means. what is that \f?
b) what *output+4 does?
User avatar
Rings
Moderator
Moderator
Posts: 1435
Joined: Sat Apr 26, 2003 1:11 am

Re: Playing a simple tone with PureBasic and PortAudio

Post by Rings »

Code: Select all

*output+4
does increment the pointer *output 4 bytes .

you can also write the long syntax form(better known from VB ) :

Code: Select all

*output = *output + 4 
SPAMINATOR NR.1
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 »

*output is a pointer.
It is declared to point at a structure of float

*output.float

f is the name of the value inside the structure float
You can see this if you open the structure viewer and go to float:

Code: Select all

Structure Float
 f.f
EndStructure
so with *output\f = 1.2 you put 1.2 in the f value of *output.


When I program such stuff I use *output.f
and
PokeF(*output, 1.2)

Bernd

P.S.: + 4 -> a float has 4 bytes, so the pointer is incremented to the next float value.
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 does one go about linking this to a basic GUI: say a form with one command button and one drop down list. The drop down list contains the choices 'Sine', 'Saw', 'Square' with 'Sine' highlighted as default and the sound plays when user clicks the command button?
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 »

Well, I cobbled together this. It uses a gosub; it works, but isn't an elegant solution
Image

Code: Select all

XIncludeFile "PortAudio.pb"

Global SelectedWave.s
Global Freq.d

Enumeration
#WIN_MAIN
#BUTTON_Play
#Combo_0
#TEXT_INPUT
#STRING_INPUT
EndEnumeration

Global Quit.b = #False
#FLAGS = #PB_Window_SystemMenu | #PB_Window_ScreenCentered
If OpenWindow(#WIN_MAIN, 0, 0, 200, 200, "Interaction", #FLAGS)
If CreateGadgetList(WindowID(#WIN_MAIN))
  ButtonGadget(#BUTTON_Play, 10, 150, 100, 20, "Play")
  
  ComboBoxGadget(#Combo_0, 10, 10, 100, 20)
  AddGadgetItem(#Combo_0, -1, "Sine")
  AddGadgetItem(#Combo_0, -1, "Saw")
  AddGadgetItem(#Combo_0, -1, "Square")
  SetGadgetState(#Combo_0,0)
  
  TextGadget(#TEXT_INPUT, 10, 80, 280, 20, "Frequency:")
  StringGadget(#STRING_INPUT, 10, 100, 60, 20, "200",#PB_String_Numeric)
Repeat
Event.l = WaitWindowEvent()
Select Event
Case #PB_Event_Gadget
  Select EventGadget()
    Case #BUTTON_Play
      SelectedWave = GetGadgetText(#Combo_0)
      Freq = Val(GetGadgetText(#STRING_INPUT))
      Gosub PlayWave      
  EndSelect
EndSelect
Until Event = #PB_Event_CloseWindow Or Quit = #True
EndIf
EndIf
End
  
PlayWave:
Procedure Error(err)
  Debug Pa_GetErrorText(err)
  MessageRequester("", PeekS(Pa_GetErrorText(err)))
  End
EndProcedure

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

Global Phase.d
Global Phase_Add.d
Global SampleRate.d = 44100
Global Pi2.d = 6.28318530717958

Phase_Add = Freq/SampleRate

Procedure.d Sine(Phh.d)
  Define Q.d 
   Q = Phh
ProcedureReturn Q
EndProcedure

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

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

ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData)
  For I = 1 To frameCount
    ;*output.f
    If SelectedWave = "Sine"
      PokeF(*output, Sin(Pi2*Sine(Phase)))
    ElseIf SelectedWave = "Saw"
      PokeF(*output, Sin(Pi2*Saw(Phase)))
    ElseIf SelectedWave = "Square"
      PokeF(*output, Sin(Pi2*Sq(Phase)))
    EndIf  

    Phase = Phase + Phase_Add
    If Phase > 1
    Phase = Phase - 1
    EndIf 
    *output+4
  Next
EndProcedure

op.PaStreamParameters
op\channelCount = 1
op\device = 7
op\sampleFormat = #paFloat32
op\suggestedLatency = 150/1000
err = Pa_OpenStream(@stream, 0, @op, SampleRate, 0, #paNoFlag, @PaStreamCallback(), 0)

If err <> #paNoError
  Error(err)
EndIf

Pa_StartStream(stream)
Pa_Sleep(2000)
Pa_StopStream(stream)

Pa_Terminate()
Return
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Re: Playing a simple tone with PureBasic and PortAudio

Post by Trond »

Instead of gosub you can use procedures. Put the code that plays the sound in a procedure and call it.
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 »

One possibility:

Code: Select all

XIncludeFile "PortAudio.pb"

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

#PI2 = 6.28318530717958

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


Enumeration
  #Sine
  #Saw
  #Square
EndEnumeration


Structure UserDataStr
  WaveForm.i
  PhaseAdd.d
EndStructure




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
  
  Select *userData\WaveForm
    Case #Sine
      For i = 1 To frameCount
        PokeF(*output, Sin(#PI2 * Sine(Phase)))  
        Phase = Phase + *userData\PhaseAdd
        If Phase > 1 : Phase = Phase - 1 : EndIf
        *output + 4
      Next i
    Case #Saw
      For i = 1 To frameCount
        PokeF(*output, Sin(#PI2 * Saw(Phase)))
        Phase = Phase + *userData\PhaseAdd
        If Phase > 1 : Phase = Phase - 1 : EndIf
        *output + 4
      Next i
    Case #Square
      For i = 1 To frameCount
        PokeF(*output, Sin(#PI2 * Sq(Phase)))
        Phase = Phase + *userData\PhaseAdd
        If Phase > 1 : Phase = Phase - 1 : EndIf
        *output + 4
      Next i
  EndSelect
  
EndProcedure


Procedure PlayWave(WaveForm, SampleRate.d, Freq, Duration = 2000)
  
  err = Pa_Initialize()
  If err <> #paNoError : Error(err) : EndIf
  
  UserData.UserDataStr\WaveForm = Wave
  UserData\PhaseAdd = Freq / SampleRate
  
  op.PaStreamParameters
  op\channelCount = 1
  op\device = 7
  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)
  Pa_Sleep(Duration)
  Pa_StopStream(stream)
  
  Pa_Terminate()
  
EndProcedure





If OpenWindow(#WIN_MAIN, 0, 0, 200, 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)
  
  TextGadget(#DurationText, 120, 70, 80, 20, "Duration (ms)")
  StringGadget(#DurationString, 120, 90, 60, 20, "2000", #PB_String_Numeric)
  
  ButtonGadget(#PlayButton, 10, 130, 180, 30, "Play")
  
  Quit = #False
  Repeat
    Event = WaitWindowEvent()
    Select Event
      Case #PB_Event_Gadget
        Select EventGadget()
          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 #PlayButton
            PlayWave(GetGadgetState(#SelectedWave), #SampleRate, Val(GetGadgetText(#FrequenceString)), Val(GetGadgetText(#DurationString)))
        EndSelect
      Case #PB_Event_CloseWindow
        Quit = #True
    EndSelect
  Until Quit

EndIf
Bernd
Post Reply