DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer

Started by Midimaster, March 22, 2021, 15:16:23

Previous topic - Next topic

Scaremonger

Looking good.

I notice you are creating the ring buffer from the format of the sample. If you created the buffer using shorts regardless of the format and then convert whatever samples are thrown at it using the Blitzmax audio convert function you wouldn't need to make sure the source format conforms.

You could then do something like this ( not real code):

Local audio:TRingbuffer = new TRingbuffer()
Local sample:TAudiosample.load("whatever.wav")
Audio.send( sample )


Send() can then receive an audio samples,  and convert it to the format you need for the ringbuffer


Method send( sample:TAudioSample)
Local stereo: TAudioSample = sample.convert( SF_STEREO16LE)

...

End method



Midimaster

Yes of course you can write a variant of the Send()-function with more features.

But the idea is, that Send() not needs complete TAudioSamples, but single sample values or a variable number of values.

You open it "empty" as a  STEREO or a MONO device. And now it stays open for throwing anything into it at any time.

If you already have "ready to play" TSounds or TSamples... why should you use the ringbuffer? You could already do this with the standard BlitzMax approach.

Some weeks ago here in the forum somebody ask for a simulation of the old ZX way to put bytes directly to the speaker port. The theme was "playing sounds in chunks". Do you remember? With TSounds smaller than 100msec we had a lot of crackling. With extrem short TSounds (< 20msec) it was completely un-usable.

https://www.syntaxbomb.com/index.php/topic,8285.0.html

With this ringbuffer you could simulate those old 8bit computer-speaker

At the moment the buffer expects chunks of 20msec without any crackling or noise. But I'm sure I can reduce this to 10msec or 5msec or 2msec also. This is not able with an TSound-based system.

...back from Egypt

Scaremonger

Quote from: Midimaster on March 27, 2021, 18:06:08
If you already have "ready to play" TSounds or TSamples... why should you use the ringbuffer? You could already do this with the standard BlitzMax approach.

Yes,  I see what you mean.

Quote from: Midimaster on March 27, 2021, 18:06:08
Some weeks ago here in the forum somebody ask for a simulation of the old ZX way to put bytes directly to the speaker port. The theme was "playing sounds in chunks".

This will make that a lot easier. 

Baggey

Quote from: iWasAdam on March 25, 2021, 10:44:12
@Derron - cant you just dump the steaming pile that is soloud in the bin where it belongs?
soloud is just a whole load of wrappers around another set of stuff NONE OF IT DOCUMENTED OR HAVING ANY NG EXAMPLES

without proper NG examples soloud is just a pile of crap. it is no use to ANY user without huge amount of knowlege about soloud and NG and how they interface.

It's one of the many many reasons NG is not well used - loads of wrappers for stuff no one know how to use.

I can write a wrapper for my blanket, but that just for me - if you want ANYONE to be able to use my blanket - then you MUST have documentation and examples - otherwise it's just useless code.


and if you dare to suggest that there are demos in the soloud folder I would SHOUT to you - they are in C NOT NG. and are as much use (to any user without detailed knowledge on how to convert C to workable NG - which is most people) as a knitted teapot!


To say 'lets take a low level audio experiment that directly hits the devices, and try to mash it into a fully fledged sound system with no documentation' is frankly absurd. Go stand in the corner...

Love that comment!  ;) :))

