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

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

Previous topic - Next topic

Derron

Freeaudio can use different backends.

Another thing would be to get it working with audio.soloud (but not yet ... has time to try this out later).



@ previous post
cool... Treasures of knowledge :)


bye
Ron

iWasAdam

#31
@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...

Midimaster

The latency test tests also the variants of FreeAudio. Try it.

But the results are poor. Winner is always DirectSound with 50msec. FreeAudio is around 150msec and OpenAl at 100msec.

...on the way to Egypt

iWasAdam

Hmm that's an interesting finding.

One the ring buffer is set up latency should be the same across different devices - initial startup is where the issues usually occur

Midimaster

#34
Quote from: iWasAdam on March 25, 2021, 11:25:31
...One the ring buffer is set up latency should be the same across different devices...

This is still a test without the ring buffer. This test is checking the standard BlitzMax approach of using LoadSound() and PlaySound() under a certain Audio-Driver.

The next step will be to check, how the new ring buffer is doing its job. I fear that my 40msec will be added to the problems Freeaudio already shows.

by the way...

Does anybody know, who is responsible for "FreeAudio"? Will the software still being maintained? Where is the documentation?

...on the way to Egypt

iWasAdam

Nope, there is no hard documentation on what is going on and it is completely unmaintained.

I believe it was originated from a third party under licence.

I had a quick look into the sources (and this is going back a bit) so...

the devices set up the buffers and connections
freeaudio.cpp is the core sound player <- that was where the most of my work went rewriting the playback systems


Derron

#36
soloud just does the same think as freeaudio - it provides access to various backends. Normally you should not care for what it does in its inner beings. Playsound, Pausechannel ... these commands you are supposed to use. Same as "DrawImage()".
So yes the inner beings are undocumented, and samples? Are there any freesound samples ? Dunno about "pub.openal" - does it have samples?
The samples are in "brl.Audio" as this is what you "should" use.
There is also no example using pub.freetype or so ... as it is not what you should use.


Also: if latency is too high, you can always try to write your own implementation - my suggestion was just to show that it _might_ be possible to use what is already provided.



@ Maintenance
It is almost none of the modules really "maintained" currently - some stuff is updated here and there but nobody writes/extends documentation or so.


bye
Ron

Derron

Quote from: Midimaster on March 25, 2021, 09:55:07
It would be time to check, which driver causes which latency. Latency is the time the driver needs to really bring the sound to the speakers.

You think you hear the sound exactly in the moment where you start it playing? Wrong! Use this test app to find out how FreeAudio or DirectSound really perform on BlitzMax!

This is an executable BlitzMax-app. you need the Test_5_Clicks.ogg from the attachment. First select an avaiable driver in the code line #12. The start the app. Follow the steps in the app window. The 5 ticks will repeat every 6sec, so you can repeatly test the latency and get an avarage time.

For FreeAudio Alsa and also PulseAudio I get avg 210ms.


bye
Ron

Midimaster

210msec sounds to long for me. The drivers are bad, but not that bad!

do the test like a musician would do:

In the test you hear 4 ticks. Now you should not wait for the 5th and press the key, when you heared it.
Better listen to the four and the guess when the 5th will come. Now press, when you think it is time for the fifth.
This will bring better and realistic values.

Also be aware that a wireless mouse or wireless keyboard needs a lot of time. So if you use this you have an adidtional delay.

...on the way to Egypt

Derron

#39
Ah ... I always clicked as soon as I heard the 5th ... so I should click when I think it will just start to play.

One could consider taking this into the calculation - so hitting X as soon as one "hears" the audio.


I think one of the problems here is anticipation - you "expect" it to "click" ... and you want it to be exact...

Driver: FreeAudio Pulse Audio
197
245
149
181
141
148
219
172
78
150
203
61


Driver: FreeAudio ALSA
197
245
149
181
141
148
219
172
78
150
203
61

I wonder about the spread of eg 60 to 200 - this is rather big. Dunno how much delay event processing in bmax on linux has (so from OS event to app event)


