Page 1 of 3
Playing a simple tone with PureBasic and PortAudio
Posted: Sun Oct 23, 2011 7:59 am
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
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Sun Oct 23, 2011 10:54 am
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
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Sun Oct 23, 2011 9:30 pm
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?
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Sun Oct 23, 2011 9:55 pm
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.
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Mon Oct 24, 2011 8:28 am
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
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:

And is it this which is setting the length of the sound in mSec:
Pa_Sleep(2000)
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Mon Oct 24, 2011 12:48 pm
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.
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Mon Oct 24, 2011 1:05 pm
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.
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Mon Oct 24, 2011 2:57 pm
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

Re: Playing a simple tone with PureBasic and PortAudio
Posted: Wed Oct 26, 2011 11:40 am
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:

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?
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Wed Oct 26, 2011 12:54 pm
by Rings
does increment the pointer *output 4 bytes .
you can also write the long syntax form(better known from VB ) :
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Wed Oct 26, 2011 1:24 pm
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:
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.
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Fri Oct 28, 2011 10:20 pm
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?
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Sat Oct 29, 2011 3:11 pm
by doctornash
Well, I cobbled together this. It uses a gosub; it works, but isn't an elegant solution
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
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Sat Oct 29, 2011 4:00 pm
by Trond
Instead of gosub you can use procedures. Put the code that plays the sound in a procedure and call it.
Re: Playing a simple tone with PureBasic and PortAudio
Posted: Sat Oct 29, 2011 4:46 pm
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