I feel like that with almost all coding these day's without examples and a simple way to get things working most coder's can get things of the ground!
I miss the old computer manuals that came with computers on how to program them with endless Examples. :-[

I can certainly code to a certain extent as ive written a Zx Spectrum Emulator using BlitzMax 1.5 out of the box. All be it 95% Working.

Watching this thread with interest!

As Ring buffer is what i think i need to access for my Blitz Max Spectrum emulator to play sound properl'y. Writing sample's in realtime as they occur from the OUT Function.
Being a hobby programmer all this SDL, .dll, wrappers, and link Modules files simply blows me away!  :o
You need to know how to install them and then how to use them.

Look forward to using something like this but it will need samples and an easy way to setup in BlitzMax.

Kudos to MidiMaster. Keep up the good work look forward to trying the PortAudio_Ring Buffer.

By the way, Ive only read the post upto this point so far.

Kind Regards Baggey
Running a PC that just Aint fast enough!? i7 Quad core 16GB ram 1TB SSD and NVIDIA Quadro K620 . DID Technology stop! Or have we been assimulated!

ZX Spectrum 48k, C64, ORIC Atmos 48K, Enterprise 128K, The SID chip. Im Misunderstood!

Midimaster

#49
Quote from: Baggey on March 28, 2021, 11:37:44
...
As Ring buffer is what i think i need to access for my Blitz Max Spectrum emulator to play sound properl'y. Writing sample's in realtime as they occur from the OUT Function.
...
Look forward to using something like this but it will need samples and an easy way to setup in BlitzMax.
...
Kind Regards Baggey

The use of the ringbuffer is simpler than your old approach inside the OUT_CHUNKS()-function!

old function:
Code (BlitzMax) Select
Function OUT_CHUNKS(Port%, Value%)
        Const SAMPLE_RATE%=44100
        Global NextSample:TAudioSample, PlayTime%
        If Counter=0
                Print "Sample created"
                NextSample = CreateAudioSample(4410, SAMPLE_RATE, SF_MONO8 )
        EndIf

        Select Value & 8
                Case 0
                        NextSample.Samples[Counter]=50
                Case 8
                        NextSample.Samples[Counter]=200
        End Select
        Counter=Counter + 1
        If counter=4408
                NextSample.Samples[4408]=0     
                NextSample.Samples[4409]=0     
                Local Audio:TSound = LoadSound( NextSample )
                Repeat
                        Delay 1
                Until PlayTime<MilliSecs()
                Print "play TSound"
                PlayTime=MilliSecs()+100
                PlaySound Audio
                Counter=0
        EndIf
End Function

You replace the TAudioSample with a simple Array, then collect the values here. And when they reach f.e. 441 bytes you send them to the BufferClass(). Thats all. So the access is faster you can reach 10msec-chunks without having crackles.

possible new (theoretic) approach function:
Code (BlitzMax) Select
Function OUT_CHUNKS(Port%, Value%)
         Global NextSample:BYTE[441], PlayTime%

        Select Value & 8
                Case 0
                        NextSample[Counter]=50
                Case 8
                        NextSample[Counter]=200
        End Select
        Counter=Counter + 1
        If Counter=441
       Delay PlayTime-Millisecs()
               RingBuffer.Send NextSample
               PlayTime=PlayTime+10        
               Counter=0
Endif
End Function


This is theoretic! At the moment I have not implemented SF_MONO8 as Format and the use of the DELAY is not very elegant. This would interrupt the emulator for a to long time.
...back from Egypt

Midimaster

#50
Another step brings us to more usability. You can now already test the RingBuffer-Class in your apps. I now use Threads for more stability. This means that now BlitzMax-NG is the better choice for this

How to add?

I changed the calling of the Ringbuffer:


Code (BlitzMax) Select

RingBufferClass.SetDriver("FreeAudio Multimedia")
Global RingBuffer:RingBufferClass = New RingBufferClass

Global Source:TAudioSample=LoadAudioSample("Test8.wav")

RingBuffer.CreateNew(Source.Format , Source.Hertz , 4)
SendSize = RingBuffer.IntervalSamples()
SEND_TIME= RingBuffer.IntervalTime()


For starting the RingBuffer you need to set its driver to any FreeAudio-Driver.

Then set the FORMAT to one of: SF_MONO8 or SF_MONO16LE or SF_STEREO16LE

Then set the HERTZ to a multiple of 1000 for best results, There is no need to use the Source.Hertz

The last parameter defines a symbolic "latency" of a InBuffer where you later transfer your datas to the Ringbuffer.
A value of 4 is ok in the most cases. If you hear crackles use higher values.

Later you will send the datas in intervalls to the Ringbuffer. Therefore you need to know how many data exactly you have to send in which time.


Now start two Threads:

The first is for the Ringbuffers internal timing to send datas to the Audio-Device
The second is for you to send your datas right in time.

Code (BlitzMax) Select

Local WatchThread:TThread=CreateThread(WatchLoop, "")
Local SendThread:TThread=CreateThread(SendLoop, "")

Function WatchLoop:Object(data:Object)
Repeat
Delay 3
RingBuffer.Watch
Forever
End Function

Function SendLoop:Object(data:Object)
Repeat
Delay 4
If WriteTime<MilliSecs()
' this cares about really 20msec timing:
WriteTime =WriteTime + SEND_TIME
SendAudio
EndIf
Forever
End Function



Your SendAudio()-Function cares about sending the datas to the Ringbuffers:
Code (BlitzMax) Select

Function SendAudio()
Local AudioArray:Short[SendSize]
SourceStream.Seek(ReadPointer)
For Local i%=0 To SendSize-1
AudioArray[i] = ...whatever
Next
RingBuffer.Send AudioArray
ReadPointer= ReadPointer+SendSize*2
End Function



This is an executable code, if you download the 8bit AudioFile Test8.wav

Code (BlitzMax) Select

SuperStrict

Global SendSize% , ReadPointer%

Graphics 800, 600
RingBufferClass.SetDriver("FreeAudio")

Global  SEND_TIME:Int, raus%=0

Global Source:TAudioSample=LoadAudioSample("Test8.wav")
Global SourceStream:TStream = CreateRamStream(Source.Samples,Source.Length*4,True,True)
Print  Source.Hertz

Global RingBuffer:RingBufferClass=New RingBufferClass
RingBuffer.CreateNew(Source.Format, 22000, 4)
SendSize = RingBuffer.IntervalSamples()
SEND_TIME= RingBuffer.IntervalTime()
Print SendSize + " " + send_time

Global WriteTime%=MilliSecs()

Local WatchThread:TThread=CreateThread(WatchLoop, "")

Local SendThread:TThread=CreateThread(SendLoop, "")


Repeat
Cls
SetColor 255,255,255
DrawText "click here to PAUSE:",200,370

DrawRect 200,400,100,100
SetColor 1,1,1
DrawRect 201,401,98,98

Flip 0
Until AppTerminate()
End


Function WatchLoop:Object(data:Object)
Repeat
Delay 3
RingBuffer.Watch
Until raus=1
End Function


Function SendLoop:Object(data:Object)
Repeat
Delay 4
If WriteTime<MilliSecs()
' this cares about really 20msec timing:
WriteTime =WriteTime + SEND_TIME
If MouseDown(1)=0
SendAudio
EndIf
EndIf
Until raus=1
End Function


Function SendAudio()
Local AudioArray:Short[SendSize]
SourceStream.Seek(ReadPointer)
For Local i%=0 To SendSize-1
AudioArray[i] = SourceStream.ReadShort
Next
RingBuffer.Send AudioArray

ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)
End Function