But ... for audio stuff on linux you once had JACK ... or maybe even OSS worked good. With PulseAudio you get a lot of configurability (rerouting app 1 to play over bluetooth speaker 2, app 2 runs on hdmi over monitor, ...). And maybe Alsa is just a virtual backend leading to PulseAudio now too (dunno how it is confgured).

https://juho.tykkala.fi/Pulseaudio-and-latency


Ziel #1
Status: RUNNING
Name: alsa_output.pci-0000_00_14.2.analog-stereo
Beschreibung: Eingebautes Tongerät Analog Stereo
Treiber: module-alsa-card.c
Abtastwert-Angabe: s16le 2ch 44100Hz
Kanalzuordnung: front-left,front-right
Besitzer-Modul: 6
Stumm: nein
Lautstärke: front-left: 42597 /  65% / -11,23 dB,   front-right: 42597 /  65% / -11,23 dB
        Verteilung 0,00
Basis-Lautstärke: 65536 / 100% / 0,00 dB
Quellen-Monitor: alsa_output.pci-0000_00_14.2.analog-stereo.monitor
Latenz: 100093 usec, eingestellt 100136 usec
Flags: HARDWARE HW_MUTE_CTRL HW_VOLUME_CTRL DECIBEL_VOLUME LATENCY
Eigenschaften:
alsa.resolution_bits = "16"
device.api = "alsa"
device.class = "sound"
alsa.class = "generic"
alsa.subclass = "generic-mix"
alsa.name = "ALC887-VD Analog"
alsa.id = "ALC887-VD Analog"

So for this ouput I already have an configured latency of 100ms ... the link above describes to minimize latency.

bye
Ron

Midimaster

#40
Are these the only two drivers your system has?

Ok this means they have a really bad latency.

They differences can have two reasons:
- The system has alternating latency: improbable
- You are tapping not very accurate: probable

As you are no musician you get returned various measurement results. I'm pianist and the result vary from 40 to 80

But nevertheless... when you are doing the tests with all different devices on your system your "personal deviation" will always be the same. So you still can compare the devices and say "device 1 is better than device 2".

did you use a wired keyboard? Wireless keyboards need upto 100msec!

A second test is to record the signal of the speakers immediately with the same computer. Now you get a exact result, but ist a combined latency of input and output. See the FAQ of AUDACITY for details.

Latency test on the ring buffer
Ok, i combined my latency test code with the ringbuffer code and measure the latency:

My 40msec of the ringbuffer are not added to the given latency of the driver. It looks like the latency keeps the same with or without buffer.
So driver "FreeAudio" shows 150ms latency. "FreeAudio Mulitmedia" has 100msec and "FreeAudio DirectSound" 50msec.

It looks like the last one is the best. But if I use it I get problems when pressing keys while sound is playing. The sound sometimes stutters for this very short moment. If I raise the CHUNK_TIME to 40msec the problems are gone

Now you can hear the latency as a crackling noise:

In this code example you will hear the tap on the key
  • as a fine crackling noise, which appears after the latency time. Use this code and the Test_5_Click.ogg from the attachment
    Code (BlitzMax) Select
    SuperStrict

    Graphics 800, 600
    SetAudioDriver("FreeAudio DirectSound")
    Global WritePointer:Int, ReadPointer:Int, WriteTime:Int
    Global BITS:Int, CHANNELS:Int, BUFFER_SIZE:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, FORMAT:Int
    Global Volume#
    Const HERTZ:Int=48000

    Const CHUNK_TIME:Int = 20 'msec  (=441 samples)

    Global Source:TAudioSample=LoadAudioSample("Test_5_Clicks.ogg")
    FORMAT  = Source.Format
    'FORMAT = SF_STEREO16LE

    Select FORMAT
    Case SF_MONO16LE
    BITS:Int=16
    CHANNELS:Int=1
    CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
    BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000*4
    BUFFER_SIZE    = BUFFER_SAMPLES * 2 * CHANNELS
    Case SF_STEREO16LE
    BITS:Int=16
    CHANNELS:Int=2
    CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
    BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000 *4
    BUFFER_SIZE    = BUFFER_SAMPLES * 2 *CHANNELS
    Default
    Notify "Audio format not supported"
    End
    End Select

    Global Lesen:TStream= CreateRamStream(Source.Samples,Source.length * CHANNELS * BITS/8,True,0)

    Print "Chunksize=" + Chunk_size
    Print "buffersize=" + BUFFER_SIZE
    Print "source length=" + Source.length  + " format" + source.format + " in samples="  + Source.length * CHANNELS * BITS/8


    ' derron's part:
    Global Buffer:TAudioSample = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
    Local fa_sound:Byte Ptr = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
    Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)


    Global Schreiben:TStream= CreateRamStream(Buffer.Samples,Buffer_size,True,True)

    Global LastLatency%, Avarage%, Rounds%

    ' now put 40msec Latency into the Buffer:
    SendOneChunk()
    SendOneChunk()
    SendOneChunk()
    'already start playback with an empty buffer
    PlaySound sound
    WriteTime=MilliSecs()
    Local StartTime%=MilliSecs()


    Global KeyNew%
    Repeat
    Cls
    DrawText "tap on key [X] and listen to the fine crackling",100,100
    'DrawText "Move the mouse to pan between left and right",100,100
    'DrawText "LEFT",10,300
    'DrawText "MIDDLE",300,300
    'DrawText "RIGHT",700,300
    DrawTabText "Write to:", WritePointer  , 300,400
    DrawTabText "at time:", (WriteTime-StartTime) , 300,430
    DrawTabText "read from:", ReadPointer , 300,460

    If WriteTime<MilliSecs()
    ' this cares about really 20msec timing:
    WriteTime=WriteTime + CHUNK_TIME
    'Print "Write to " + WritePointer + " at time: " + (WriteTime-StartTime) +" " + Buffer.Length + " " + buffer_size
    SendOneChunk
    EndIf
    If KeyDown(KEY_X) And KeyNew=0
    keynew=MilliSecs()+1000
    Print latency(MilliSecs()-StartTime)
    EndIf
    If keynew<MilliSecs()
    keynew=0
    EndIf
    volume=1-MouseX()/800.0
    Flip 0
    Until AppTerminate()
    End


    Function DrawTabText(t1$,t2$,X%,y%)
    DrawText t1, X-TextWidth(T1)-70,Y
    DrawText t2, X-TextWidth(T2),Y
    End Function

    Function SendOneChunk()
    ' put a amount of samples into the buffer:
    Local Zeit%=MilliSecs()
    Lesen.Seek(ReadPointer)
    Schreiben.Seek(WritePointer)
    For Local i:Int = 0 Until CHUNK_SIZE Step 2
    Local value:Int=Lesen.ReadShort()
    If value>32768
    value=value-65535
    EndIf

    ' mouse volume:
    If keynew>MilliSecs()+1000-2*CHUNK_TIME
    keynew=MilliSecs()+1000-2*CHUNK_TIME-1
    value=32000
    ElseIf i Mod 4=0 Or FORMAT=SF_MONO16LE
    value=value*volume
    Else
    value=value*(1-volume)
    EndIf
    If value<0
    value=value+65535
    EndIf

    Schreiben.WriteShort(value)
    ReadPointer=ReadPointer+2
    Next
    WritePointer=(WritePointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
    ReadPointer=ReadPointer Mod (Source.length * CHANNELS * BITS/8)
    'Print (Zeit-MilliSecs())
    End Function




    Function Latency%(Value%)
    value=value Mod 1000
    If value>5000
    value=value-1000
    EndIf
    LastLatency=value
    Rounds=Rounds+1
    Avarage=Avarage+value
    Return value
    End Function
         



...on the way to Egypt

Derron

output is 145, 169,  137, 185, 152, 152
And the noise/crack is playing "after" the click sound - so I can hear it "separately".

In a second run I already anticipated "when to click" (unwillently) and values were 40-70 then. With an inbuilt latency of 100 already (according to configuration) I clicked before it should play :) ... so anticipated it already, which influences the test.


Will have to test it on Windows though - maybe DS has really a bit less on latency.


bye
Ron


