October 19, 2021, 09:25:31

Author Topic: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer  (Read 3798 times)

Offline Derron

  • Hero Member
  • *****
  • Posts: 3664
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #30 on: March 25, 2021, 09:57:01 »
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

Offline iWasAdam

  • Hero Member
  • *****
  • Posts: 2478
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #31 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...
« Last Edit: March 25, 2021, 10:49:20 by iWasAdam »

Offline Midimaster

  • Sr. Member
  • ****
  • Posts: 363
    • Midimaster Music Education Software
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #32 on: March 25, 2021, 10:54:33 »
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.

See my current project on PlayStore: 20Tracks-Audio-Player https://play.google.com/store/apps/details?id=midimaster.twentytrackd

Offline iWasAdam

  • Hero Member
  • *****
  • Posts: 2478
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #33 on: March 25, 2021, 11:25:31 »
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

Offline Midimaster

  • Sr. Member
  • ****
  • Posts: 363
    • Midimaster Music Education Software
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #34 on: March 25, 2021, 11:59:26 »
...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?

« Last Edit: March 25, 2021, 12:05:48 by Midimaster »
See my current project on PlayStore: 20Tracks-Audio-Player https://play.google.com/store/apps/details?id=midimaster.twentytrackd

Offline iWasAdam

  • Hero Member
  • *****
  • Posts: 2478
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #35 on: March 25, 2021, 12:45:58 »
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


Offline Derron

  • Hero Member
  • *****
  • Posts: 3664
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #36 on: March 25, 2021, 13:38:42 »
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
« Last Edit: March 25, 2021, 16:21:35 by Derron »

Offline Derron

  • Hero Member
  • *****
  • Posts: 3664
Re: Testing latency of different Audio Drivers
« Reply #37 on: March 25, 2021, 17:27:36 »
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

Offline Midimaster

  • Sr. Member
  • ****
  • Posts: 363
    • Midimaster Music Education Software
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #38 on: March 25, 2021, 17:52:59 »
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.

See my current project on PlayStore: 20Tracks-Audio-Player https://play.google.com/store/apps/details?id=midimaster.twentytrackd

Offline Derron

  • Hero Member
  • *****
  • Posts: 3664
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #39 on: March 25, 2021, 18:37:30 »
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...
Code: [Select]
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

Code: [Select]
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
« Last Edit: March 25, 2021, 18:49:55 by Derron »