'------------------------------------------------------------------------

Type RingBufferClass
Global MyDriver$
Global BufferMutex:TMutex=CreateMutex()

Field CHANNELS:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, BUFFER_SIZE:Int
Field FORMAT:Int, CHUNK_TIME:Int, HERTZ:Int, BITS:Int, ZERO:Int
Field WritePointer:Int, ReadPointer:Int, RingPointer:Int
Field WriteTime:Int, WatchTime:Int

Field RingBuffer:TAudioSample, InBuffer:TAudioSample, Sound:TSound
Field RingStream:TStream, InBufferStream:TStream


Function SetDriver(Driver$)
If MyDriver<>"" Return
If Driver.contains("FreeAudio")=False
Notify "wrong AudioDriver"
End
EndIf
MyDriver = Driver
SetAudioDriver(MyDriver)
End Function


Method CreateNew(Format%, Hertz%, Latency%)
' HERTZ should be a multiple of 1000 for CHUNK_TIME=10, 20, 40 or 50
' HERTZ can also be 44100 when CHUNK_TIME=20 or 40
'
' FORMAT can be SF_MONO8 or SF_MONO16LE or SF_STEREO16LE
'
' LATENCY can be from 1 to 32
' 2=extrem small, 4=normal size,  8..=secure sizes
'
If MyDriver=""
Notify "No AudioDriver selected"
End
EndIf
Self.HERTZ=Hertz
Self.FORMAT=Format
DefineBuffer Latency
ClearBuffer
WatchTime=MilliSecs()
PlaySound Sound
End Method


Private Method DefineBuffer(Latency%)
CHUNK_TIME=20
Select FORMAT
Case SF_MONO16LE
BITS=16
CHANNELS=1
Case SF_STEREO16LE
BITS=16
CHANNELS=2
Case SF_MONO8
BITS=8
CHANNELS=1
Default
Notify "Audio format not supported"
End
End Select
CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
BUFFER_SAMPLES = 4*Latency * HERTZ * CHUNK_TIME             /1000
BUFFER_SIZE    = BUFFER_SAMPLES * BITS/8 *CHANNELS

RingBuffer     = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
InBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
RingStream     = CreateRamStream(RingBuffer.Samples , Buffer_size,True,True)
InBufferStream = CreateRamStream(InBuffer.Samples,Buffer_size,True,True)
RingPointer    =  BUFFER_SIZE/4
End Method


Public Method Watch()
If WatchTime<MilliSecs()
WatchTime = WatchTime + CHUNK_TIME
SendOneChunk
EndIf
End Method


Private Method SendOneChunk()
LockMutex BufferMutex
Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
InBufferStream.Seek ReadPointerMod
RingStream.Seek RingPointer