Midimaster

#42
the time difference between the tapping and the crack is your real latency. In the moment, when you press the key, I add a crackling into the samples and then it needs time (latency) until you hear it. So this is the combined latency of the ring buffer and the device.

To anticipate the test is not a good idea. From listening to the 4 clicks before you can guess the time difference between the clicks and you shall tap, when you expect that you will hear the 5th click. like a musician, who is  listening to a count in "1-2-3-4-1" from the band leader and shall play on the second "1".

So, you only have this devices? The first test exe showed a list of aviable devices. You should try it on a windows computer to see, how much better the directsound is!!!

Next step is now to convert the SendOneChunk()  into a thread. but I have no idea how to do that and I never used threads. So I would need your help now.





...on the way to Egypt

Derron

#43
Threading is not an issue.

What is important is: do never WRITE to something without blocking access to it from other threads (including the main). For this we simply use a "mutex" (TMutex is the typename in Bmax).
LockMutex(myStreamMutex)
stream.Write()...
UnlockMutex(myStreamMutex)

that way you simply .. ensure that you do only continue (lock will wait until mutex is free) if the mutex is not "locked". So if any other threat operates on the the stream (eg reading) it will lock the mutex while doing so.
This way thread A locks the mutex and reads. Thread B wants to write and has to wait until the mutex is no longer locked (thread A finished reading and unlocked the mutex).

This way they would not interfer each other.


Running a thread is rather easy: you create a thread and pass a function to it (and optional some thread data object). This function is then executed inside this thread. As the thread does not block the main thread, you can simply do a "while wend/repeat forever..." loop inside the function. The thread would run forever then.
I often have a global integer variable telling if the thread has to exit. Then the thread's function loop is "while notExitThread .... doX; doY; wend".

Why global integer? Integers are written in one cpu instruction (atomic operation) - so a thread A can write to it without having thread B reading it simultaneously and getting a "mix of old and new value" returned. So for changing a single integer (or byte) value you do not need a mutex to "save" it from concurrent accesses.


I do not know why it segfaults when creating the mutexes right on global-definition. But I assume it is how globals are getting filled with values. - not in order


Code (BlitzMax) Select

SuperStrict
'Framework Brl.StandardIO
Import Brl.Audio
Import Brl.FreeAudioAudio
Import Brl.OGGLoader
Import Brl.GLMax2D
Import Brl.stream
Import Brl.Ramstream

Graphics 800,600
Local Drivername$[9]
Local Nr%
For Local a:String = EachIn AudioDrivers()
Drivername[Nr]=a
Nr :+ 1
Next

'*** Select driver here:***********
Nr=1
print "Driver: " + Drivername[Nr]
SetAudioDriver DriverName[Nr]




Global WritePointer:Int, ReadPointer:Int, WriteTime:Int
Global BITS:Int, CHANNELS:Int, BUFFER_SIZE:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, FORMAT:Int
Global Volume#
Const HERTZ:Int=48000

Const CHUNK_TIME:Int = 20 'msec  (=441 samples)

Global Source:TAudioSample=LoadAudioSample("Test_5_Clicks.ogg")
FORMAT  = Source.Format
'FORMAT = SF_STEREO16LE

Select FORMAT
        Case SF_MONO16LE
                BITS:Int=16
                CHANNELS:Int=1
                CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
                BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000*4
                BUFFER_SIZE    = BUFFER_SAMPLES * 2 * CHANNELS
        Case SF_STEREO16LE
                BITS:Int=16
                CHANNELS:Int=2
                CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
                BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000 *4
                BUFFER_SIZE    = BUFFER_SAMPLES * 2 *CHANNELS
        Default
                Notify "Audio format not supported"
                End
End Select

Global Lesen:TStream= CreateRamStream(Source.Samples,Source.length * CHANNELS * BITS/8,True,0)

Print "Chunksize=" + Chunk_size
Print "buffersize=" + BUFFER_SIZE
Print "source length=" + Source.length  + " format" + source.format + " in samples="  + Source.length * CHANNELS * BITS/8