Offline Midimaster

  • Sr. Member
  • ****
  • Posts: 363
    • Midimaster Music Education Software
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #40 on: March 26, 2021, 00:58:48 »
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
  1. SuperStrict
  2.  
  3. Graphics 800, 600
  4. SetAudioDriver("FreeAudio DirectSound")
  5. Global WritePointer:Int, ReadPointer:Int, WriteTime:Int
  6. Global BITS:Int, CHANNELS:Int, BUFFER_SIZE:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, FORMAT:Int
  7. Global Volume#
  8. Const HERTZ:Int=48000
  9.  
  10. Const CHUNK_TIME:Int = 20 'msec  (=441 samples)
  11.  
  12. Global Source:TAudioSample=LoadAudioSample("Test_5_Clicks.ogg")
  13. FORMAT  = Source.Format
  14. 'FORMAT = SF_STEREO16LE
  15.  
  16. Select FORMAT
  17.         Case SF_MONO16LE
  18.                 BITS:Int=16
  19.                 CHANNELS:Int=1
  20.                 CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  21.                 BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000*4
  22.                 BUFFER_SIZE    = BUFFER_SAMPLES * 2 * CHANNELS
  23.         Case SF_STEREO16LE
  24.                 BITS:Int=16
  25.                 CHANNELS:Int=2
  26.                 CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  27.                 BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000 *4
  28.                 BUFFER_SIZE    = BUFFER_SAMPLES * 2 *CHANNELS
  29.         Default
  30.                 Notify "Audio format not supported"
  31.                 End
  32. End Select
  33.  
  34. Global Lesen:TStream= CreateRamStream(Source.Samples,Source.length * CHANNELS * BITS/8,True,0)
  35.  
  36. Print "Chunksize=" + Chunk_size
  37. Print "buffersize=" + BUFFER_SIZE
  38. Print "source length=" + Source.length  + " format" + source.format + " in samples="  + Source.length * CHANNELS * BITS/8
  39.  
  40.  
  41. ' derron's part:
  42. Global Buffer:TAudioSample = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  43. Local fa_sound:Byte Ptr = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
  44. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  45.  
  46.  
  47. Global Schreiben:TStream= CreateRamStream(Buffer.Samples,Buffer_size,True,True)
  48.  
  49. Global LastLatency%, Avarage%, Rounds%
  50.  
  51. ' now put 40msec Latency into the Buffer:
  52. SendOneChunk()
  53. SendOneChunk()
  54. SendOneChunk()
  55. 'already start playback with an empty buffer
  56. PlaySound sound
  57. WriteTime=MilliSecs()
  58. Local StartTime%=MilliSecs()
  59.  
  60.  
  61. Global KeyNew%
  62. Repeat
  63.         Cls
  64.         DrawText "tap on key [X] and listen to the fine crackling",100,100
  65.         'DrawText "Move the mouse to pan between left and right",100,100
  66.         'DrawText "LEFT",10,300
  67.         'DrawText "MIDDLE",300,300
  68.         'DrawText "RIGHT",700,300
  69.         DrawTabText "Write to:", WritePointer  , 300,400
  70.         DrawTabText "at time:", (WriteTime-StartTime) , 300,430
  71.         DrawTabText "read from:", ReadPointer , 300,460
  72.        
  73.         If WriteTime<MilliSecs()
  74.                 ' this cares about really 20msec timing:
  75.                 WriteTime=WriteTime + CHUNK_TIME
  76.                 'Print "Write to " + WritePointer + " at time: " + (WriteTime-StartTime) +" " + Buffer.Length + " " + buffer_size
  77.                 SendOneChunk
  78.         EndIf
  79.         If KeyDown(KEY_X) And KeyNew=0
  80.                 keynew=MilliSecs()+1000
  81.                 Print latency(MilliSecs()-StartTime)
  82.         EndIf
  83.         If keynew<MilliSecs()
  84.                 keynew=0
  85.         EndIf
  86.         volume=1-MouseX()/800.0
  87.         Flip 0
  88. Until AppTerminate()
  89. End
  90.  
  91.  
  92. Function DrawTabText(t1$,t2$,X%,y%)
  93.         DrawText t1, X-TextWidth(T1)-70,Y
  94.         DrawText t2, X-TextWidth(T2),Y
  95. End Function
  96.  
  97. Function SendOneChunk()
  98.                 ' put a amount of samples into the buffer:
  99.                 Local Zeit%=MilliSecs()
  100.                                 Lesen.Seek(ReadPointer)
  101.                                 Schreiben.Seek(WritePointer)
  102.                 For Local i:Int = 0 Until CHUNK_SIZE Step 2
  103.                                 Local value:Int=Lesen.ReadShort()
  104.                                 If value>32768
  105.                                         value=value-65535
  106.                                 EndIf
  107.                                
  108.                                 ' mouse volume:
  109.                                 If      keynew>MilliSecs()+1000-2*CHUNK_TIME
  110.                                         keynew=MilliSecs()+1000-2*CHUNK_TIME-1
  111.                                         value=32000
  112.                                 ElseIf i Mod 4=0 Or FORMAT=SF_MONO16LE
  113.                                         value=value*volume
  114.                                 Else
  115.                                         value=value*(1-volume)                         
  116.                                 EndIf
  117.                                 If value<0
  118.                                         value=value+65535
  119.                                 EndIf
  120.                                
  121.                                 Schreiben.WriteShort(value)
  122.                                 ReadPointer=ReadPointer+2
  123.                 Next
  124.                 WritePointer=(WritePointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
  125.                 ReadPointer=ReadPointer Mod (Source.length * CHANNELS * BITS/8)
  126.                 'Print (Zeit-MilliSecs())
  127. End Function
  128.  
  129.  
  130.  
  131.  
  132. Function Latency%(Value%)
  133.         value=value Mod 1000
  134.         If value>5000
  135.                 value=value-1000
  136.         EndIf
  137.         LastLatency=value
  138.         Rounds=Rounds+1
  139.         Avarage=Avarage+value
  140.         Return value
  141. End Function
  142.      
  143.  


« Last Edit: March 26, 2021, 02:08:57 by Midimaster »
See my current project on PlayStore: 20Tracks-Audio-Player https://play.google.com/store/apps/details?id=midimaster.twentytrackd

Offline Derron

  • Hero Member
  • *****
  • Posts: 3664
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #41 on: March 26, 2021, 07:16:34 »
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


Offline Midimaster

  • Sr. Member
  • ****
  • Posts: 363
    • Midimaster Music Education Software
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #42 on: March 26, 2021, 07:33:08 »
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.





« Last Edit: March 27, 2021, 00:48:18 by Midimaster »
See my current project on PlayStore: 20Tracks-Audio-Player https://play.google.com/store/apps/details?id=midimaster.twentytrackd

Offline Derron

  • Hero Member
  • *****
  • Posts: 3664
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #43 on: March 26, 2021, 10:00:44 »
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
  1. SuperStrict
  2. 'Framework Brl.StandardIO
  3. Import Brl.Audio
  4. Import Brl.FreeAudioAudio
  5. Import Brl.OGGLoader
  6. Import Brl.GLMax2D
  7. Import Brl.stream
  8. Import Brl.Ramstream
  9.  
  10. Graphics 800,600
  11. Local Drivername$[9]
  12. Local Nr%
  13. For Local a:String = EachIn AudioDrivers()
  14.         Drivername[Nr]=a
  15.         Nr :+ 1
  16. Next
  17.  
  18. '*** Select driver here:***********
  19. Nr=1
  20. print "Driver: " + Drivername[Nr]
  21. SetAudioDriver DriverName[Nr]
  22.  
  23.  
  24.  
  25.  
  26. Global WritePointer:Int, ReadPointer:Int, WriteTime:Int
  27. Global BITS:Int, CHANNELS:Int, BUFFER_SIZE:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, FORMAT:Int
  28. Global Volume#
  29. Const HERTZ:Int=48000
  30.  
  31. Const CHUNK_TIME:Int = 20 'msec  (=441 samples)
  32.  
  33. Global Source:TAudioSample=LoadAudioSample("Test_5_Clicks.ogg")
  34. FORMAT  = Source.Format
  35. 'FORMAT = SF_STEREO16LE
  36.  
  37. Select FORMAT
  38.         Case SF_MONO16LE
  39.                 BITS:Int=16
  40.                 CHANNELS:Int=1
  41.                 CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  42.                 BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000*4
  43.                 BUFFER_SIZE    = BUFFER_SAMPLES * 2 * CHANNELS
  44.         Case SF_STEREO16LE
  45.                 BITS:Int=16
  46.                 CHANNELS:Int=2
  47.                 CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  48.                 BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * BITS/8/1000 *4
  49.                 BUFFER_SIZE    = BUFFER_SAMPLES * 2 *CHANNELS
  50.         Default
  51.                 Notify "Audio format not supported"
  52.                 End
  53. End Select
  54.  
  55. Global Lesen:TStream= CreateRamStream(Source.Samples,Source.length * CHANNELS * BITS/8,True,0)
  56.  
  57. Print "Chunksize=" + Chunk_size
  58. Print "buffersize=" + BUFFER_SIZE
  59. Print "source length=" + Source.length  + " format" + source.format + " in samples="  + Source.length * CHANNELS * BITS/8
  60.  
  61.  
  62. ' derron's part:
  63. Global Buffer:TAudioSample = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  64. Local fa_sound:Byte Ptr = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
  65. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  66.  
  67.  
  68. 'Derron: thread stuff
  69. Global ReadStreamMutex:TMutex
  70. Global WriteStreamMutex:TMutex
  71. Global ExitSendAChunkThread:Int = False
  72. Global RefillBufferThread:TThread
  73. Function SendAChunkThread:Object(data:Object)
  74.         While not ExitSendAChunkThread
  75.         If WriteTime < MilliSecs()
  76.                         ' this cares about really 20msec timing:
  77.                         WriteTime :+ CHUNK_TIME
  78.                         'Print "Write to " + WritePointer + " at time: " + (WriteTime-StartTime) +" " + Buffer.Length + " " + buffer_size
  79.                         SendOneChunk()
  80.                 Else
  81.                         'we could have a fixed delay ... so only checking every 5ms or so
  82.                         'or we could ... eg try to wait longer if possible
  83.                
  84.                         'did not check if this is useful                       
  85.                         If Millisecs() - WriteTime > 10
  86.                                 Delay(Millisecs() - WriteTime - 5) 'so if it was 50, we wait 45
  87.                         else
  88.                                 Delay(5)
  89.                         endif
  90.         EndIf
  91.         Wend
  92. End Function
  93.  
  94.  
  95.  
  96. Global Schreiben:TStream= CreateRamStream(Buffer.Samples,Buffer_size,True,True)
  97.  
  98. Global LastLatency%, Avarage%, Rounds%
  99.  
  100.  
  101. ' now put 40msec Latency into the Buffer:
  102. SendOneChunk()
  103. SendOneChunk()
  104. SendOneChunk()
  105. 'already start playback with an empty buffer
  106. PlaySound sound
  107. WriteTime=MilliSecs()
  108. Local StartTime%=MilliSecs()
  109.  
  110. 'Derron: thread stuff
  111. 'initialize thread (do this after the latency-buffer-fill-SendOneChunk()
  112. 'and also ensure mutexes are created before calling SendOneChunk()
  113. RefillBufferThread = CreateThread(SendAChunkThread, null) 'null = no data to pass
  114.  
  115.  
  116.  
  117.  
  118. Global KeyNew%
  119. Repeat
  120.         Cls
  121.         DrawText "tap on key [X] and listen to the fine crackling",100,100
  122.         'DrawText "Move the mouse to pan between left and right",100,100
  123.         'DrawText "LEFT",10,300
  124.         'DrawText "MIDDLE",300,300
  125.         'DrawText "RIGHT",700,300
  126.         DrawTabText "Write to:", WritePointer  , 300,400
  127.         DrawTabText "at time:", (WriteTime-StartTime) , 300,430
  128.         DrawTabText "read from:", ReadPointer , 300,460
  129.        
  130.         If KeyDown(KEY_X) And KeyNew=0
  131.                 keynew=MilliSecs()+1000
  132.                 Print latency(MilliSecs()-StartTime)
  133.         EndIf
  134.         If keynew<MilliSecs()
  135.                 keynew=0
  136.         EndIf
  137.         volume=1-MouseX()/800.0
  138.         Flip 0
  139. Until AppTerminate()
  140. 'Derron: thread stuff
  141. 'ensure we finish our threads?
  142. 'maybe it wants to cleanup some stuff or so...
  143. ExitSendAChunkThread = True
  144. If RefillBufferThread Then WaitThread(RefillBufferThread)
  145.  
  146. End
  147.  
  148.  
  149. Function DrawTabText(t1$,t2$,X%,y%)
  150.         DrawText t1, X-TextWidth(T1)-70,Y
  151.         DrawText t2, X-TextWidth(T2),Y
  152. End Function
  153.  
  154. Function SendOneChunk()
  155.         If not ReadStreamMutex then ReadStreamMutex = CreateMutex()
  156.         If not WriteStreamMutex then WriteStreamMutex = CreateMutex()
  157.  
  158.         ' put a amount of samples into the buffer:
  159.         Local Zeit%=MilliSecs()
  160.         LockMutex(ReadStreamMutex)
  161.                 Lesen.Seek(ReadPointer)
  162.         UnlockMutex(ReadStreamMutex)
  163.         LockMutex(WriteStreamMutex)
  164.                 Schreiben.Seek(WritePointer)
  165.         UnlockMutex(WriteStreamMutex)
  166.  
  167.         For Local i:Int = 0 Until CHUNK_SIZE Step 2
  168.                 LockMutex(ReadStreamMutex)
  169.                         Local value:Int=Lesen.ReadShort()
  170.                 UnlockMutex(ReadStreamMutex)
  171.                 If value>32768
  172.                                 value=value-65535
  173.                 EndIf
  174.  
  175.                 ' mouse volume:
  176.                 If      keynew>MilliSecs()+1000-2*CHUNK_TIME
  177.                                 keynew=MilliSecs()+1000-2*CHUNK_TIME-1
  178.                                 value=32000
  179.                 ElseIf i Mod 4=0 Or FORMAT=SF_MONO16LE
  180.                                 value=value*volume
  181.                 Else
  182.                                 value=value*(1-volume)                        
  183.                 EndIf
  184.                 If value<0
  185.                                 value=value+65535
  186.                 EndIf
  187.  
  188.                 LockMutex(WriteStreamMutex)
  189.                         Schreiben.WriteShort(value)
  190.                 UnlockMutex(WriteStreamMutex)
  191.                 ReadPointer=ReadPointer+2
  192.         Next
  193.         WritePointer=(WritePointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
  194.         ReadPointer=ReadPointer Mod (Source.length * CHANNELS * BITS/8)
  195.         'Print (Zeit-MilliSecs())
  196. End Function
  197.  
  198.  
  199.  
  200.  
  201. Function Latency%(Value%)
  202.         value=value Mod 1000
  203.         If value>5000
  204.                 value=value-1000
  205.         EndIf
  206.         LastLatency=value
  207.         Rounds=Rounds+1
  208.         Avarage=Avarage+value
  209.         Return value
  210. End Function
  211.  


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
« Last Edit: March 26, 2021, 10:19:07 by Derron »

Offline Midimaster

  • Sr. Member
  • ****
  • Posts: 363
    • Midimaster Music Education Software
Re: A new Audio-Out Approach in BlitzMax PortAudio
« Reply #44 on: March 27, 2021, 00:11:41 »
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
  1. RingBufferClass.SetDriver("FreeAudio DirectSound")
  2. Global RingBuffer:RingBufferClass=New RingBufferClass
  3. RingBuffer.CreateNew(Format, Hertz)
  4. SendSize= RingBuffer.HowManySamples()
  5.  
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
  1. Repeat
  2.         Cls
  3.         RingBuffer.Watch
  4.         ....
  5.         Flip 0
  6. Until AppTerminate()
  7. End
  8.  

feed it with sound:
Code: BlitzMax
  1. Local AudioArray:Short[SendSize]
  2. For Local i%=0 To SendSize-1
  3.         AudioArray[i]=value what ever....
  4. Next
  5. RingBuffer.Send AudioArray
  6.  
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
  1.         Method Send(AudioArray:Short[])
  2.                 Local WritePointerMod% = WritePointer Mod BUFFER_SIZE
  3.                 Lesen.Seek WritePointerMod
  4.                
  5.                 If AudioArray.Length*2 + (WritePointerMod) <= BUFFER_SIZE
  6.                         ' for the case the array fits into the in-buffer without reaching its end
  7.                         .....          
  8.                 Else
  9.                         ' for the case the array will skip the limit of the inbuffer and needs MOD
  10.                         .....
  11.                 EndIf
  12.                 WritePointer=WritePointer + AudioArray.Length*2
  13.         End Method
  14.  
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
  1. Private Method SendOneChunk()
  2.         Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
  3.         Lesen.Seek ReadPointerMod
  4.         Schreiben.Seek RingPointer
  5.        
  6.         For Local i:Int = 0 To CHUNK_SIZE-1
  7.                 Schreiben.WriteByte(Lesen.ReadByte())
  8.         Next
  9.         ReadPointer=ReadPointer + CHUNK_SIZE
  10.         RingPointer=(RingPointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
  11. End Method
  12.  

For the case, the user did not send enough data for playing, the ringbuffer is filles with silence:
Code: BlitzMax
  1. Private Method SendOneChunk()
  2.         Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
  3.         Lesen.Seek ReadPointerMod
  4.         Schreiben.Seek RingPointer
  5.  
  6.         If ReadPointer + CHUNK_SIZE > WritePointer
  7.                 Local Maxi%=WritePointer-ReadPointer
  8.                                
  9.                 For Local i:Int = 0 To Maxi-1
  10.                         Schreiben.WriteByte Lesen.ReadByte()
  11.                 Next
  12.                 For Local i:Int = Maxi To CHUNK_SIZE-1
  13.                         Schreiben.WriteByte 0
  14.                 Next
  15.                 ReadPointer=ReadPointer + Maxi
  16.         Else
  17.                 ....
  18.  

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
  1. Global SendSize% , ReadPointer%
  2.  
  3. Graphics 800, 600
  4. RingBufferClass.SetDriver("FreeAudio DirectSound")
  5.  
  6. Const SEND_TIME:Int = 20 'msec
  7.  
  8. Global Source:TAudioSample=LoadAudioSample("Test.ogg")
  9. Global Lesen:TStream = CreateRamStream(Source.Samples,Source.Length*4,True,True)
  10.  
  11.  
  12.  
  13. Global RingBuffer:RingBufferClass=New RingBufferClass
  14. RingBuffer.CreateNew(Source.Format,Source.Hertz)
  15. SendSize = RingBuffer.HowManySamples()
  16.  
  17. Local WriteTime%=MilliSecs()
  18.  
  19.  
  20. Repeat
  21.         RingBuffer.Watch
  22.         Cls
  23.         SetColor 255,255,255
  24.         DrawText "click here to PAUSE:",200,370
  25.  
  26.         DrawRect 200,400,100,100
  27.         SetColor 1,1,1
  28.         DrawRect 201,401,98,98
  29.  
  30.  
  31.         If WriteTime<MilliSecs()
  32.                 ' this cares about really 20msec timing:
  33.                 WriteTime =WriteTime + SEND_TIME
  34.                 If MouseDown(1)=0
  35.                         SendAudio
  36.                 EndIf
  37.         EndIf
  38.         Flip 0
  39. Until AppTerminate()
  40. End
  41.  
  42.  
  43. Function SendAudio()
  44.         Local AudioArray:Short[SendSize]
  45.                 Lesen.Seek(ReadPointer)
  46.                 For Local i%=0 To SendSize-1
  47.                         Local V:Short=Lesen.ReadShort
  48.                         AudioArray[i]=V
  49.                 Next
  50.                 RingBuffer.Send AudioArray
  51.  
  52.                 ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)
  53. End Function
  54.  
  55. '_________________________________________________________________________________
  56.  
  57. Type RingBufferClass
  58.         Global MyDriver$
  59.  
  60.         Field CHANNELS:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, BUFFER_SIZE:Int
  61.         Field FORMAT:Int, CHUNK_TIME:Int, HERTZ:Int
  62.         Field WritePointer:Int, ReadPointer:Int, RingPointer:Int, WriteTime:Int
  63.  
  64.         Field Buffer:TAudioSample, Sound:TSound
  65.         Field Schreiben:TStream, WatchTime%, Lesen:TStream
  66.        
  67.         Field InBuffer:TAudioSample
  68.  
  69.  
  70.         Function SetDriver(Driver$)
  71.                 If MyDriver<>"" Return
  72.                 If Driver.contains("FreeAudio")=False
  73.                         Notify "wrong AudioDriver"
  74.                         End
  75.                 EndIf
  76.                 MyDriver = Driver
  77.                 SetAudioDriver(MyDriver)
  78.         End Function
  79.        
  80.        
  81.         Method CreateNew(Format%, Hertz%)
  82.                         If MyDriver=""
  83.                                         Notify "No AudioDriver selected"
  84.                                         End
  85.                         EndIf
  86.                         Self.HERTZ=Hertz
  87.                         Self.FORMAT=Format
  88.                         DefineBuffer
  89.                         RingPointer=(3*CHUNK_SIZE)
  90.                         WatchTime=MilliSecs()
  91.                         PlaySound Sound
  92.         End Method
  93.        
  94.        
  95.         Private Method DefineBuffer()
  96.                         CHUNK_TIME=20
  97.                         Local BITS:Int
  98.                         Select FORMAT
  99.                                 Case SF_MONO16LE
  100.                                         BITS:Int=16
  101.                                         CHANNELS:Int=1
  102.                                 Case SF_STEREO16LE
  103.                                         BITS:Int=16
  104.                                         CHANNELS:Int=2
  105.                                 Default
  106.                                         Notify "Audio format not supported"
  107.                                         End
  108.                         End Select      
  109.                         CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  110.                         BUFFER_SAMPLES = HERTZ * CHUNK_TIME            * 8 /1000
  111.                         BUFFER_SIZE    = BUFFER_SAMPLES * 2 *CHANNELS
  112.                         Buffer         = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  113.                         InBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  114.                         Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
  115.                         Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  116.                         Schreiben      = CreateRamStream(Buffer.Samples,Buffer_size,True,True)
  117.                         Lesen          = CreateRamStream(InBuffer.Samples,Buffer_size,True,True)
  118.         End Method
  119.        
  120.        
  121.         Private Method Watch()
  122.                 If WatchTime<MilliSecs()
  123.                         WatchTime = WatchTime + CHUNK_TIME
  124.                         SendOneChunk
  125.                 EndIf
  126.         End Method
  127.  
  128.  
  129.         Private Method SendOneChunk()
  130.                         Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
  131.                         Lesen.Seek ReadPointerMod
  132.                         Schreiben.Seek RingPointer
  133.  
  134.                         If ReadPointer + CHUNK_SIZE > WritePointer
  135.                                 Local Maxi%=WritePointer-ReadPointer
  136.                                
  137.                                 For Local i:Int = 0 To Maxi-1
  138.                                                 Schreiben.WriteByte Lesen.ReadByte()
  139.                                 Next
  140.                                 For Local i:Int = Maxi To CHUNK_SIZE-1
  141.                                         Schreiben.WriteByte 0
  142.                                 Next
  143.                                 ReadPointer=ReadPointer + Maxi
  144.                         Else
  145.                                 For Local i:Int = 0 To CHUNK_SIZE-1
  146.                                         Schreiben.WriteByte(Lesen.ReadByte())
  147.                                 Next
  148.                                 ReadPointer=ReadPointer + CHUNK_SIZE
  149.                         EndIf
  150.                         RingPointer=(RingPointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
  151.         End Method
  152.  
  153.  
  154.         Method Send(AudioArray:Short[])
  155.                 Local WritePointerMod% = WritePointer Mod BUFFER_SIZE
  156.                 Lesen.Seek WritePointerMod
  157.                
  158.                 If AudioArray.Length*2 + (WritePointerMod) <= BUFFER_SIZE
  159.                         For Local i%=0 To AudioArray.Length-1
  160.                                 Lesen.WriteShort AudioArray[i]
  161.                         Next
  162.                        
  163.                 Else
  164.                         Local Maxi% = BUFFER_SIZE-WritePointerMod
  165.                        
  166.                         For Local i%=0 To Maxi/2-1
  167.                                 Lesen.WriteShort AudioArray[i]
  168.                         Next
  169.                         Lesen.Seek 0
  170.                         For Local i%=Maxi/2 To AudioArray.Length-1
  171.                                 Lesen.WriteShort AudioArray[i]
  172.                         Next
  173.                 EndIf
  174.                 WritePointer=WritePointer + AudioArray.Length*2
  175.         End Method
  176.  
  177.  
  178.         Method HowManySamples%()
  179.                 Return CHUNK_SIZE/2
  180.         End Method
  181. End Type
  182.  
« Last Edit: March 27, 2021, 00:39:59 by Midimaster »
See my current project on PlayStore: 20Tracks-Audio-Player https://play.google.com/store/apps/details?id=midimaster.twentytrackd

 

SimplePortal 2.3.6 © 2008-2014, SimplePortal