If ReadPointer + CHUNK_SIZE > WritePointer
Local Maxi%=WritePointer-ReadPointer
For Local i:Int = 0 To Maxi-1
RingStream.WriteByte InBufferStream.ReadByte()
Next
For Local i:Int = Maxi To CHUNK_SIZE-1
RingStream.WriteByte ZERO
Next
ReadPointer=ReadPointer + Maxi
Else
For Local i:Int = 0 To CHUNK_SIZE-1
RingStream.WriteByte InBufferStream.ReadByte()
Next
ReadPointer=ReadPointer + CHUNK_SIZE
EndIf
RingPointer=(RingPointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
UnlockMutex BufferMutex
End Method


Private Method ClearBuffer()
If BITS=8 ZERO=127
For Local i:Int =0 To BUFFER_SIZE
RingBuffer.Samples[i]=ZERO
Next
End Method


Public Method Send(AudioArray:Short[])
LockMutex BufferMutex
Local WritePointerMod% = WritePointer Mod BUFFER_SIZE

InBufferStream.Seek WritePointerMod

If AudioArray.Length*2 + (WritePointerMod) <= BUFFER_SIZE
For Local i%=0 To AudioArray.Length-1
InBufferStream.WriteShort AudioArray[i]
Next

Else
Local Maxi% = BUFFER_SIZE-WritePointerMod

For Local i%=0 To Maxi/2-1
InBufferStream.WriteShort AudioArray[i]
Next
InBufferStream.Seek 0
For Local i%=Maxi/2 To AudioArray.Length-1
InBufferStream.WriteShort AudioArray[i]
Next
EndIf
WritePointer=WritePointer + AudioArray.Length*2
UnlockMutex BufferMutex
End Method


Public Method IntervalSamples:Int()
Return CHUNK_SIZE/2
End Method


Public Method IntervalTime:Int()
Return CHUNK_TIME
End Method
End Type








...back from Egypt

iWasAdam

well done.
You might want to check your buffer filling with something that doesn't have a beat though.
It looks like there is a tiny dropout (a few millisecs) that seems to be regular

Midimaster

#52
I think the filling of the buffers runs 100% perfect. I checked it with sending thousand of samples and compared the bytes before sending them with the bytes reached at the PlaySound.

The dropouts maybe depend on the latency parameter we set.  Latency differs related to the device we use.
Here on my Windows 10 I can use 2 without problems and 4 is very stabile.

Maybe a value of 8 or 16 would already bring a improvement:
Code (BlitzMax) Select
RingBuffer.CreateNew(Source.Format, 22000, 16) 


There was one thing I cannot explain with FreeAudio and SF_MONO8. I heard a rhythmic crackle of 1 byte size even though the byte transfer was all correct.

So I had to react with a "unlogic" workaround and the crackle has gone:

This is the code I would have expected to be logic:
Code (BlitzMax) Select
RingBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )


This workaround was necessary:
Code (BlitzMax) Select
RingBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )


Perhaps this is a Windows version behavior? And on your computer it is not necessary?
...back from Egypt

iWasAdam

Yep, the one thing you will get with windows is a whole load of 'this worked, this didn't' lol
Tbh I treat windows as mess of pain- you will never get stuff to work everywhere...

Scaremonger

I tried the code from post 51 on my dev laptop running Linux Mint 18 and it sounded great, but there seems to be an issue when it reaches the end of the music.

I ran it a few times, clicking, holding and most of the time I received a segmentation fault and sometimes a wicked screeching noise first.

The sample is 1010288 bytes, but there is nothing to stop the ReadPointer in SendAudio() from extending past the end of the music. The loop carries on attempting to read from the SourceStream past it's end. You can see this by simply adding a print statement and watching the output....


Function SendAudio()
                Local AudioArray:Short[SendSize]
                SourceStream.Seek(ReadPointer)
                For Local i%=0 To SendSize-1
Print source.length+", "+sendsize + ", " +readpointer + ", "+ i
                        AudioArray[i] = SourceStream.ReadShort
                Next
                RingBuffer.Send AudioArray

                ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)
End Function


1010288, 220, 1011560, 63
1010288, 220, 1011560, 64
1010288, 220, 1011560, 65
1010288, 220, 1011560, 66
1010288, 220, 1011560, 67
1010288, 220, 1011560, 68
Segmentation fault (core dumped)

Process complete


I'm running on BlitzMax NG:

BCC Ver: bcc[ng] release 0.129
BMK Ver: bmk 3.45 mt-linux-x64 / gcc 070500 (cpu x4)
GCC Version: 7

Derron

Quote from: Scaremonger on March 30, 2021, 07:11:15
I ran it a few times, clicking, holding and most of the time I received a segmentation fault and sometimes a wicked screeching noise first.


ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)


This is the culprit I think.
sendsize = 10
length = 100
readpointer = 80

-> (80 + 10*2) mod ((100-10)*2) = 100 mod 180 = 100

While "modulo" looks like a smart way to do things it often leads to such little mistakes.

You want "readpointer" to grow by "sendsize*2" and to wrap at "source.length", not at "source.length minus sendsize-something". Only subtract at the end if you were talking about a non-looping buffer. Your code ensures there is "space left" at the end of the buffer/block to use (and this only if you replaced "((Source.length-SendSize)*2)" with "(Source.length-SendSize*2)".



ReadPointer=(ReadPointer+SendSize*2) Mod Source.length


This should loop "around" the source block. So "pointer + readmount mod length".


bye
Ron




Midimaster

This is a little "copy&paste"-bug reamining from the former SF_16MONO-Version. As I never listen to the end of the source, I didi not check it. But this is only the test-code's bug. Not a bug in the Class.

The OP has to take care about checking the limits of the source. So this was only a "turn around 100% before the song ends" MOD calculation. The last possible chunk that can be sent varies depending on the parameters you use in the Class. To ensure that it works 100% I substracted SendSize from the end of the source.
this works for 16bit:
ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)

