Synthesizer in BlitzMax for Beginners

Started by Midimaster, April 15, 2023, 07:11:21

Previous topic - Next topic

Midimaster

To keep the tutorial clean I ask to write comments and questions not in this topic, but use the BlitzMax-Forum.

Chapter I: What is an oscillator?

A oscillator is not more than a point in a 360° circle. And the point knows its circle position Arc:Double. This simple construct can be used to make music. If we now change its circle position, the oscillator begins to "run":
Code: BASIC
Type TOsci
	Field Arc:Double
	
	Method Run()
		Arc = (Arc+ 0.1) Mod 360
	End Method
End Type

The next step is, that we need a very  exact tick to call the method Run(). In BlitzMax we have the Millisecs()-timer, that ticks 1000 times a second:

Code: BASIC
Function Tick()
	If Timer<MilliSecs()
		Timer = MilliSecs()
		Osci.Run
	EndIf
End Function 

In this runable example we add only 0.1 to the Arc of the oscillator to be able to observe it on the screen:
Code: BASIC
Graphics 800,600

Global Osci:TOsci =New TOsci
Global Timer:Int 
Repeat
	Cls
	Tick
	DrawText Int(Osci.Arc),100,100
	Flip 0
Until AppTerminate()

Function Tick()
	If Timer<MilliSecs()
		Timer = MilliSecs()
		Osci.Run
	EndIf
End Function 

Type TOsci
	Field Arc:Double
	
	Method Run()
		Arc = (Arc+ 0.1) Mod 360
	End Method
End Type
...on the way to China.

Midimaster

Hertz and Time and Arc

We expand our oscillator to a second field ArcAdd:Double, where we can store the value we want to add each tick. Now we are able to control the speed from outside:

Code: BASIC
Osci.ArcAdd= 0.1
...
Type TOsci
	Field Arc:Double
	Field ArcAdd:Double
	
	Method Run()
		Arc = (Arc+ ArcAdd) Mod 360
	End Method
End Type


If we rise ArcAdd the oscillator will run faster and the audio frequence will become higher. To define the correlation between audio Hertz and arc we use this formula:

ArcAdd = 360*Hertz/Ticks

In our example we have the 1000 ticks Millisecs() timer. If we would like to have 1Hertz (1 turn per second), this would mean we need a ArcAdd of 360*1/1000 = 0.36

for calculations like this we add a new Method Set(). In the method Set() we send the audio frequence Hertz as a paramter and the oscillator calculates the corresponding ArcAdd

Code: BASIC
Osci.Set 1
...
Type TOsci
	...
	
	Method Set(Hertz:Double)
		ArcAdd = 360.0*Hertz/TICKS
	End Method
End Type


Only for design purposes we add a function to display the oscillator running in a circle:
Code: BASIC
Function DisplayOsci()
	SetColor 255,255,255
	DrawOval 105,105,200,200
	SetColor 0,0,0
	DrawOval 106,106,198,198
	SetColor 255,255,0
	DrawOval 200+ Sin(Osci.Arc)*100 , 200-Cos(Osci.Arc)*100 ,11,11
End Function 

OsciTutor1.gif


Here is the complete runable code:
Code: BASIC
Graphics 800,600

Const TICKS:Int=1000
Global Osci:TOsci =New TOsci
Global Timer:Int 

Osci.Set 0.5

Repeat
	Cls
	Tick
	Local t:String=Int(Osci.Arc)
	DrawText t,100-TextWidth(t),100
	DisplayOsci
	Flip 0
Until AppTerminate()


Function DisplayOsci()
	SetColor 255,255,255
	DrawOval 105,105,200,200
	SetColor 0,0,0
	DrawOval 106,106,198,198
	SetColor 255,255,0
	DrawOval 200+ Sin(Osci.Arc)*100 , 200-Cos(Osci.Arc)*100 ,11,11
End Function 


Function Tick()
	If Timer<MilliSecs()
		Timer = MilliSecs()
		Osci.Run
	EndIf
End Function 


Type TOsci
	Field Arc:Double
	Field ArcAdd:Double
	
	Method Run()
		Arc = (Arc+ ArcAdd) Mod 360
	End Method
	
	Method Set(Hertz:Double)
		ArcAdd = 360.0*Hertz/TICKS
	End Method
End Type

.
...on the way to China.

Midimaster

Chapter III: The Oscillator return values

Now the main reason for the tutorial comes. Each tick the oscillator calculates values and thee method Run() return this to the main app or to a audio device

Code: BASIC
Type TOsci
	...
	
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		Return Cos(Arc)

	End Method