' derron's part:
Global Buffer:TAudioSample = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
Local fa_sound:Byte Ptr = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)


'Derron: thread stuff
Global ReadStreamMutex:TMutex
Global WriteStreamMutex:TMutex
Global ExitSendAChunkThread:Int = False
Global RefillBufferThread:TThread
Function SendAChunkThread:Object(data:Object)
While not ExitSendAChunkThread
        If WriteTime < MilliSecs()
' this cares about really 20msec timing:
WriteTime :+ CHUNK_TIME
'Print "Write to " + WritePointer + " at time: " + (WriteTime-StartTime) +" " + Buffer.Length + " " + buffer_size
SendOneChunk()
Else
'we could have a fixed delay ... so only checking every 5ms or so
'or we could ... eg try to wait longer if possible

'did not check if this is useful
If Millisecs() - WriteTime > 10
Delay(Millisecs() - WriteTime - 5) 'so if it was 50, we wait 45
else
Delay(5)
endif
        EndIf
Wend
End Function



Global Schreiben:TStream= CreateRamStream(Buffer.Samples,Buffer_size,True,True)

Global LastLatency%, Avarage%, Rounds%


' now put 40msec Latency into the Buffer:
SendOneChunk()
SendOneChunk()
SendOneChunk()
'already start playback with an empty buffer
PlaySound sound
WriteTime=MilliSecs()
Local StartTime%=MilliSecs()

'Derron: thread stuff
'initialize thread (do this after the latency-buffer-fill-SendOneChunk()
'and also ensure mutexes are created before calling SendOneChunk()
RefillBufferThread = CreateThread(SendAChunkThread, null) 'null = no data to pass




Global KeyNew%
Repeat
        Cls
        DrawText "tap on key [X] and listen to the fine crackling",100,100
        'DrawText "Move the mouse to pan between left and right",100,100
        'DrawText "LEFT",10,300
        'DrawText "MIDDLE",300,300
        'DrawText "RIGHT",700,300
        DrawTabText "Write to:", WritePointer  , 300,400
        DrawTabText "at time:", (WriteTime-StartTime) , 300,430
        DrawTabText "read from:", ReadPointer , 300,460
       
        If KeyDown(KEY_X) And KeyNew=0
                keynew=MilliSecs()+1000
                Print latency(MilliSecs()-StartTime)
        EndIf
        If keynew<MilliSecs()
                keynew=0
        EndIf
        volume=1-MouseX()/800.0
        Flip 0
Until AppTerminate()
'Derron: thread stuff
'ensure we finish our threads?
'maybe it wants to cleanup some stuff or so...
ExitSendAChunkThread = True
If RefillBufferThread Then WaitThread(RefillBufferThread)

End


Function DrawTabText(t1$,t2$,X%,y%)
        DrawText t1, X-TextWidth(T1)-70,Y
        DrawText t2, X-TextWidth(T2),Y
End Function

Function SendOneChunk()
If not ReadStreamMutex then ReadStreamMutex = CreateMutex()
If not WriteStreamMutex then WriteStreamMutex = CreateMutex()

' put a amount of samples into the buffer:
Local Zeit%=MilliSecs()
LockMutex(ReadStreamMutex)
Lesen.Seek(ReadPointer)
UnlockMutex(ReadStreamMutex)
LockMutex(WriteStreamMutex)
Schreiben.Seek(WritePointer)
UnlockMutex(WriteStreamMutex)

For Local i:Int = 0 Until CHUNK_SIZE Step 2
LockMutex(ReadStreamMutex)
Local value:Int=Lesen.ReadShort()
UnlockMutex(ReadStreamMutex)
If value>32768
value=value-65535
EndIf

' mouse volume:
If      keynew>MilliSecs()+1000-2*CHUNK_TIME
keynew=MilliSecs()+1000-2*CHUNK_TIME-1
value=32000
ElseIf i Mod 4=0 Or FORMAT=SF_MONO16LE
value=value*volume
Else
value=value*(1-volume)                         
EndIf
If value<0
value=value+65535
EndIf