and this for 8bit:
ReadPointer=(ReadPointer+SendSize*2) Mod (Source.length-SendSize*2)



More interesting are the crackles Scaremonger descripes. Are they only at the end of the source or could you hear them also before?


Now I decided to finish the investigation and offer a "final" version which uses  always SF_STEREO16 intern and converts received samples to this format before adding them to the ringbuffer. This frees me from the need to offern several ways for a lot of hardware/software cases.



...back from Egypt

Derron

Quote from: Midimaster on March 30, 2021, 09:39:42
this works for 16bit:
ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)

ReadPointer describes where to read from "source" ?

What if this are values in use there (before calling this line)
ReadPointer = 100
SendSize = 10
Source.length = 100

ReadPointer = (100+10*2) mod ((100 - 10) * 2)
ReadPointer = 120 mod 180
ReadPointer = 120

BUT ... the stream has only a size/length of 100 ... so it will try to read at a position not available
You would need to do another "mod source.length" to think of the "source" as a ring of data.


bye
Ron

Midimaster

In a SF_MONO16LE the size of the TAudioSample is double of the TAudioSample.Length

So if TAudioSample.Length=100 the ReadPointer can run from 0 to 199

in my test-surrounding the ReadPointer was only allowed to run to (TAudioSample.Length-SendSize)*2, which means from 0 to 179.

In the final version this is a thing the OP has to care about himself. Only he knows the sources of the samples values. Maybe they come from a live sinus calculation. Therefore he does not need this line anyway. My next post will show some code examples.


...back from Egypt

Midimaster

#59
Here is the final version of an BlitzMax AudioDriver that works like a port. It is always open and you can throw single datas in it until the very last moment of 10msec before playing. If you have no datas, the AudioDriver keeps open, but silent.


Detailed description Version 1.3.2
The Driver is based on FreeAudio, so you have to choose "FreeAudio" or it's variants like "FreeAudio Directsound". Thanks to Derron, who coded the part of the direct access to the FreeAudio-Ram.

The RingBuffer needs Threaded Build by default. On old BlitzMax 1.50 you have to select this option manually.

You can select HERTZ as you want, but best result are with multiples of 1000, f.e. 22000 is better than 22050.

You can select FORMAT from 4 variants: SF_MONO8 or SF_STEREO8 or SF_MONO16LE or SF_STEREO16LE. This only affect the sending of your datas. Internally the AudioDriver converts it all to SF_STEREO16LE before sending it to the speakers.

If you send too few datas, you will not get any problems but PAUSE or little Breaks.
If you permanent send too many datas you will get a increasing latency until the buffer overruns*.

*)the new version 1.3 cares about buffer overrun by pausing your app for 20msec
each time the buffer threadens to overrun and reporting to the debug window:
"RINGBUFFER: Prevent Buffer Overrun! Wait for 20msec"
This behavior can be switched of by RingBuffer.WITH_PROTECTION=0


So you need to know what is the best amount of data.s.
Therefore you aks the functions RingBuffer.IntervalSamples() and RingBuffer.IntervalTime() for the best timing.

RULE: "Send  RingBuffer.IntervalSamples() each RingBuffer.IntervalTime()"

Method I: Sending a bulk of datas:
You have to control yourself the sending of your datas. Fill them in a local INT-Array and send this to the AudioDriver.
Code (BlitzMax) Select
RingBuffer.Send AudioArray
Your amount of sending can vary, but in total you should keep an eye on the optimal amount.



Method II: Sending single sample values:
Send single Sample Values this way to the AudioDriver:
Code (BlitzMax) Select
RingBuffer.SendOne Value
Your rate/intervall of sending can vary, but in total you should keep an eye on the optimal amount/msec.



The INT-Array or the SINGLE VALUE contains one sample value in one cell.
If you send 8bit-Samples the values need to be

  • unsignd-BYTE
If you send 16bit-Samples it does not matter whether this value is...

  • unsigned-SHORT

  • signed-SHORT

  • signed-INTEGER