We can return whatever we want. It does not matter if we return SIN() or COS(), both has the same reaction, we might even return RAND() values, which would cause NOISE wave sound on the audio device. Later we will see, that we also can return elements of a given Array. This will be called WAVETABLE sound.

In our function Tick() we store the values in an array Sample:Int[] and can use them to feed the audio device or display
as graphic:

Code: BASIC
Function Tick()
	If Timer<MilliSecs()
		Timer = MilliSecs()
		Local value:Double = Osci.Run()
		Pointer = (Pointer +1) Mod 10000
		Samples[Pointer] = value* 100
	EndIf
End Function


A graphical solution could be to display them on the screen from left to right. A little bit like you know it from AUDACITY:

Code: BASIC
Function DisplaySamples()
	For Local i:Int=0 To 500
		Local ShowPointer = (Pointer-i) Mod 10000
		DrawRect 310+i,200,1,-Samples[ShowPointer]
	Next 
End Function


OsciTutor2.gif


Here is the runable example:

Code: BASIC
Graphics 800,600
 
Const TICKS:Int=1000
Global Osci:TOsci =New TOsci
Global Timer:Int 
Global Samples:Int[10000], Pointer:Int
Osci.Set 1
 
Repeat
	Cls
	Tick
	Local t:String=Int(Osci.Arc)
	DrawText t,100-TextWidth(t),100
	DisplayOsci
	DisplaySamples
	Flip 0
Until AppTerminate()
 
 
Function DisplayOsci()
	SetColor 255,255,255
	DrawOval 105,105,200,200
	SetColor 0,0,0
	DrawOval 106,106,198,198
	SetColor 255,255,0
	DrawOval 200+ Sin(Osci.Arc)*100 , 200-Cos(Osci.Arc)*100 ,11,11
End Function 
 
 
Function DisplaySamples()
	For Local i:Int=0 To 500
		Local ShowPointer = (Pointer-i) Mod 10000
		DrawRect 310+i,200,1,-Samples[ShowPointer]
	Next 
End Function 
 
 
Function Tick()
	If Timer<MilliSecs()
		Timer = MilliSecs()
		Local value:Double = Osci.Run()
		Pointer = (Pointer +1) Mod 10000
		Samples[Pointer]   = value* 100
	EndIf
End Function 
 
 
Type TOsci
	Field Arc:Double
	Field ArcAdd:Double
	
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		Return Cos(Arc)
	End Method
	
	Method Set(Hertz:Double)
		ArcAdd = 360.0*Hertz/TICKS
	End Method

End Type


..
.

.

...on the way to China.

Midimaster

Chapter IV: The Mini-Audio Out

With the MiniAudio-SDK we have a real time audio interface for BlitzMax, which is much faster and more flexibel than the old TSound() approach BlitzMax offers. All you need is the mima.miniaudio.mod from Midimaster. you dowload it from his GitHub here:

https://github.com/MidimasterSoft/BlitzMax-Miniaudio-Wrapper 

The module runs in a separate thread, but needs a CallBack function in your code to get in contact with your app. Miniaudio calls this function every 10msec and fetches the amount of sample values it needs for the next 10msec.

What we have to do is filling an array PlayBuffer[] , which MiniAudio provides in the CallBack. If we do not fill it, this will result in SILENCE on the audio device. If we want sound, we fill it with INTEGER values between -15000 and +15000.

MiniAudio tells us in Frames:Int how many value we should send this time.

The starting needs only three lines:

Code: BASIC
Import mima.miniaudio
Const SAMPLE_HERTZ:Double = 1000, VOLUME:Int=10000

Graphics 800,600

Global MiniAudio:TMiniAudio = New TMiniAudio
MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, 1, Int(SAMPLE_HERTZ), MyCallBack)
MiniAudio.StartDevice()

Today we want to use MiniAudio for PLAYBACK, means Audio Out

The array format is SHORT SIGNED, which we don not have in BlitzMax, but no problem, this means we can enter values between -15000 and +15000.

SAMPLE_HERTZ is the speed (and quality) of the audio out. Today we only use 1.000Hz, but all values between 1.000 and 48.000 are allowed 

MyCallBack is the name we gave our CallBack function. MiniAudio will read this name and find our function. So do never start the interface until you have written the MyCallBack()



The MyCallBack() fills the PlayBuffer[] with sample values. You can fill in whatever you want: Random values, sinus results or values coming from another Integer array. Today we fill it with Random:

Code: BASIC
Function MyCallBack(void:Int, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
	' called by MiniAudio in a independent thread
	For Local i:Int=0 Until Frames
		Local sum:Double = Rnd(-1,1)
		PlayBuffer[i]    = Int(sum*VOLUME)
	Next 
End Function 


This is a runable example. You will here a dark noise in your speakers:

Code: BASIC
SuperStrict
Import mima.miniaudio
Const SAMPLE_HERTZ:Double = 1000, VOLUME:Int=10000

Graphics 800,600

Global MiniAudio:TMiniAudio = New TMiniAudio
MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, 1, Int(SAMPLE_HERTZ), MyCallBack)
MiniAudio.StartDevice()

Repeat
	Cls

	Flip 0
Until AppTerminate()
Miniaudio.CloseDevice()
End


Function MyCallBack(void:Int, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
	' called by MiniAudio in a independent thread
	For Local i:Int=0 Until Frames
		Local sum:Double = Rnd(-1,1)
		PlayBuffer[i]    = Int(sum*VOLUME)
	Next 
End Function 



.
...on the way to China.

Midimaster

#4
Chapter V: Glueing everything together


When we now combine Audio and Display we have the problem, that a good frequency for display is too slow to be hearable. Anda good frequency for audio is too fast to see something useful. So we use two oszillators: one for audio and one for video.

Code: BASIC
Global Osci:TOsci[9]
Osci[1] = New TOsci
Osci[2] = New TOsci
Osci[1].Set 1
Osci[2].Set 100


We do not longer need the millisecs based function Tick() and remove it. All its jobs we move into the MyCallBack():

Code: BASIC
Function MyCallBack(void:Int, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
    ' called by MiniAudio in a independent thread
    For Local i:Int=0 Until Frames

        'audio related stuff
        Local sum:Double = Osci[2].Run()
        PlayBuffer[i]    = Int(sum*VOLUME)

        'video related stuff
        Local value:Double = Osci[1].Run()
        Pointer = (Pointer +1) Mod 10000
        Samples[Pointer]   = value* 100
    Next 
End Function 

The MiniAudio-array PlayBuffer[] gets now filled with the values of oscillator 2 which runs with 100Hz. This produces a low sound. The video array Samples[] is filles with the values of oscillator 1, which runs with only 1Hz

The Set() in the Type must be changed. The calculation is not linger based on the ticks, but now on the SAMPLE_HERTZ, which is the frequency we used when starting the MiniAudio.


In the Main loop we can now integrate a mouse related setting of the osillators. MouseX()/2 becomes the Hertz frequency we want to hear in audio (oscillator 2). MouseX()/200 is base of oscillator 1 and care about graphics

Code: BASIC
Repeat
    Cls
    Local t:String=Int(Osci[1].Arc)
    DrawText t,100-TextWidth(t),100
    DisplayOsci
    DisplaySamples
    Local hertz:Double=MouseX()
    Osci[1].Set Hertz/200.0
    Osci[2].Set Hertz/2.0

    Flip 0
Until AppTerminate()

OsciTutor3.gif

Here is the final code. Thanks for participating in this tutorial

Code: BASIC
SuperStrict
Import mima.miniaudio
 
Graphics 800,600
Const SAMPLE_HERTZ:Double = 1000, VOLUME:Int=10000
 
 
Global MiniAudio:TMiniAudio = New TMiniAudio
MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, 1, Int(SAMPLE_HERTZ), MyCallBack)
MiniAudio.StartDevice()
 
Global Osci:TOsci[9]
Osci[1] = New TOsci
Osci[2] = New TOsci
Osci[1].Set 1
Osci[2].Set 100
 
Global Timer:Int 
Global Samples:Int[10000], Pointer:Int
 
Repeat
	Cls
	Local t:String=Int(Osci[1].Arc)
	DrawText t,100-TextWidth(t),100
	DisplayOsci
	DisplaySamples
	Local hertz:Double=MouseX()
	Osci[1].Set Hertz/200.0
	Osci[2].Set Hertz/2.0
 
	Flip 0
Until AppTerminate()
Miniaudio.CloseDevice()
End
 
 
 
Function MyCallBack(void:Int, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
	' called by MiniAudio in a independent thread
	For Local i:Int=0 Until Frames
 
		'audio related stuff
		Local sum:Double = Osci[2].Run()
		PlayBuffer[i]    = Int(sum*VOLUME)
 
		'video related stuff
		Local value:Double = Osci[1].Run()
		Pointer = (Pointer +1) Mod 10000
		Samples[Pointer]   = value* 100
	Next 
End Function 
 
 
 
Function DisplayOsci()
	SetColor 255,255,255
	DrawOval 105,105,200,200
	SetColor 0,0,0
	DrawOval 106,106,198,198
	SetColor 255,255,0
	DrawOval 200+ Sin(Osci[1].Arc)*100 , 200-Cos(Osci[1].Arc)*100 ,11,11
End Function 
 
 
Function DisplaySamples()
	For Local i:Int=0 To 500
		Local ShowPointer:Int = (Pointer-i) Mod 10000
		DrawRect 310+i,200,1,-Samples[ShowPointer]
	Next 
End Function 
 
 
 
Type TOsci
	Field Arc:Double
	Field ArcAdd:Double
	
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		Return Cos(Arc)
	End Method
	
	Method Set(Hertz:Double)
		ArcAdd = 360.0*Hertz/SAMPLE_HERTZ
	End Method
End Type

.
...

.
...on the way to China.

Midimaster

Chapter VI Different Wave Forms


A typical syntesizer knows various waveforms and here you learn how to produce them in your BlitzMax oszillator. The complete production is always inside the method Run(), where at the moment is a SINUS operation

Code: BASIC
Type TOsci
	...	
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		Return Cos(Arc)
	End Method


SINUS

The mother of all wave forms. Source is a sinus-function, the wave produces a clear single tone. The signal looks smooth and elegant and so it sounds: often used for organ sounds, wood based instruments like flutes. With a clever combination of 5-8 sinus oscillators you can imitate waveforms like SQUARE or SAWTOOTH.

we use the SIN() or COS() function with Arc as degree parameter:
Code: BASIC
Type TOsci
	...
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		Return Cos(Arc)
	End Method



SAWTOOTH

looks like the shark fin. Produces a sharp tone with a lot of overtones. When you examine the sound with a FFT you will find a bundle of frequencies above the base frequency: 1:2:3:4:5:6:7:8 means a 100Hz SAWTOOTH produces also 200Hz, 300Hz, 400Hz 500Hz ... In Notes: C-C-G-C-E-G-Bb-C
SAWTOOTH are the base for Piano sounds and Strings.

The SAWTOOTH runs from -1 to +1 over the complete period (wave-length), then jumps back to -1. To simulate this, we use the fact that Arc always runs from 0 to 360 to calculate the icreasing value:

Code: BASIC
Type TOsci
	...
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		Return -1 + Arc/180
	End Method

Code: BASIC
for ARC=  0 this gives -1.0
for ARC= 90 this gives -0.5
for ARC=180 this gives  0.0
for ARC=270 this gives +0.5
for ARC=359 this gives +1.0



SQUARE

looks like a rectangle. Produces a half sharp tone with less overtones. When you examine the sound with a FFT you will find some frequencies above the base frequency: 1:3:5:7:9 means a 100Hz SQUARE produces also 300Hz, 500Hz, 700Hz 900Hz ... In Notes: C-G-E-Bb-D
SQUARE are the base for Clarinet and Brass.

The SQUARE alters between -1 and +1. It changes in the middle of the period. To simulate this, we use the fact that Arc always runs from 0 to 360 to decide for onne of the values:

Code: BASIC
Type TOsci
	...
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		If Arc<180
			Return -1
		Else
			Return  1
		EndIf
	End Method

Code: BASIC
for ARC=  0 to 179 this gives -1.0
for ARC=180 to 359 thid gives +1.0




PULSE
looks like a two rectangles: a narrow and a wide rectangle alternate. Produces a more sharp tone with a lot of overtones. When you examine the sound with a FFT you will find some frequencies above the base frequency: 0.5:1:3:5:7:9 means a 100Hz SAWTOOTH produces also 50Hz, 200Hz, 300Hz 400Hz ... In Notes: C-C-C-G-C-E-G. PULSE is a synthetic sound

The PULSE alters once between -1 and +1. The point where it changes can be anywhere between 1 and 358. To simulate this, we use the fact that Arc always runs from 0 to 360 to decide for one of the values:

Here we decide for a 1:5 PULSE WAVE, that means the second rectangle is 5 times bigger than the first
Code: BASIC
Type TOsci
	...
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		If Arc<60
			Return -1
		Else
			Return  1
		EndIf
	End Method

Code: BASIC
for ARC= 0 to  59 this gives -1.0
for ARC=60 to 359 thid gives +1.0




NOISE

looks like random values: chaotic . Produces a noise like sound of the sea or like wind. When you examine the sound with a FFT you will find all frequencies, there is no base frequency. There are no correspoding notes.


The NOISE has chaotic values between -1 and +1. To simulate this, we do not need Arc.


Code: BASIC
Type TOsci
	...
	Method Run:Double()
		Arc = (Arc+ ArcAdd) Mod 360
		Return Rnd(-1, +1)
	End Method

...on the way to China.