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":
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:

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:
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
...back from North Pole.

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:

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

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:
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:
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

.
...back from North Pole.

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

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:

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:

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:

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


..
.

.

...back from North Pole.

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:

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:

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:

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



.
...back from North Pole.

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.

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():

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

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

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

.
...

.
...back from North Pole.

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

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:
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:

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

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:

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

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
Type TOsci
...
Method Run:Double()
Arc = (Arc+ ArcAdd) Mod 360
If Arc<60
Return -1
Else
Return  1
EndIf
End Method

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.


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

...back from North Pole.