So here is the Class: RingBufferClass.bmx for BlitzMax NG and BlitzMax 1.50
Code (BlitzMax) Select
SuperStrict
Type RingBufferClass
' A permant running Audio-Output using FreeAudio
' --------------------------------------------------
' Author: Midimaster at www.midimaster.com
' V1.3.2  2021-08-09
' see examples of use at https://www.syntaxbomb.com/index.php/topic,8377.0.html
' This is Public Domain
' -------------------------------------------------------------------
' History:
' 1.3
'     added a ShowPercent() function
'     added WITH_PROTECTION flag default=ON
'     added REPEAT_LAST_SAMPLE flag default=OFF
'     speed improvements
'
' 1.2
'     added a SendOne() function
'     added a Buffer Overruns protection
'     now with internal thread
' -------------------------------------------------------------------
' minimal example:
'   RingBufferClass.SetDriver("FreeAudio....")
'   Global RingBuffer:RingBufferClass = New RingBufferClass
'   RingBuffer.CreateNew(HERTZ, FORMAT)
'   SendSize:INT  = RingBuffer.IntervalSamples()
'   SendTime:INT = RingBuffer.IntervalTime()
'   Global WriteTime = MilliSecs()
'   Function SendSamples
' If WriteTime>MilliSecs() Return
' WriteTime = WriteTime + SendTime
' Local AudioArray:Int[SendSize]
' For Local i:Int=0 To SendSize-1
' AudioArray[i] = any value....
' Next
' RingBuffer.Send AudioArray
'   End Function

Global MyDriver$
Global BufferMutex:TMutex=CreateMutex()

Field CHANNELS:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, BUFFER_SIZE:Int
Field FORMAT:Int, CHUNK_TIME:Int, HERTZ:Int, BITS:Int
Field WritePointer:Int, ReadPointer:Int, RingPointer:Int
Field WriteTime:Int, WatchTime:Int, InFormat:Int

Field RingBuffer:TAudioSample,  Sound:TSound
Field InBuffer:TBank
Field InPtrShort:Short Ptr, RingPtrShort:Short Ptr
Field RingPtrInt:Int Ptr, InPtrInt:Int Ptr

Field WatchThreadB:TThread
Field WITH_PROTECTION:Int    = 1  ' waits 20msec before buffer overrun


Function SetDriver(Driver$)
' PUBLIC: Use this to...
' select one of the audio drivers. It needs to be FreeAudio
' on Windows and BlitzMax 1.5 needs to be FreeAudio DirectSound
If MyDriver<>"" Return
If Driver.contains("FreeAudio")=False
Notify "wrong AudioDriver"
End
EndIf
MyDriver = Driver
SetAudioDriver(MyDriver)

End Function



Method CreateNew(Hertz:Int, UserFormat:Int=SF_STEREO16LE , Latency:Int=8)
' PUBLIC: Use this to define the Ringbuffer...
' HERTZ should be a multiple of 1000 for CHUNK_TIME=10, 20, 40 or 50
' HERTZ can also be 44100 when CHUNK_TIME=20 or 40
'
' UserFormat can be SF_MONO8 or SF_STEREO8 or SF_MONO16LE or SF_STEREO16LE
'
' LATENCY can be from 1 to 32
' 2=extrem small, 4=normal size,  8-32..=secure sizes
'
If MyDriver=""
Notify "No AudioDriver selected"
End
EndIf
Self.HERTZ=Hertz
Self.FORMAT=SF_STEREO16LE
Self.InFormat=UserFormat
DefineBuffer Latency

ClearBuffer
WatchTime=MilliSecs()
PlaySound Sound
Local WatchThread:TThread = CreateThread(WatchLoop,Self)
End Method



Method IntervalSamples:Int()
' PUBLIC: Use this to...
' inform how many samples you should send each call
If  (InFormat=SF_MONO8) Or (InFormat=SF_MONO16LE)
Return CHUNK_SIZE/2
Else
Return CHUNK_SIZE
EndIf
End Method


Method IntervalTime:Int()
' PUBLIC: Use this to...
' inform how long you should wait between calls (in msecs)
Return CHUNK_TIME
End Method



Method ShowPercent:Int()
' PUBLIC: Use this to...
' inform how intensiv the buffer is filled 0% - 100%
Local diff:Int=WritePointer-Readpointer
diff=diff*100/BUFFER_SIZE
Return diff
End Method



Method SendOne(Value:Int)
' PUBLIC: Use this to...
' send one single sample value to the ringbuffer
Global ShortArray:Short[CHUNK_SIZE]
    Global Counter:Int

Select InFormat
Case SF_MONO8
Value=(Value-128)*128
ShortArray[Counter]   = Value
ShortArray[Counter+1] = Value
Counter=Counter+2
Case SF_STEREO8
Value=(Value-128)*128
ShortArray[Counter] = Value
Counter=Counter+1
Case SF_MONO16LE
ShortArray[Counter]   = Value
ShortArray[Counter+1] = Value
Counter=Counter+2
Case SF_STEREO16LE
ShortArray[Counter] = Value
Counter=Counter+1
End Select
If Counter = CHUNK_SIZE
CheckBufferOverRun
Transfer ShortArray
          Counter=0