LockMutex(WriteStreamMutex)
Schreiben.WriteShort(value)
UnlockMutex(WriteStreamMutex)
ReadPointer=ReadPointer+2
Next
WritePointer=(WritePointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
ReadPointer=ReadPointer Mod (Source.length * CHANNELS * BITS/8)
'Print (Zeit-MilliSecs())
End Function




Function Latency%(Value%)
        value=value Mod 1000
        If value>5000
                value=value-1000
        EndIf
        LastLatency=value
        Rounds=Rounds+1
        Avarage=Avarage+value
        Return value
End Function


As you see I am doing a lot of "lock unlock" in the last function ... you could split "read" and "write" a bit more to save on lock/unlock (so reading in all values into an array and then writing that array in a second step  - and only these big steps surrounded by lock/unlock).

If reading and writing is fast enough - you could also simply have a single mutex around the whole function content (so lock before seek and unlock after writing).


Just try it out - threads can be really helpful (eg. I load my assets in threads ... BUT ... you cannot load TImage in threads - these are textures on the GPU so you need to load them on the main thread. But ... you can load pixmaps (so decode images like png from file to RAM). So I load pixmaps in threads - and then just LoadImage(pixmap) in the main thread at the end - saves a lot of time).


Edit: Ensure to NOT use Millisecs() that way you are doing in this prototype. Millisecs() is an integer and does a wrapover after around 28 days of uptime() (eg notebooks which are only "closed" and hibernating - on waking up the uptime continues). Wrapover means, it becomes suddenly negative (from max integer value to minimum integer value).

Assume max is 10.000 and min is -10.000
lastTime = 9999
nowTime = lastTime + 50 'assume 50ms gone)
nowTime is now -9951


your check "If nowTime - lastTime > 50" would here return false for one cycle (as you later adjust "lastTime" this will become "OKish" in the next loop cycle).


bye
Ron

Midimaster

#44
new step, but still without threading (comes with the next version)


RingBuffer as a Class with FIFO

The next version has encapsuled the Sound playing into a class. This makes it easy to use. The main code needs only a few steps to make it running.

you can start it:
Code (BlitzMax) Select
RingBufferClass.SetDriver("FreeAudio DirectSound")
Global RingBuffer:RingBufferClass=New RingBufferClass
RingBuffer.CreateNew(Format, Hertz)
SendSize= RingBuffer.HowManySamples()

Format is one the both values SF_STEREO16 or SF_MONO16. In the current version the ringbuffer ticks every 20msec. So you have to know how many Samples you need to send every 20msec. Sending exactly this number of samples ensures that the audio stream does not break.

keep it running:
Code (BlitzMax) Select
Repeat
Cls
RingBuffer.Watch
....
Flip 0
Until AppTerminate()
End


feed it with sound:
Code (BlitzMax) Select

Local AudioArray:Short[SendSize]
For Local i%=0 To SendSize-1
AudioArray[i]=value what ever....
Next
RingBuffer.Send AudioArray

You put samples values into a SHORT-array and send this to the Class.

Thats all you have to know! Thats all you need to do!

Lets have a look into the Class

The class has a function for receiving datas:
Code (BlitzMax) Select
Method Send(AudioArray:Short[])
Local WritePointerMod% = WritePointer Mod BUFFER_SIZE
Lesen.Seek WritePointerMod

If AudioArray.Length*2 + (WritePointerMod) <= BUFFER_SIZE
' for the case the array fits into the in-buffer without reaching its end
.....
Else
' for the case the array will skip the limit of the inbuffer and needs MOD
.....
EndIf
WritePointer=WritePointer + AudioArray.Length*2
End Method

In Inbuffer is again a ringbuffer,  but ringbuffer have an end. To prevent the need of always calculating each datas position in the inbuffer (with MOD) the function differs two cases.

The main function is feeding the audio device inner ringbuffer :
Code (BlitzMax) Select
Private Method SendOneChunk()
Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
Lesen.Seek ReadPointerMod
Schreiben.Seek RingPointer

For Local i:Int = 0 To CHUNK_SIZE-1
Schreiben.WriteByte(Lesen.ReadByte())
Next
ReadPointer=ReadPointer + CHUNK_SIZE
RingPointer=(RingPointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
End Method


For the case, the user did not send enough data for playing, the ringbuffer is filles with silence:
Code (BlitzMax) Select
Private Method SendOneChunk()
Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
Lesen.Seek ReadPointerMod
Schreiben.Seek RingPointer

If ReadPointer + CHUNK_SIZE > WritePointer
Local Maxi%=WritePointer-ReadPointer

For Local i:Int = 0 To Maxi-1
Schreiben.WriteByte Lesen.ReadByte()
Next
For Local i:Int = Maxi To CHUNK_SIZE-1
Schreiben.WriteByte 0
Next
ReadPointer=ReadPointer + Maxi
Else
....


Ready to run example

So here is the complete code. It is an executable code. Use the Test.ogg from post#20 attachment.
You can simulate to "send nothing" by pressing the left mouse. This will cause silence but no stuttering.

Code (BlitzMax) Select
Global SendSize% , ReadPointer%

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

Const SEND_TIME:Int = 20 'msec

Global Source:TAudioSample=LoadAudioSample("Test.ogg")
Global Lesen:TStream = CreateRamStream(Source.Samples,Source.Length*4,True,True)



Global RingBuffer:RingBufferClass=New RingBufferClass
RingBuffer.CreateNew(Source.Format,Source.Hertz)
SendSize = RingBuffer.HowManySamples()

Local WriteTime%=MilliSecs()


Repeat
RingBuffer.Watch
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


If WriteTime<MilliSecs()
' this cares about really 20msec timing:
WriteTime =WriteTime + SEND_TIME
If MouseDown(1)=0
SendAudio
EndIf
EndIf
Flip 0
Until AppTerminate()
End


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

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

'_________________________________________________________________________________

Type RingBufferClass
Global MyDriver$

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

Field Buffer:TAudioSample, Sound:TSound
Field Schreiben:TStream, WatchTime%, Lesen:TStream

Field InBuffer:TAudioSample


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%)
If MyDriver=""
Notify "No AudioDriver selected"
End
EndIf
Self.HERTZ=Hertz
Self.FORMAT=Format
DefineBuffer
RingPointer=(3*CHUNK_SIZE)
WatchTime=MilliSecs()
PlaySound Sound
End Method


Private Method DefineBuffer()
CHUNK_TIME=20
Local BITS:Int
Select FORMAT
Case SF_MONO16LE
BITS:Int=16
CHANNELS:Int=1
Case SF_STEREO16LE
BITS:Int=16
CHANNELS:Int=2
Default
Notify "Audio format not supported"
End
End Select
CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * 8 /1000
BUFFER_SIZE    = BUFFER_SAMPLES * 2 *CHANNELS
Buffer         = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
InBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
Schreiben      = CreateRamStream(Buffer.Samples,Buffer_size,True,True)
Lesen          = CreateRamStream(InBuffer.Samples,Buffer_size,True,True)
End Method


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


Private Method SendOneChunk()
Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
Lesen.Seek ReadPointerMod
Schreiben.Seek RingPointer

If ReadPointer + CHUNK_SIZE > WritePointer
Local Maxi%=WritePointer-ReadPointer

For Local i:Int = 0 To Maxi-1
Schreiben.WriteByte Lesen.ReadByte()
Next
For Local i:Int = Maxi To CHUNK_SIZE-1
Schreiben.WriteByte 0
Next
ReadPointer=ReadPointer + Maxi
Else
For Local i:Int = 0 To CHUNK_SIZE-1
Schreiben.WriteByte(Lesen.ReadByte())
Next
ReadPointer=ReadPointer + CHUNK_SIZE
EndIf
RingPointer=(RingPointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
End Method


Method Send(AudioArray:Short[])
Local WritePointerMod% = WritePointer Mod BUFFER_SIZE
Lesen.Seek WritePointerMod

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

Else
Local Maxi% = BUFFER_SIZE-WritePointerMod

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


Method HowManySamples%()
Return CHUNK_SIZE/2
End Method
End Type

...on the way to Egypt