EndIf
End Method



Method Send(AudioArray:Int[])
' PUBLIC:  Use this to...
' send a couple of samples value to the ringbuffer
Local ShortArray:Short[]
Select InFormat
Case SF_MONO8
ShortArray= New Short[AudioArray.Length*2]
For Local i:Int=0 To AudioArray.Length-1
Local v:Int = (AudioArray[i]-128)*128
ShortArray[2*i]   = v
ShortArray[2*i+1] = v
Next
Case SF_STEREO8
ShortArray= New Short[AudioArray.Length]
For Local i:Int=0 To AudioArray.Length-1
ShortArray[i] = (AudioArray[i]-128)*128
Next
Case SF_MONO16LE
ShortArray= New Short[AudioArray.Length*2]
For Local i:Int=0 To AudioArray.Length-1
ShortArray[2*i]   = AudioArray[i]
ShortArray[2*i+1] = AudioArray[i]
Next
Case SF_STEREO16LE
ShortArray= New Short[AudioArray.Length]
For Local i:Int=0 To AudioArray.Length-1
ShortArray[i] = AudioArray[i]
Next
End Select
CheckBufferOverRun
Transfer ShortArray
End Method


'
'   E N D   O F   T H E   P U B L I C   F U N C T I O N S
'
' ***************************************************************************
'
'    I N T E R N A L   F U N C T I O N S :

?bmxng
Private
?
Method Watch()
If WatchTime<MilliSecs()
WatchTime = WatchTime + CHUNK_TIME
SendOneChunk
EndIf
End Method



Function WatchLoop:Object(data:Object)
        Local RingBuffer:RingBufferClass = RingBufferClass(data)
        If Not RingBuffer Then Return Null 
Repeat
Delay 3
RingBuffer.Watch
Forever
End Function


Method DefineBuffer(Latency:Int)
CHUNK_TIME=20
BITS=16
CHANNELS=2
CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000/2
Print "CHUNK = " + chunk_size

BUFFER_SAMPLES = 4*Latency * HERTZ * CHUNK_TIME             /1000
BUFFER_SIZE    = BUFFER_SAMPLES * BITS/8 *CHANNELS  /2

RingBuffer     = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
InBuffer       = CreateBank(BUFFER_SAMPLES*4)
InPtrShort     = Short Ptr(InBuffer.Lock())
InPtrInt       = Int Ptr(InPtrShort)
RingPtrShort   = Short Ptr(RingBuffer.Samples)
RingPtrInt     = Int Ptr(RingPtrShort)


?bmxng
Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
?Not bmxng
Local fa_sound:Int  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
?

Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
RingPointer    =  BUFFER_SIZE/2
End Method






Method SendOneChunk()
LockMutex BufferMutex
Local ReadPointerMod:Int = (ReadPointer Mod BUFFER_SIZE) /2
Local r:Int=RingPointer/2

If ReadPointer + CHUNK_SIZE > WritePointer
Local Maxi:Int = (WritePointer-ReadPointer)/2
For Local i:Int = 0 To Maxi-1
RingPtrInt[i+r] = InPtrShort[i+ReadPointerMod]
Next
Print "Underrun"
For Local i:Int = Maxi To CHUNK_SIZE/2-1
RingPtrInt[i+r]=0
Next
ReadPointer=ReadPointer + Maxi
Else
For Local i:Int = 0 To CHUNK_SIZE/2-1
RingPtrInt[i+r] = InPtrInt[i+ReadPointerMod]
Next
ReadPointer=ReadPointer + CHUNK_SIZE
EndIf
RingPointer=(RingPointer + CHUNK_SIZE) Mod BUFFER_SIZE
UnlockMutex BufferMutex
End Method





Method ClearBuffer()
For Local i:Int =0 To BUFFER_SIZE/2
RingPtrInt[i]=0
Next
End Method






Method Transfer(ShortArray:Short[])
LockMutex BufferMutex
Local WritePointerMod:Int =  WritePointer Mod BUFFER_SIZE

If (ShortArray.Length + WritePointerMod) <= BUFFER_SIZE
'Print "in ptr"
For Local i:Int=0 To ShortArray.Length-1
InPtrShort[i+WritePointerMod] = ShortArray[i]
Next
Else
'Print " transfer in 2 teilen"
Local Maxi:Int = BUFFER_SIZE-WritePointerMod

For Local i:Int=0 To Maxi-1
InPtrShort[i+WritePointerMod] = ShortArray[i]
Next
For Local i:Int=Maxi To ShortArray.Length-1
InPtrShort[i-Maxi] = ShortArray[Maxi]
Next
EndIf
WritePointer=WritePointer + ShortArray.Length
UnlockMutex BufferMutex
End Method


Method CheckBufferOverRun()
Return
' private  cares about buffer overruns and report if you send to fast
Local grade:Int
If ShowPercent()>80
Print "RINGBUFFER: Buffer Overrun!"
If WITH_PROTECTION=0 Return
Print "RINGBUFFER: Protection ON! Wait for " + IntervalTime() + "msec"
Delay IntervalTime()
EndIf
End Method


End Type





Some examples will follow...

Code Example "Some Noise":
Code (BlitzMax) Select
SuperStrict
Import "RingBufferClass.bmx"
Graphics 800,600
RingBufferClass.SetDriver("FreeAudio DirectSound")
Global RingBuffer:RingBufferClass = New RingBufferClass
RingBuffer.CreateNew(12000, SF_MONO16LE)

Global SendSize:Int  = RingBuffer.IntervalSamples()
Global SendTime:Int = RingBuffer.IntervalTime()

Global WriteTime:Int = MilliSecs()
Repeat
    Cls
    SendSamples
    Flip
Until AppTerminate()

Function SendSamples()
            If WriteTime>MilliSecs() Return
            WriteTime =WriteTime + SendTime
            Local AudioArray:Int[SendSize]
            For Local i:Int=0 To SendSize-1
                    AudioArray[i] = Rand(-1000,+1000)
            Next
            RingBuffer.Send AudioArray
End Function




Code Example "Stereo Sinus Mouse":
Code (BlitzMax) Select


SuperStrict
Import "RingBufferClass.bmx"
Graphics 800,600
RingBufferClass.SetDriver("FreeAudio DirectSound")
Global RingBuffer:RingBufferClass = New RingBufferClass
RingBuffer.CreateNew(48000, SF_STEREO16LE)

Global SendSize:Int  = RingBuffer.IntervalSamples()
Global SendTime:Int = RingBuffer.IntervalTime()

Global WriteTime:Int = MilliSecs()
Repeat
    Cls
DrawText "Move your Mouse in X-dir to change the LEFT speaker", 100,100
DrawText "Move your Mouse in Y-dir to change the RIGHT speaker", 100,130
    SendSamples
    Flip
Until AppTerminate()


Global Arc1:Double,  Arc2:Double

Function SendSamples()
If WriteTime>MilliSecs() Return
WriteTime =WriteTime + SendTime
Local AudioArray:Int[SendSize]
For Local i:Int=0 To SendSize-1 Step 2
arc1=arc1+MouseX()/100.0+1
AudioArray[i] = Sin(arc1)*10000.0

arc2=arc2+MouseY()/100.0+1
AudioArray[i+1] = Sin(arc2)*10000.0
Next
RingBuffer.Send AudioArray
End Function




Code Example "Moving free in an Audio-File":
(you need the file TextABC.ogg from attachment)
Code (BlitzMax) Select


SuperStrict
Import "RingBufferClass.bmx"
Graphics 800,600
RingBufferClass.SetDriver("FreeAudio")
Global RingBuffer:RingBufferClass = New RingBufferClass

Global Source:TAudioSample=LoadAudioSample("TestABC.ogg")

RingBuffer.CreateNew(12000, SF_MONO16LE)

Global SendSize:Int  = RingBuffer.IntervalSamples()
Global SendTime:Int = RingBuffer.IntervalTime()

Global WriteTime:Int = MilliSecs()

Global Pointer:Int ,LastMouse:Int, Speed:Int=1

Repeat
    Cls
SetColor 255,255,255
DrawText "Mouse the Slider to navigate through the spoken ABC", 100,100
DrawRect 50,300,700,20
SetColor 1,1,1
DrawRect 52,302,696,16
SetColor 255,255,255
DrawOval 50+ Pointer*680/Source.Length/2,298,25,25
SetColor 1,1,1
DrawOval 52+ Pointer*680/Source.Length/2,300,21,21
   SendSamples
If MouseDown(1)
If MouseX()>49 And MouseX()<745
If LastMouse<>MouseX()
LastMouse = MouseX()
Local p%=(MouseX()-50)*Source.Length*2/700
Pointer= p & $FFFFF4
Speed=2
Else
Speed=1
EndIf
EndIf
Else
Speed=1
LastMouse=0
EndIf
    Flip
Until AppTerminate()



Function SendSamples()
If WriteTime>MilliSecs() Return
WriteTime =WriteTime + SendTime
Local AudioArray:Int[SendSize]
For Local i:Int=0 To SendSize-1
Local V%= Source.Samples[Pointer+2*i*Speed]+Source.Samples[Pointer+2*i*Speed+1]*256
AudioArray[i] = Source.Samples[Pointer+2*i*Speed]+Source.Samples[Pointer+2*i*Speed+1]*256
Next
RingBuffer.Send AudioArray
Pointer=(Pointer + 2*SendSize*Speed) Mod ((Source.Length*2)-2*SendSize)
End Function


more code sample will follow... see next post!
...back from Egypt