SyntaxBomb - Indie Coders

General Category => Worklogs => Topic started by: Midimaster on March 22, 2021, 15:16:23

Title: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Midimaster on March 22, 2021, 15:16:23
Final Version:

The Worklog has a final solution which is on post#59. You can find it here:
https://www.syntaxbomb.com/index.php/topic,8377.msg347049187.html#msg347049187
The code and the examples work on BlitzMax NG and Blitz 1.50 too.



Why needed?

As many people here are looking for a more direct approach to play sounds in BlitzMax I will try to write something simple like Java has:

Other languages do it

Java uses a FIFO-Buffer. You fill in Sample-Values (SHORTs) on the top of this stack. And the system takes it away on the bottom. The whole will work asynchron. There is no need for a callback or for watching the state of the buffer.

If the buffer gets empty, the system will play SILENCE (like SHORTs with value=0  would do). If you fill a lot  into the buffer it will not overrun, but increase. So the only effect this will have is a longer latency time until the SHORTs reach the bottom.

What is it good for?

This gives you the opportunity to manipulate samples values until the very last moment. f.e. in synthesizer you would be able to adjust the sound color in realtime. for realtime you need to work with a very small buffer size. If you would put only 441 SHORTs on top this would mean that they only need 10msec to reach the bottom. For a user listening to it it would sound like realtime. On a stage 3 meter cause also 10msec! Everything below 40msec (13meter) can be seen as realtime.

You could mix together playback audio with incoming microphone signal. And because the times ellapsed since playback left the app is only 10msec and 10msec later the recording signal is already avaiable the recording signal appears 20msec after the playback. Fast enough for the listener to feel it synchron.

How to do that?
There is a SDK PortAudio which offers in the new version 19 exactly this approach. Years ago there was already a wrapper for portAudio V17 done by Simon Armstrong.

But first we want to see if this Ringbuffer can be done already with the "standard" Audio-SDK of BlitzMax: FreeAudio. In a future worklog I will investigate the PortAudio



Who wants to help?
Anybody who want to help is welcome. I know a lot about music, but never wrote a wrapper. Also I do not really write C. So feel free to join this project.

Step 1 will be to publish here the source codes and URLs with the inofrmations

Sources and informations:

Official Port-Audio Homepage:
http://files.portaudio.com/docs/v19-doxydocs/index.html


C Sample code for the new approach without callback:
http://files.portaudio.com/docs/v19-doxydocs/paex__write__sine_8c_source.html


Simon's PortAudio.mod:
https://github.com/nitrologic/axe.mod/tree/master/portaudio.mod


Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Scaremonger on March 22, 2021, 17:54:52
Hi,
After a recent post relating to ZXSpectrum sound emulation, I started researching exactly what you have suggested in your post and found that others had worked on it too:

https://mojolabs.nz/codearcs.php?code=3129
https://www.syntaxbomb.com/index.php?topic=5807.0

With BlitzmaxNG now including support for SDL, this is also a possibility:

https://wiki.libsdl.org/Tutorials/AudioStream

I'm spending all my spare time on my Competition game entry at the moment but will help when I can.

Si...
 



Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 22, 2021, 18:38:13
As written to you in private messages already - I am not sure if it requires PortAudio.

FreeAudio has buffers in which you can write. Talking about TSound-buffers which you play in TChannel. You just need to wrap your "fifo"-approach on it and it would not need much further adjustments.
I explained it to you (the OP) that it does not differ what kind of audio date you provide - it might be a streamed audio file (or web stream), or dynamically created sounds.

Working with audio.soloud is a bit different, as they do not expose the buffer (at least I did not get it to work - but raised an issue for it).



Why am I writing this?
If you used PortAudio you surely need to use it for all your audio aspects. You need to make it work with current BlitzMax NG versions if stuff gets updated etc. I would prefer to "avoid" this workload - if possible. So I'd prefer to piggyback on an existing audio solution.


bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Scaremonger on March 22, 2021, 21:32:40
@Derron: I saw your post (2014) on one of those threads I linked to GitHub.

https://github.com/GWRon/Dig/blob/3ea47f1e85680dc4403c490751fdd069a2d28047/base.sfx.soundstream.bmx

I've not had a chance to look at it yet, but would this still run under BlitzMaxNG?

Si...
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 22, 2021, 22:09:12
I've not had a chance to look at it yet, but would this still run under BlitzMaxNG?

https://github.com/TVTower/TVTower/blob/master/source/Dig/base.sfx.soundmanager.freeaudio.bmx
https://github.com/TVTower/TVTower/blob/master/source/Dig/base.sfx.soundmanager.freeaudio.c

should be the current version of it (did not put back all updates into "Dig" yet :)).
I already linked this to Midimaster in our private/direct communication. The "c" file is just there for "vanilla/legacy" to have a refill-buffer-thread at hand even when doing non-threaded builds (NG is threaded by default).

Placing streamed data in the buffer is nearly the same as placing "created" data in the buffer. Issues I had was that some stuff/information is not provided by "TSound" or "TChannel" (see how long a channel is playing or such stuff). Also important is: that FreeAudio has a parameter to pass to TSound so it stays "in memory" - you cannot pass this param in the TSound-creation, so you need to explicitely do some "FreeAudio" stuff.
Saying this as this needs a "switch" then to support brl.freeaudio and audio.soloud (or others).


bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: GW on March 23, 2021, 04:51:08
https://www.syntaxbomb.com/index.php/topic,5807.0.html (https://www.syntaxbomb.com/index.php/topic,5807.0.html)

If your audio buffer is too big, you get a delay. If it's too small you get stuttering with the sound driver starved for input. Every computer and program will be different.
Try it with Freeaudio/Raylib/OpenAl first and then make adjustments from there.
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 23, 2021, 09:07:28
How does the buffer define a delay???

this only is valid if you cannot alter the buffer. The buffer is like a ring - a so called "ring buffer". Excuse my simple drawing now.

(https://imgur.com/k966LM4.png)


Your buffer data ends where it starts. So once the last bit is read the first bit will follow. The ring is your data. The red area is the part currently send to your sound backend (DX, OSS, Alsa, PulseAudio ...) which sends it to your sound card / chip / ... / your playback device.
The arrow shows that this red ring moves around the data buffer. The gray area (rest of the ring) is where you can freely alter the content of the buffer.
Each time you send data out to the playback device you will move the "red ring" for the amount of send data.

I am not sure how you will now get a delay (assuming you send out data often enough).
Stuttering could then be avoided by filling the buffer with "silence" after your dynamic generated data.
So if your "play the following tone" occupies a quarter of the ring (buffer), fill the other 3 quarters with silence.

(https://imgur.com/JAWQAm9.png)
Blue is here our freshly "to add" data .. gray is what you need to fill with "silence" (not ignoring it).

So the only way you then will hear "stuttering" is:
- your "playback device feeder" (the one saying "DX please play this stuff here") is still handing out data from the ring buffer
- and your "data creator" is no longer working (so changing content of the buffer).


Or am I missing something important here?

bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 23, 2021, 10:35:35
Ok I interupted my study of usinf a FIFO-stack to now explore in the direction Derron suggested "FreeAudio-Ringbuffer". I tried to play around with the possibilities of Tsound and TAudioSample.

I created audio samples in TAudioSample and did load them into TSound. It was as I expected: LoadSound() produces a real copy of the TAudioSample-RAM for playing it. So an afterwards manipulation of the TAudioSample-RAM did not show any effects. And I see no chance of accessing this TSound-RAM.


As I never used the FreeAudio-functions directly, I do not know how to test whether the FreeAudio has more capibility. And the code Derron offers is to big for a learning.

Can somebody teach me in a dozend lines how to get writing access to a currently playing Sound in FreeAudio. I only need to find out the buffers adress. (I know how to code ringbuffers and timing behavior if I already have the access.)

Some Questions: How to get  FreeAudioDriver-Object? This is wrong:
Code: [Select]
Local Driver%=SetAudioDriver("FreeAudio" )
Local MySample:TAudioSample=CreateAudioSample(44100,44100,SF_MONO16LE)
Local NewSound:TFreeAudioSound = Driver.CreateSound( MySample,1)

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: iWasAdam on March 23, 2021, 11:23:32
Free Audio doesn't have any of the realtime functions - you will need to go into the c++ code and rewrite/augment the entire subsystem.
This will include TSound, TChannel, TAudio, TAudioSample etc

But...

There are two main issues you will have:
1. complexity - the more functions you add the more complex the audio subsystem will get and the more users won't understand how it all works - They can't use the current system and it's simple.
2. abstraction - to make it usable for end user you HAVE to write a proper editor. just having a load of function calls is NOT enough. You need to wrap it all up into something visual that will spit out the data the new audio system requires. And even then, about 70% of users wont understand a thing!
2a. structure - you will need to fully understand and create a base set of rules for any sound creation. vca, lfo, adsr, loops, and that is without adding bus fx.

4. The last thing will be a sequencer - because that is what people really want.

it's all perfectly doable - but the general timescale would be 6 months to a year for writng/testing and deving the initial editors, etc.

Now I'm going to include a few concept thoughts for you (These are real and not just photoshop - so you get an idea of the undertaking)
this is a pure realtime approach
(https://vjointeractive.files.wordpress.com/2021/03/m19miv.png)
(https://vjointeractive.files.wordpress.com/2021/03/elik8u-1.png)
for something simpler - just creating waveforms, etc
(https://vjointeractive.files.wordpress.com/2021/03/gptuks-1.png)
(https://vjointeractive.files.wordpress.com/2021/03/uljui.png)

You will also need to think about data construction.
IMHO the best way is to have the following:
1. a sample stack - once loaded this data never changes (unless you are creating new samples internally)
2. a control system - this takes a sample as input and manipulates a new result using a standard set of instructions. This is where all the magic happens
3. a channel system - this calls a single control and a sample and coordinates things
4. the core - this handles the channels and any routing/fx if this is is programmed

currently NG has a basic sample stack, and basic channel architecture


oh, and one last thing...
sample format. The best approach is to fix this as 16bit stereo and/or mono
convert all incoming to this format
you will have to duplicate the internal code (one for mono and one for stereo) and handle each. unless you deal with just stereo - then you will have to work out how to pan in stereo - it's not as simple as it first looks.
But this is much simpler that having to do it 4 times with 8bit and 16bit!!
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 23, 2021, 12:16:20
I created audio samples in TAudioSample and did load them into TSound. It was as I expected: LoadSound() produces a real copy of the TAudioSample-RAM for playing it. So an afterwards manipulation of the TAudioSample-RAM did not show any effects. And I see no chance of accessing this TSound-RAM.


Code: BlitzMax
  1. Self.buffer = New TBank
  2. Self.buffer.Resize( bufferLength * 4 ) 'SizeOf( Int(0) ) )
  3. Local audioSample:TAudioSample = CreateStaticAudioSample(buffer.Lock(), GetBufferLength(), freq, format)
  4.  
  5. 'driver specific sound creation
  6. CreateSound(audioSample)
  7. ....
  8.  
  9.  
  10. '=== CONTAINING FREE AUDIO SPECIFIC CODE ===
  11. ?bmxng
  12.         Method CreateSound:Byte Ptr(audioSample:TAudioSample)
  13. ?Not bmxng
  14.         Method CreateSound:Int(audioSample:TAudioSample)
  15. ?
  16.                 'not possible as "Loadsound"-flags are not given to
  17.                 'fa_CreateSound, but $80000000 is needed for dynamic sounds
  18.                 Rem
  19.                         $80000000 = "dynamic" -> dynamic sounds stay in app memory
  20.                         sound = LoadSound(audioSample, $80000000)
  21.                 endrem
  22.  
  23.                 'LOAD FREE AUDIO SOUND
  24. '?bmxng
  25.                 Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
  26. '?Not bmxng
  27. '               Local fa_sound:Int = int(fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 ))
  28. '?
  29.                 '"audioSample" is ignored in the module, so could be skipped
  30.                 'sound = TFreeAudioSound.CreateWithSound( fa_sound, audioSample)
  31.                 sound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  32.         End Method
  33.  

The important part is the creation of a TAudioSample with already allocated memory. "$80000000" is needed to make the "sound" to stay "in memory". You cannot pass it to "TSound"  generically (so for eg Soloud). You need to work with the "fa_" stuff (freeaudio).


so cut down:
Code: BlitzMax
  1. Local format:Int = SF_STEREO16LE
  2. Local bits:Int = 16
  3. Local freq:Int = 44100
  4. Local channels:Int = 2
  5.  
  6. Local bufferLength:Int = 1024
  7. Local audioBufferBank:TBank = New TBank
  8. audioBufferBank.Resize( bufferLength * 4 ) 'SizeOf( Int(0) ) )
  9.  
  10. Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
  11. Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
  12. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  13.  

That way you should be able to access the ringbuffer in audioBufferBank via helper methods or as byte-block with audioBufferBank.Lock().



@ IWasAdam
Your knowledge is mindbreaking  - as always. My code is just allowing to play back custom / generated data. Yours is more of a generator itself already. This is a can of worms I would hopefully not have to open :)



Edit: Once you get the ringbuffer "manipulateable" ... you would just put your "FIFO"-stack on top of it. Adding data, and the buffer-refiller takes data from it as much as possible (fitting into the ring buffer). If the stack is empty, it fills with "silence" (but does not alter "last valid added data" position - so you could add again to the stack and it starts filling - if desired).

bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 23, 2021, 12:23:58
@iWasAdam

I was also convinced, that FreeAudio will not help me in my quest, but Derron told me, that there is a usable approach to do this with FreeAudio. So I still would prefer an audio-SDK which offers a  FIFO-buffer. It is easy to use. I can push various amount of SHORTs into it. And there is no need to keep a exact timing for this pushing.



For me a ringbuffer for audio has the charme of a racing course, where a sportscar turn his rounds, while I was forced to repaint the center lines on the street.

I not try to write any sythesizer software, but i gave this sample that other user understand, that I do not handle with existing sounds.


I also do not want to write a new mod, which other users can use. I'm only interested in one single function. Sending 16bit-unsigned-48kHz sample values to an audio device. The sample values need to react to OpenAl incoming stream and need to be played within 20-40msec after the signal came in.

So may I ask how you managed this approach? It looks you also work in music software. Thats also my business. Did you already have a look on my 20track-player for practicing for orchester rehearsals?
https://20tracks.org/ or Google Playstore: https://play.google.com/store/apps/details?id=midimaster.twentytrackd

Here I push every 20msec 441 SHORTs on a Java-based audio stack, while the audio device fetches 441 SHORTs in the same time. In the middle of the buffer are another 441 SHORTS not to risk crackles.

I heared about the new PortAudio V19, which now offers a FIFO approach next tot the previous CALLBACK 
But for BlitzMax only exist a Wrapper for old V18.1.
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 23, 2021, 12:31:16
So your application does connect to OpenAL already. Did not check yet if you can use FreeAudio "coexisting" but it should - if you only use FreeAudio for "output" and OpenAL for "input" (the brl.audio system does not handle "input" - so it should be doable).


Using the code I posted above should be easy to "try out" ... and if it is not feasible you can always go back to wrap a complete library - just thought I could help you avoiding this big pile of workload.


bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 23, 2021, 12:49:59
...so cut down:

Derron, you speak in riddles to me...

When I do this little code:
Code: BlitzMax
  1. Graphics 800,600
  2. Local AudioSample:TAudioSample = CreateAudioSample(44100, 44100, SF_MONO16LE)
  3. For Local i%=0 To 44100-1
  4.         AudioSample.samples[i]=Sin(i)*1000.0
  5. Next
  6. Local sound:TSound =LoadSound(AudioSample)
  7. PlaySound Sound
  8. Repeat
  9.         Flip 1
  10. Until AppTerminate()
...it works as expected.

Now to your code:
Code: BlitzMax
  1. Graphics 800,600
  2. Local audioSample:TAudioSample = CreateAudioSample(44100, 441000, SF_MONO16LE)
  3. For Local i%=0 To 44100-1
  4.         AudioSample.samples[i]=Sin(i)*1000.0
  5. Next
  6.  
  7. Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, 16, 1, 44100, audioSample.samples, $80000000 )
  8. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  9.  
  10. PlaySound Sound
  11. Repeat
  12.  
  13. Until AppTerminate()
  14.  
...does not play anything. What did I forget?

now to additional use the TBanks makes the things more complicate, but not working, or?



Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: iWasAdam on March 23, 2021, 12:53:49
@ MidiMaster
20 tracks sounds a lot like a STEM mixer - which is great :)

I went directly into the BlitzMax mods and began working out how it all worked and how modify it all. Internally FreeAudio uses a ring buffer (I think that most audio systems do at their base), but the has a sort of queueing voice architecture that both freeaudio and audio and channel and sample all link into - so you can't modify one without the results being passed along. All the base code is in c++, so you will need to think in c++ then write wrappers around them and interface with the Mod structure to get it all working. Hence there is no version of NG as I didn't want to go through all the trouble of rewriting all the stuff.

So it would be better to have (as you rightly said) something that was above all that and just went for the hardware.

Simple ring buffer is the way to go, you can directly output 32 voices with no issues beyond that you will need to have very tight code. Trick here is to only use half your volume in any sample. that way you can simply mix them together and the results wont 'clip' ;)

It's why I suggest having your voice architecture different from the sample bank and the control bank.
That way you could pick a 'free' voice load it with a control and sample and you have a single voice outputted.

If you go down the road of realtime manipulation then I would suggest only using shorts as the output, but float as the internal storage with 0 being the mid point. This will give you much better fidelity, but also allow you to scale/volume and mix just by doing
sample*0 = no volume
sample*1 = full volume
sample = sample A + sample B, etc

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: iWasAdam on March 23, 2021, 12:54:54
The best way would be to create your own (simple) format that you can test and display and start from there ;)

Don't expect anyone to write your editor for you - they wont - hehehe
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 23, 2021, 13:23:48
The way you descripe is exactly what i'm already doing in the 20Tracks. But this is Java and there the  use of audio is already as we need it.

Now I'm searching for a approach to do it on BlitzMax. I don not want that somebody writes the code for me. I only need a minmal code to understand how to access the FreeAudio directlty. Derron wrote me, that it is possible but without a working example.

By the way: reducing the volume of the channels to the half, will only work upto 10 instruments. A half level is -10dB and 10 instruments with level of -10dB will reach 0dB again.
So my 20Tracks uses a "forecast-Normalize-Algo" to reduce the single tracks to the best value.


Back to PortAudio


The versio 19 has this feature:
Quote
....In addition to this "Callback" architecture, V19 also supports a "Blocking I/O" model which uses read and write calls which may be more familiar to non-audio programmers. Note that at this time, not all APIs support this functionality....


see the double NULL in the OpenStream-Call:
Code: [Select]
Pa_OpenStream(
           &stream,
           NULL,
           &outputParameters,
           SAMPLE_RATE,
           FRAMES_PER_BUFFER,
           paClipOff,
           NULL,      /* no callback, use blocking API */
           NULL ;    /* no callback, so no callback userData */
....
Pa_WriteStream( stream, buffer, FRAMES_PER_BUFFER ));

And I also found soething similar in the old PortAudio 18.1, where we have a wrapper. There is a folder PABLIO and inside is this pablio.h:
Quote
#ifndef _PABLIO_H
#define _PABLIO_H

#ifdef __cplusplus
extern "C"
{
#endif /* __cplusplus */

/*
 * $Id: pablio.h,v 1.1.1.1 2002/01/22 00:52:53 phil Exp $
 * PABLIO.h
 * Portable Audio Blocking read/write utility.
 *
 * Author: Phil Burk, http://www.softsynth.com/portaudio/
 *
 * Include file for PABLIO, the Portable Audio Blocking I/O Library.
 * PABLIO is built on top of PortAudio, the Portable Audio Library.
 * For more information see: http://www.audiomulch.com/portaudio/
 * Copyright (c) 1999-2000 Ross Bencina and Phil Burk
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "portaudio.h"
#include "ringbuffer.h"
#include <string.h>

typedef struct
{
    RingBuffer   inFIFO;
    RingBuffer   outFIFO;
    PortAudioStream *stream;
    int          bytesPerFrame;
    int          samplesPerFrame;
}
PABLIO_Stream;

/* Values for flags for OpenAudioStream(). */
#define PABLIO_READ     (1<<0)
#define PABLIO_WRITE    (1<<1)
#define PABLIO_READ_WRITE    (PABLIO_READ|PABLIO_WRITE)
#define PABLIO_MONO     (1<<2)
#define PABLIO_STEREO   (1<<3)

/************************************************************
 * Write data to ring buffer.
 * Will not return until all the data has been written.
 */
long WriteAudioStream( PABLIO_Stream *aStream, void *data, long numFrames );

/************************************************************
 * Read data from ring buffer.
 * Will not return until all the data has been read.
 */
long ReadAudioStream( PABLIO_Stream *aStream, void *data, long numFrames );

/************************************************************
 * Return the number of frames that could be written to the stream without
 * having to wait.
 */
long GetAudioStreamWriteable( PABLIO_Stream *aStream );

/************************************************************
 * Return the number of frames that are available to be read from the
 * stream without having to wait.
 */
long GetAudioStreamReadable( PABLIO_Stream *aStream );

/************************************************************
 * Opens a PortAudio stream with default characteristics.
 * Allocates PABLIO_Stream structure.
 *
 * flags parameter can be an ORed combination of:
 *    PABLIO_READ, PABLIO_WRITE, or PABLIO_READ_WRITE,
 *    and either PABLIO_MONO or PABLIO_STEREO
 */
PaError OpenAudioStream( PABLIO_Stream **aStreamPtr, double sampleRate,
                         PaSampleFormat format, long flags );

PaError CloseAudioStream( PABLIO_Stream *aStream );

#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* _PABLIO_H */





Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 23, 2021, 14:44:06
The TBank is just a convenient helper to have "managed memory blocks". So an easy way to copy stuff, to write and read - and if no longer used, freeing it.


Now to your code:
Code: BlitzMax
  1. Graphics 800,600
  2. Local audioSample:TAudioSample = CreateAudioSample(44100, 441000, SF_MONO16LE)
  3. For Local i%=0 To 44100-1
  4.         AudioSample.samples[i]=Sin(i)*1000.0
  5. Next
  6.  
...does not play anything. What did I forget?


This is not my code. And I even added explanatory words -- for why I need to pass certain parameters ...
Instead of CreateAudioSample() I use CreateStaticAudioSample() .


I took your code and added some stuff here and there:
Code: BlitzMax
  1. SuperStrict
  2. Framework Brl.StandardIO
  3. Import Brl.Audio
  4. Import Brl.FreeAudioAudio
  5.  
  6. Import Brl.GLMax2D
  7. Import Brl.Bank
  8. Import random.xoshiro
  9.  
  10. 'open window
  11. Graphics 800, 600
  12. SetAudioDriver("FreeAudio")
  13.  
  14.  
  15. 'mono sound
  16. rem
  17. Local AudioSample:TAudioSample = CreateAudioSample(44100, 44100, SF_MONO16LE)
  18. For Local i%=0 To 44100-1
  19.         AudioSample.samples[i] = Sin(i)*1000.0
  20. Next
  21. Local sound:TSound = LoadSound(AudioSample)
  22. PlaySound Sound
  23. endrem
  24.  
  25.  
  26. Local format:Int = SF_MONO16LE 'SF_STEREO16LE
  27. Local bits:Int = 16
  28. Local freq:Int = 44100
  29. Local channels:Int = 2
  30. 'Local bufferLength:Int = 1024
  31. Local bufferLength:Int = 44100
  32. Local audioBufferBank:TBank = New TBank
  33. audioBufferBank.Resize( bufferLength )
  34.  
  35. Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
  36. Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
  37. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  38.  
  39. 'change sound after creation
  40. 'For Local i:int = 0 Until audioBufferBank.capacity()
  41. '       AudioSample.samples[i] = Sin(i)*1000.0
  42. 'Next
  43.  
  44. 'or do it by manipulating the memory, not the "samples"
  45. For Local i:int = 0 Until audioBufferBank.capacity()
  46.         audioBufferBank.PokeByte(size_t(i), int(Sin(i)*1000.0))
  47. Next
  48.  
  49.  
  50.  
  51. PlaySound sound
  52.  
  53. Repeat
  54.         Cls
  55.  
  56.         Flip
  57. Until KeyHit(KEY_ESCAPE) Or AppTerminate()
  58.  

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 23, 2021, 14:54:43
This code should play your whatever-it-is (am no sound expert - not even an beginner). Press "space" and it should play a different tone.
All of them "repeated".

Use a channel (returned by PlaySound) to control volume etc - or to stop / pause playing.

Code: BlitzMax
  1. SuperStrict
  2. Framework Brl.StandardIO
  3. Import Brl.Audio
  4. Import Brl.FreeAudioAudio
  5.  
  6. Import Brl.GLMax2D
  7. Import Brl.Bank
  8. Import random.xoshiro 'I prefer that over brl.RandomDefault
  9.  
  10. 'open window
  11. Graphics 800, 600
  12. SetAudioDriver("FreeAudio")
  13.  
  14.  
  15.  
  16. Local format:Int = SF_MONO16LE 'SF_STEREO16LE
  17. Local bits:Int = 16
  18. Local freq:Int = 44100
  19. Local channels:Int = 2
  20. 'Local bufferLength:Int = 1024
  21. Local bufferLength:Int = 44100
  22. Local audioBufferBank:TBank = New TBank
  23. audioBufferBank.Resize( bufferLength )
  24.  
  25. Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
  26. Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
  27. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  28.  
  29. FillSoundBuffer(audioBufferBank)
  30.  
  31.  
  32. PlaySound sound
  33.  
  34. Repeat
  35.         Cls
  36.         DrawText("SPACE for random sound data", 0,0)
  37.         If KeyHit(KEY_SPACE) Then FillSoundBuffer(audioBufferBank)
  38.         Flip
  39. Until KeyHit(KEY_ESCAPE) Or AppTerminate()
  40.  
  41.  
  42.  
  43. Function FillSoundBuffer(bank:TBank)
  44.         Local tone:int = Rand(0,1000)
  45.         'or do it by manipulating the memory, not the "samples"
  46.         For Local i:int = 0 Until bank.capacity()
  47.                 bank.PokeByte(size_t(i), int(Sin(i)* tone))
  48.         Next
  49. End Function
  50.  
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 23, 2021, 15:30:06
As said I have no clue what samples etc really mean ... so I do not know what exactly which parameter changes

with
Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
I get "gaps" in the playback - so sound .. silence ... sound ... silence.
I assume "audioSample.length" is incorrect here.

All this stuff requires a calculation based on bits, freq etc - so how many "samples" (??) fit into it.

For me this worked gapless (I divided the "audio sample length" by a number found by "testing")
Code: BlitzMax
  1. Local format:Int = SF_STEREO16LE
  2. Local bits:Int = 16
  3. Local freq:Int = 44100
  4. Local channels:Int = 2
  5. Local bufferLength:Int = 1024 * 4 '4kb buffer
  6. Local audioBufferBank:TBank = New TBank
  7. audioBufferBank.Resize( bufferLength )
  8.  
  9. Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
  10. Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length / 32, bits, channels, freq, audioSample.samples, $80000000 )
  11. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  12.  
But ... it also altered the sound (pitched higher) ... so this is not the solution. Yet you now have a memory block which you can alter - and is used for playback, it just needs the right "parameters/sizes".


I am sure Midimaster and IWasAdam are now secretly laughing at me (come on be honest! this feeling of "I know it... young boy... I know it!"). So excuse me :)


bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 23, 2021, 16:10:19
You seem to be able to skip "TAudioSample" ... yet I do not know how to get rid of the "gap".

Code: BlitzMax
  1. Local format:Int = SF_STEREO16LE
  2. Local bits:Int = 16
  3. Local freq:Int = 44100
  4. Local channels:Int = 2
  5. Local sampleCount:Int = 1000
  6. Local sampleSize:Int = 8
  7. Local sampleData:TBank = CreateBank(sampleCount * sampleSize)
  8.  
  9. Local fa_sound:Byte Ptr = fa_CreateSound( int(sampleData.size()), bits, channels, freq, sampleData.Lock(), $80000000 )
  10. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null )
  11.  
  12. FillSoundBuffer(sampleData)
  13.  


And you might even be able to skip the "TSound" too
Code: BlitzMax
  1. Local format:Int = SF_STEREO16LE
  2. Local bits:Int = 16
  3. Local freq:Int = 44100
  4. Local channels:Int = 2
  5. Local sampleCount:Int = 1000
  6. Local sampleSize:Int = 8     'think that equals  BytesPerSample[format] * channels
  7. Local sampleData:TBank = CreateBank(sampleCount * sampleSize)
  8.  
  9. Local fa_sound:Byte Ptr = fa_CreateSound( int(sampleData.size()), bits, channels, freq, sampleData.Lock(), $80000000 )
  10. 'start playing
  11. Local fa_channel:Int = fa_PlaySound( fa_sound,  0, 0)
  12. 'in case you need the TChannel (for volume adjustment etc)
  13. 'Local channel:TChannel = TFreeAudioChannel.CreateWithChannel( fa_channel )
  14.  
  15. FillSoundBuffer(sampleData)
  16.  

Edit - maybe "length" is "size of block divided by bytersPerSample" ?
Code: BlitzMax
  1. Local fa_sound:Byte Ptr = fa_CreateSound( int(sampleData.size() / BytesPerSample[format]), bits, channels, freq, sampleData.Lock(), $80000000 )
  2.  

Yet I still have a little "nothing" between the repetitions - at least it sounds so.
in my streamed audio thingy I did not have these hearable "error" - so maybe the way the buffer is filled is not how it is supposed to be done.


bye
Ron
Title: Re:doing it with FreeAudio
Post by: Midimaster on March 24, 2021, 02:08:13
I took your  code and added some stuff here and there:.....
Code: BlitzMax
  1. SuperStrict
  2. Framework Brl.StandardIO
  3. Import Brl.Audio
  4. Import Brl.FreeAudioAudio
  5.  
  6. Import Brl.GLMax2D
  7. Import Brl.Bank
  8.  
  9.  
  10. 'open window
  11. Graphics 800, 600
  12. SetAudioDriver("FreeAudio")
  13. .....
  14.  

Ok I try to understand your code. It works.

There is a little bug in the code. You fill the whole buffer, but the Sound plays only half the time. This is because 2 channels with a 16bit-sound only needs 4Bytes.

Does this part only manipulating BYTEs instead of SHORTs?
Code: [Select]
    'or do it by manipulating the memory, not the "samples"
    For Local i:Int = 0 Until audioBufferBank.capacity()
            audioBufferBank.PokeByte(Size_T(i), Int(Sin(i)*1000.0))
    Next

Another question: What is this Size_T()-casting good for? Doesn't PokeByte expect an integer here? Or is it only because of preventing negativ values?


As a first result of the night I was able to code a 300msec Ringbuffer, which plays a song by copying 882 samples every 20msec. It is a executable example (...you need the "test.ogg" from attachment)
Code: BlitzMax
  1. SuperStrict
  2. Graphics 800, 600
  3. SetAudioDriver("FreeAudio")
  4.  
  5.  
  6. Global Vorlage:TAudioSample=LoadAudioSample("test.ogg")
  7.  
  8. Local audioSample:TAudioSample = CreateAudioSample(8820, 22050, SF_MONO16LE)
  9.  
  10. Local fa_sound:Byte Ptr = fa_CreateSound( 8820, 16, 1, 22050, audioSample.samples, $80000000 )
  11. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  12.  
  13.  
  14. Global Zeit%=MilliSecs()
  15. PlaySound sound
  16. Zeit=MilliSecs()+50
  17. Local Zeiger%=0
  18. Global lfd%=0
  19. Repeat
  20.         Cls
  21.         If Zeit<MilliSecs()
  22.                 zeit=zeit+20
  23.                 Print Zeiger
  24.                 For Local i:Int = 0 Until 882
  25.                         AudioSample.samples[zeiger+i] = Vorlage.Samples[lfd]
  26.                         lfd=lfd+1
  27.                 Next
  28.                 Zeiger=(Zeiger +882) Mod 17640
  29.         EndIf
  30.         Flip 0
  31. Until AppTerminate()

The only thing I do not understand is: Why do I need to raise the variable ZEIGER% to 17640, when the AudioSamples has only 8820 samples. Does this mean, that AudioSample.Samples[0] and AudioSample.Samples[1] show two Bytes of the same SHORT value? I would have expected that AudioSample.Samples[0] shows the first sample value and AudioSample.Samples[1] already the second, and so on.
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 24, 2021, 07:13:04
How all this stereo, mono ... stuff works: I do not really know how this is interchained or not.


size_t ... can be removed, left over from some other tries (Streams in NG use size_t not integers).

Quote
Does this part only manipulating BYTEs instead of SHORTs?

there is also "PokeShort" and "PokeInt". for now this Code just places "the value" into each byte of the memory. At least I understood it that way.
You could even make "sampleData" an "sampleData:int[]" - should work too (then passing "sampleData" as param instead of "sampleData.lock()").



So I would interested in a code which works for SF_STEREO16LE and SF_MONO16LE (and other formats) ... without "gaps".


bye
Ron
Title: Reducing latency to 40msec
Post by: Midimaster on March 24, 2021, 12:12:56
Here is a more abstract version of the SF_MONO16 Ringbuffer. It will be the base for testing also SF_STEREO16 next.

I could reduce the latency to 40msec and sharpen the code to a minimum:
 It is a executable example (...you need the "test.ogg" from post#20 attachment)
Code: BlitzMax
  1. SuperStrict
  2. Graphics 800, 600
  3. SetAudioDriver("FreeAudio")
  4. Global WritePointer:Int, ReadPointer:Int, WriteTime:Int
  5. Global BITS:Int, CHANNELS:Int, BUFFER_SIZE:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int
  6.  
  7. Const HERTZ:Int=22050
  8.  
  9. Const CHUNK_TIME:Int = 20 'msec
  10. Const FORMAT:Int     = SF_MONO16LE
  11.  
  12. Select FORMAT
  13.         Case SF_MONO16LE
  14.                 BITS:Int=16
  15.                 CHANNELS:Int=1
  16.                 CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  17.                 BUFFER_SAMPLES = CHUNK_SIZE*2
  18.                 BUFFER_SIZE    = BUFFER_SAMPLES * CHANNELS * BITS/8
  19.         Default
  20.                 End
  21. End Select
  22.  
  23. ' derron's part:
  24. Global Source:TAudioSample=LoadAudioSample("test.ogg")
  25. Global Buffer:TAudioSample = CreateAudioSample(BUFFER_SIZE/2, HERTZ, FORMAT)
  26. Local fa_sound:Byte Ptr = fa_CreateSound( BUFFER_SIZE/2, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
  27. Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  28.  
  29. ' now put 40msec Latency into the Buffer:
  30. SendOneChunk()
  31. SendOneChunk()
  32.  
  33. 'already start playback with an empty buffer
  34. PlaySound sound
  35. WriteTime=MilliSecs()
  36.  
  37. Repeat
  38.         Cls
  39.         If WriteTime<MilliSecs()
  40.                 ' this cares about really 20msec timing:
  41.                 WriteTime=WriteTime + CHUNK_TIME
  42.                 Print "Write to " + WritePointer + " at time: " + WriteTime
  43.                 SendOneChunk
  44.         EndIf
  45.         Flip 0
  46. Until AppTerminate()
  47. End
  48.  
  49. Function SendOneChunk()
  50.                 ' put a amount of sampls into the buffer:
  51.                 For Local i:Int = 0 Until CHUNK_SIZE
  52.                                 Buffer.samples[WritePointer+i] = Source.Samples[ReadPointer]
  53.                                 ReadPointer=ReadPointer+1
  54.                 Next
  55.                 WritePointer=(WritePointer + CHUNK_SIZE) Mod BUFFER_SIZE
  56. End Function
  57.      
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 24, 2021, 15:30:05
why are you only able to reduce latency to 40ms?

Can you explain that a bit ?

the whole "buffer refill loop":
Code: [Select]
        If WriteTime<MilliSecs()
                ' this cares about really 20msec timing:
                WriteTime=WriteTime + CHUNK_TIME
                Print "Write to " + WritePointer + " at time: " + WriteTime
                SendOneChunk
        EndIf
can be run in thread ...   which allows to do other stuff in the main one.

But we will tackle that once you got the stereo part working.


I do not how much this here will help:
Code: [Select]
local channel:TChannel = PlaySound(sound)

...
"playing channel position: " + TFreeAudioChannel(channel).Position()
Maybe this can be used to display what "x" is currently played.


bye
Ron
Title: Latency below 40msec
Post by: Midimaster on March 24, 2021, 16:03:02
Why 40msec?
1.
40msec is for me a first step towards better values. I always develop apps in steps from "safe" to "fast". Each step is tested and when there are no problem the next step will be explored. So 40msec is a beginning.
2.
40msec is already nearly "real-time". As sound moves 343 meter/sec also in the real world you reach 40msec if somebody plays in a distance of 14meter. You are not able to feel a dis-synchronity if you see a singer performing 14 meters ahead of you.
3.
As closer you try to reach the 0-value you get more and more problems with dropouts etc.

At the moment I use a minimum of 3 Chunks:
- One (the first), where the SoundDevise is reading
- One (the second) as a buffer between the first and the third
- One (the third), where my App is writing


STEREO

Stereo is not that complicate. You only have to shovel the douple amount of bytes in the SendOneChunk()-function. Thats all.


At the moment the SendOneChunk()-function needs <1msec to complete. But the data are copied as single Bytes. This is not usefull in practice, because you only can manipulate the music when you have it as SHORTs. The Bytes in the RAM represent alternating the high and the low byte of these SHORTS. To combine them you have to care about LittleEndian or BigEndian.

In my next step I will try to find a fast SHORTs access to it. Maybe it's your BANK-approach, maybe I will try to do it with RAM-Streams. We will see.
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 24, 2021, 16:20:37
Thanks for the elaborative explanation.


bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Scaremonger on March 24, 2021, 19:45:37
So will the ringbuffer continue to play silence when the buffer is empty or will it simply pause playback until the buffer is refilled?
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron on March 24, 2021, 21:27:03
This depends on you I think.
You could always use the channel (returned by PlaySound) to stop playback - and if you do you might better also stop refilling the buffers.

Yet I assume Midimaster just wants a way to "live output" something - so he won't stop (pun intended).


bye
Ron
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 25, 2021, 01:13:45
Silence at the end?
this is the dis-advantage of ring buffers. they will play the last datas of the buffer over and over again. So you have to fill the buffer in a last round with 0 to prevent this.

Now 16bit Stereo and Manipulation
Here is the next step. Now you can stream SF_STEREO16 and you can manipulate the samples by using a RAMStream. This is an executable example. use the TestC.ogg from attachment. (the poor quality you can hear is in the ogg file, not because of the algo). you can move the mouse to pan between LEFT and RIGHT.

What are the possible Chunk-times?

Any chunk time must be a possible divider of the 1000msec. f.e. 20 or 25 or 40, but not 30!
But the result must again be a possible divider of the samples frequency. 

In our example the chunk-time for refreshing must stay at 20msec, because only this value is without crackling. This is caused by the fact that the prime number in 22050Hz is 441 and this a 1/50 of 22050Hz. ---> 1/50 from 100msec= 20msec.
Also a possible chunk-times is 40msec = 882samples

This means with 44100Hz you could also use 10msec, 20msec and 40msec

This means with 48000Hz or 24000Hz you are more flexible: 1, 2, 4, 5, 8, 10, 20, 25 and 40msec

Here is the next version:
Code: BlitzMax
  1. SuperStrict
  2.  
  3. Graphics 800, 600
  4. SetAudioDriver("FreeAudio")
  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=22050
  9.  
  10. Const CHUNK_TIME:Int = 20 'msec  (=441 samples)
  11.  
  12. Global Source:TAudioSample=LoadAudioSample("testc.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*8
  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 *8
  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.  
  50. ' now put 40msec Latency into the Buffer:
  51. SendOneChunk()
  52. SendOneChunk()
  53.  
  54. 'already start playback with an empty buffer
  55. PlaySound sound
  56. WriteTime=MilliSecs()
  57. Local StartTime%=MilliSecs()
  58. Local z1$,z2$,z3$
  59. Repeat
  60.         Cls
  61.         DrawText "Move the mouse to pan between left and right",100,100
  62.         DrawText "LEFT",10,300
  63.         DrawText "MIDDLE",300,300
  64.         DrawText "RIGHT",700,300
  65.         DrawTabText "Write to:", WritePointer  , 300,400
  66.         DrawTabText "at time:", (WriteTime-StartTime) , 300,430
  67.         DrawTabText "read from:", ReadPointer , 300,460
  68.        
  69.         If WriteTime<MilliSecs()
  70.                 ' this cares about really 20msec timing:
  71.                 WriteTime=WriteTime + CHUNK_TIME
  72.                 'Print "Write to " + WritePointer + " at time: " + (WriteTime-StartTime) +" " + Buffer.Length + " " + buffer_size
  73.                 SendOneChunk
  74.         EndIf
  75.         volume=1-MouseX()/800.0
  76.         Flip 0
  77. Until AppTerminate()
  78. End
  79.  
  80.  
  81. Function DrawTabText(t1$,t2$,X%,y%)
  82.         DrawText t1, X-TextWidth(T1)-70,Y
  83.         DrawText t2, X-TextWidth(T2),Y
  84. End Function
  85.  
  86. Function SendOneChunk()
  87.                 ' put a amount of samples into the buffer:
  88.                 Local Zeit%=MilliSecs()
  89.                                 Lesen.Seek(ReadPointer)
  90.                                 Schreiben.Seek(WritePointer)
  91.                 For Local i:Int = 0 Until CHUNK_SIZE Step 2
  92.                                 Local value:Int=Lesen.ReadShort()
  93.                                 If value>32768
  94.                                         value=value-65535
  95.                                 EndIf
  96.                                
  97.                                 ' mouse volume:
  98.                                 If i Mod 4=0 Or FORMAT=SF_MONO16LE
  99.                                         value=value*volume
  100.                                 Else
  101.                                         value=value*(1-volume)                         
  102.                                 EndIf
  103.                                 If value<0
  104.                                         value=value+65535
  105.                                 EndIf
  106.                                
  107.                                 Schreiben.WriteShort(value)
  108.                                 ReadPointer=ReadPointer+2
  109.                 Next
  110.                 WritePointer=(WritePointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
  111.                 ReadPointer=ReadPointer Mod (Source.length * CHANNELS * BITS/8)
  112. End Function
  113.      

Title: Testing latency of different Audio Drivers
Post by: 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.

Code: BlitzMax
  1. Graphics 800,600
  2. Local Drivername$[9]
  3. Local Nr%
  4. For Local a:String = EachIn AudioDrivers()
  5.         Print "Use Nr=" + Nr + " to use  Driver: " + a
  6.         Drivername[Nr]=a
  7.         Nr=Nr+1
  8. Next
  9.  
  10. '*** Select driver here:***********
  11.  
  12. Nr=1
  13.  
  14. '*********************************
  15. SetAudioDriver DriverName[Nr]
  16.  
  17. Global Test:TSound=LoadSound("Test_5_Clicks.ogg",SOUND_LOOP)
  18. PlaySound Test
  19. Global StartTime%=MilliSecs()
  20. Global LastLatency%, Avarage%, Rounds%
  21. Repeat
  22.         Cls
  23.         SetColor 255,255,255
  24.         DrawText "A U D I O   D R I V E R   L A T E N C Y   C H E C K " ,100,40
  25.         DrawText "Checking audio driver: " + DriverName[Nr] ,200,70
  26.         DrawText "1. Listen to the ticks",200,90
  27.         DrawText "2. Feel the timing...",200,110
  28.         DrawText "3. Exactly at the 5th push LEFT MOUSE or the Key [X]",200,130
  29.         DrawText "RESULTS:",200,200
  30.         DrawText "   Last Lastency = " + LastLatency + " msec",200,230
  31.         If Rounds>0
  32.                 DrawText "Avarage Lastency = " + Int(Avarage/rounds)+ " msec",200,250
  33.         EndIf
  34.         DrawText "click here:",200,370
  35.         DrawText "Press [R] to reset values",500,570
  36.         DrawRect 200,400,100,100
  37.         SetColor 1,1,1
  38.         DrawRect 201,401,98,98
  39.         If MouseDown(1)
  40.                 Print latency(MilliSecs()-StartTime)
  41.                 Repeat         
  42.                 Until MouseDown(1)=False
  43.         EndIf
  44.        
  45.         If KeyDown(KEY_X)
  46.                 Print latency(MilliSecs()-StartTime)
  47.                 Repeat         
  48.                 Until KeyDown(KEY_X)=0
  49.         ElseIf KeyDown(KEY_R)
  50.                 Rounds=0
  51.                 Avarage=0
  52.                 LastLatency=0
  53.                 Repeat         
  54.                 Until KeyDown(KEY_X)=0
  55.         EndIf
  56.         Flip 0
  57. Until AppTerminate()
  58.  
  59. Function Latency%(Value)
  60.         value=value-4000
  61.         value=value Mod 6000
  62.         If value>1000
  63.                 value=value-6000
  64.         EndIf
  65.         LastLatency=value
  66.         Rounds=Rounds+1
  67.         Avarage=Avarage+value
  68.         Return value
  69. End Function
  70.  
  71.  
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron 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
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: 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...
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster 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.

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: iWasAdam 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
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster 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?

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: iWasAdam 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

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron 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
Title: Re: Testing latency of different Audio Drivers
Post by: Derron 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
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster 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.

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron 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
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster 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
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.  


Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron 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

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster 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.





Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Derron 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
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster 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.  
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Scaremonger on March 27, 2021, 17:16:23
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):
Code: [Select]
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

Code: [Select]
Method send( sample:TAudioSample)
Local stereo: TAudioSample = sample.convert( SF_STEREO16LE)

...

End method

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 27, 2021, 18:06:08
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.

Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Scaremonger on March 27, 2021, 22:15:41
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.

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. 
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Baggey on March 28, 2021, 11:37:44
@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
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 28, 2021, 12:50:11
...
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
  1. Function OUT_CHUNKS(Port%, Value%)
  2.         Const SAMPLE_RATE%=44100
  3.         Global NextSample:TAudioSample, PlayTime%
  4.         If Counter=0
  5.                 Print "Sample created"
  6.                 NextSample = CreateAudioSample(4410, SAMPLE_RATE, SF_MONO8 )
  7.         EndIf
  8.  
  9.         Select Value & 8
  10.                 Case 0
  11.                         NextSample.Samples[Counter]=50
  12.                 Case 8
  13.                         NextSample.Samples[Counter]=200
  14.         End Select
  15.         Counter=Counter + 1
  16.         If counter=4408
  17.                 NextSample.Samples[4408]=0    
  18.                 NextSample.Samples[4409]=0    
  19.                 Local Audio:TSound = LoadSound( NextSample )
  20.                 Repeat
  21.                         Delay 1
  22.                 Until PlayTime<MilliSecs()
  23.                 Print "play TSound"
  24.                 PlayTime=MilliSecs()+100
  25.                 PlaySound Audio
  26.                 Counter=0
  27.         EndIf
  28. 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
  1. Function OUT_CHUNKS(Port%, Value%)
  2.          Global NextSample:BYTE[441], PlayTime%
  3.  
  4.         Select Value & 8
  5.                 Case 0
  6.                         NextSample[Counter]=50
  7.                 Case 8
  8.                         NextSample[Counter]=200
  9.         End Select
  10.         Counter=Counter + 1
  11.         If Counter=441
  12.                Delay PlayTime-Millisecs()
  13.                RingBuffer.Send NextSample
  14.                PlayTime=PlayTime+10            
  15.                Counter=0
  16.         Endif
  17. 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.
Title: Now Threads and support for SF_MONO8
Post by: Midimaster on March 29, 2021, 18:42:59
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
  1. RingBufferClass.SetDriver("FreeAudio Multimedia")
  2. Global RingBuffer:RingBufferClass = New RingBufferClass
  3.  
  4. Global Source:TAudioSample=LoadAudioSample("Test8.wav")
  5.  
  6. RingBuffer.CreateNew(Source.Format , Source.Hertz , 4)
  7. SendSize = RingBuffer.IntervalSamples()
  8. SEND_TIME= RingBuffer.IntervalTime()
  9.  

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
  1. Local WatchThread:TThread=CreateThread(WatchLoop, "")
  2. Local SendThread:TThread=CreateThread(SendLoop, "")
  3.  
  4. Function WatchLoop:Object(data:Object)
  5.         Repeat
  6.                 Delay 3
  7.                 RingBuffer.Watch
  8.         Forever
  9. End Function
  10.  
  11. Function SendLoop:Object(data:Object)
  12.         Repeat
  13.                 Delay 4
  14.                 If WriteTime<MilliSecs()
  15.                         ' this cares about really 20msec timing:
  16.                         WriteTime =WriteTime + SEND_TIME
  17.                         SendAudio
  18.                 EndIf
  19.         Forever
  20. End Function
  21.  


Your SendAudio()-Function cares about sending the datas to the Ringbuffers:
Code: BlitzMax
  1. Function SendAudio()
  2.                 Local AudioArray:Short[SendSize]
  3.                 SourceStream.Seek(ReadPointer)
  4.                 For Local i%=0 To SendSize-1
  5.                         AudioArray[i] = ...whatever
  6.                 Next
  7.                 RingBuffer.Send AudioArray
  8.                 ReadPointer= ReadPointer+SendSize*2
  9. End Function
  10.  


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

Code: BlitzMax
  1. SuperStrict
  2.  
  3. Global SendSize% , ReadPointer%
  4.  
  5. Graphics 800, 600
  6. RingBufferClass.SetDriver("FreeAudio")
  7.  
  8. Global  SEND_TIME:Int, raus%=0
  9.  
  10. Global Source:TAudioSample=LoadAudioSample("Test8.wav")
  11. Global SourceStream:TStream = CreateRamStream(Source.Samples,Source.Length*4,True,True)
  12. Print  Source.Hertz
  13.  
  14. Global RingBuffer:RingBufferClass=New RingBufferClass
  15. RingBuffer.CreateNew(Source.Format, 22000, 4)
  16. SendSize = RingBuffer.IntervalSamples()
  17. SEND_TIME= RingBuffer.IntervalTime()
  18. Print SendSize + " " + send_time
  19.  
  20. Global WriteTime%=MilliSecs()
  21.  
  22. Local WatchThread:TThread=CreateThread(WatchLoop, "")
  23.  
  24. Local SendThread:TThread=CreateThread(SendLoop, "")
  25.  
  26.  
  27. Repeat
  28.         Cls
  29.         SetColor 255,255,255
  30.         DrawText "click here to PAUSE:",200,370
  31.  
  32.         DrawRect 200,400,100,100
  33.         SetColor 1,1,1
  34.         DrawRect 201,401,98,98
  35.  
  36.         Flip 0
  37. Until AppTerminate()
  38. End
  39.  
  40.  
  41. Function WatchLoop:Object(data:Object)
  42.         Repeat
  43.                 Delay 3
  44.                 RingBuffer.Watch
  45.         Until raus=1
  46. End Function
  47.  
  48.  
  49. Function SendLoop:Object(data:Object)
  50.         Repeat
  51.                 Delay 4
  52.                 If WriteTime<MilliSecs()
  53.                         ' this cares about really 20msec timing:
  54.                         WriteTime =WriteTime + SEND_TIME
  55.                         If MouseDown(1)=0
  56.                                 SendAudio
  57.                         EndIf
  58.                 EndIf
  59.         Until raus=1
  60. End Function
  61.  
  62.  
  63. Function SendAudio()
  64.                 Local AudioArray:Short[SendSize]
  65.                 SourceStream.Seek(ReadPointer)
  66.                 For Local i%=0 To SendSize-1
  67.                         AudioArray[i] = SourceStream.ReadShort
  68.                 Next
  69.                 RingBuffer.Send AudioArray
  70.  
  71.                 ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)
  72. End Function
  73.  
  74. '------------------------------------------------------------------------
  75.  
  76. Type RingBufferClass
  77.         Global MyDriver$
  78.         Global BufferMutex:TMutex=CreateMutex()
  79.  
  80.         Field CHANNELS:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, BUFFER_SIZE:Int
  81.         Field FORMAT:Int, CHUNK_TIME:Int, HERTZ:Int, BITS:Int, ZERO:Int
  82.         Field WritePointer:Int, ReadPointer:Int, RingPointer:Int
  83.         Field WriteTime:Int, WatchTime:Int
  84.  
  85.         Field RingBuffer:TAudioSample, InBuffer:TAudioSample, Sound:TSound
  86.         Field RingStream:TStream, InBufferStream:TStream
  87.        
  88.  
  89.         Function SetDriver(Driver$)
  90.                 If MyDriver<>"" Return
  91.                 If Driver.contains("FreeAudio")=False
  92.                         Notify "wrong AudioDriver"
  93.                         End
  94.                 EndIf
  95.                 MyDriver = Driver
  96.                 SetAudioDriver(MyDriver)
  97.         End Function
  98.        
  99.        
  100.         Method CreateNew(Format%, Hertz%, Latency%)
  101.                         ' HERTZ should be a multiple of 1000 for CHUNK_TIME=10, 20, 40 or 50
  102.                         ' HERTZ can also be 44100 when CHUNK_TIME=20 or 40
  103.                         '
  104.                         ' FORMAT can be SF_MONO8 or SF_MONO16LE or SF_STEREO16LE
  105.                         '
  106.                         ' LATENCY can be from 1 to 32
  107.                         ' 2=extrem small, 4=normal size,  8..=secure sizes
  108.                         '      
  109.                         If MyDriver=""
  110.                                         Notify "No AudioDriver selected"
  111.                                         End
  112.                         EndIf
  113.                         Self.HERTZ=Hertz
  114.                         Self.FORMAT=Format
  115.                         DefineBuffer Latency
  116.                         ClearBuffer
  117.                         WatchTime=MilliSecs()
  118.                         PlaySound Sound
  119.         End Method
  120.        
  121.        
  122.         Private Method DefineBuffer(Latency%)
  123.                 CHUNK_TIME=20
  124.                 Select FORMAT
  125.                         Case SF_MONO16LE
  126.                                 BITS=16
  127.                                 CHANNELS=1
  128.                         Case SF_STEREO16LE
  129.                                 BITS=16
  130.                                 CHANNELS=2
  131.                         Case SF_MONO8
  132.                                 BITS=8
  133.                                 CHANNELS=1                                     
  134.                         Default
  135.                                 Notify "Audio format not supported"
  136.                                 End
  137.                 End Select      
  138.                 CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  139.                 BUFFER_SAMPLES = 4*Latency * HERTZ * CHUNK_TIME             /1000
  140.                 BUFFER_SIZE    = BUFFER_SAMPLES * BITS/8 *CHANNELS
  141.                
  142.                 RingBuffer     = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  143.                 InBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  144.                 Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
  145.                 Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  146.                 RingStream     = CreateRamStream(RingBuffer.Samples , Buffer_size,True,True)
  147.                 InBufferStream = CreateRamStream(InBuffer.Samples,Buffer_size,True,True)
  148.                 RingPointer    =  BUFFER_SIZE/4
  149.         End Method
  150.        
  151.        
  152.         Public Method Watch()
  153.                 If WatchTime<MilliSecs()
  154.                         WatchTime = WatchTime + CHUNK_TIME
  155.                         SendOneChunk
  156.                 EndIf
  157.         End Method
  158.  
  159.  
  160.         Private Method SendOneChunk()
  161.                 LockMutex BufferMutex
  162.                         Local ReadPointerMod% = ReadPointer Mod BUFFER_SIZE
  163.                         InBufferStream.Seek ReadPointerMod
  164.                         RingStream.Seek RingPointer
  165.  
  166.                         If ReadPointer + CHUNK_SIZE > WritePointer
  167.                                 Local Maxi%=WritePointer-ReadPointer
  168.                                 For Local i:Int = 0 To Maxi-1
  169.                                                 RingStream.WriteByte InBufferStream.ReadByte()
  170.                                 Next
  171.                                 For Local i:Int = Maxi To CHUNK_SIZE-1
  172.                                                 RingStream.WriteByte ZERO
  173.                                 Next
  174.                                 ReadPointer=ReadPointer + Maxi
  175.                         Else
  176.                                 For Local i:Int = 0 To CHUNK_SIZE-1
  177.                                         RingStream.WriteByte InBufferStream.ReadByte()
  178.                                 Next
  179.                                 ReadPointer=ReadPointer + CHUNK_SIZE
  180.                         EndIf
  181.                         RingPointer=(RingPointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
  182.                 UnlockMutex BufferMutex
  183.         End Method
  184.  
  185.  
  186.         Private Method ClearBuffer()
  187.                         If BITS=8 ZERO=127
  188.                         For Local i:Int =0 To BUFFER_SIZE
  189.                                 RingBuffer.Samples[i]=ZERO
  190.                         Next   
  191.         End Method
  192.        
  193.        
  194.         Public Method Send(AudioArray:Short[])
  195.                 LockMutex BufferMutex
  196.                 Local WritePointerMod% = WritePointer Mod BUFFER_SIZE
  197.  
  198.                 InBufferStream.Seek WritePointerMod
  199.                
  200.                 If AudioArray.Length*2 + (WritePointerMod) <= BUFFER_SIZE
  201.                         For Local i%=0 To AudioArray.Length-1
  202.                                 InBufferStream.WriteShort AudioArray[i]
  203.                         Next
  204.                        
  205.                 Else
  206.                         Local Maxi% = BUFFER_SIZE-WritePointerMod
  207.                        
  208.                         For Local i%=0 To Maxi/2-1
  209.                                 InBufferStream.WriteShort AudioArray[i]
  210.                         Next
  211.                         InBufferStream.Seek 0
  212.                         For Local i%=Maxi/2 To AudioArray.Length-1
  213.                                 InBufferStream.WriteShort AudioArray[i]
  214.                         Next
  215.                 EndIf
  216.                 WritePointer=WritePointer + AudioArray.Length*2
  217.                 UnlockMutex BufferMutex
  218.         End Method
  219.  
  220.        
  221.         Public Method IntervalSamples:Int()
  222.                 Return CHUNK_SIZE/2
  223.         End Method
  224.  
  225.        
  226.         Public Method IntervalTime:Int()
  227.                 Return CHUNK_TIME
  228.         End Method
  229. End Type
  230.  
  231.  
  232.  
  233.  




Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on March 29, 2021, 18:55:19
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
Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Midimaster on March 29, 2021, 19:16:47
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
  1. 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
  1. RingBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  2. Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )

This workaround was necessary:
Code: BlitzMax
  1. RingBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  2. 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?
Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on March 29, 2021, 20:06:35
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...
Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Scaremonger on March 30, 2021, 07:11:15
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....

Code: [Select]
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
Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Derron on March 30, 2021, 07:41:45
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.

Code: [Select]
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)".


Code: [Select]
ReadPointer=(ReadPointer+SendSize*2) Mod Source.length

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


bye
Ron



Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Midimaster on March 30, 2021, 09:39:42
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:
Code: [Select]
ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)
and this for 8bit:
Code: [Select]
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.



Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Derron on March 30, 2021, 10:02:18
this works for 16bit:
Code: [Select]
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
Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Midimaster on March 30, 2021, 12:57:57
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.


Title: Final Version of the FreeAudio-Ringbuffer
Post by: Midimaster on March 30, 2021, 14:06:00
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
  1. 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
  1. 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
If you send 16bit-Samples it does not matter whether this value is...


So here is the Class: RingBufferClass.bmx for BlitzMax NG and BlitzMax 1.50
Code: BlitzMax
  1. SuperStrict
  2. Type RingBufferClass
  3.         ' A permant running Audio-Output using FreeAudio
  4.         ' --------------------------------------------------
  5.         ' Author: Midimaster at www.midimaster.com
  6.         ' V1.3.2  2021-08-09
  7.         ' see examples of use at https://www.syntaxbomb.com/index.php/topic,8377.0.html
  8.         ' This is Public Domain
  9.         ' -------------------------------------------------------------------
  10.         ' History:
  11.         ' 1.3
  12.         '     added a ShowPercent() function
  13.         '     added WITH_PROTECTION flag default=ON
  14.         '     added REPEAT_LAST_SAMPLE flag default=OFF
  15.         '     speed improvements
  16.         '
  17.         ' 1.2
  18.         '     added a SendOne() function
  19.         '     added a Buffer Overruns protection
  20.         '     now with internal thread
  21.         ' -------------------------------------------------------------------
  22.         ' minimal example:
  23.         '   RingBufferClass.SetDriver("FreeAudio....")
  24.         '   Global RingBuffer:RingBufferClass = New RingBufferClass
  25.         '   RingBuffer.CreateNew(HERTZ, FORMAT)
  26.         '   SendSize:INT  = RingBuffer.IntervalSamples()
  27.         '   SendTime:INT = RingBuffer.IntervalTime()
  28.         '   Global WriteTime = MilliSecs()
  29.         '   Function SendSamples
  30.         '               If WriteTime>MilliSecs() Return
  31.         '               WriteTime = WriteTime + SendTime
  32.         '               Local AudioArray:Int[SendSize]
  33.         '               For Local i:Int=0 To SendSize-1
  34.         '                       AudioArray[i] = any value....
  35.         '               Next
  36.         '               RingBuffer.Send AudioArray
  37.         '   End Function
  38.  
  39.         Global MyDriver$
  40.         Global BufferMutex:TMutex=CreateMutex()
  41.  
  42.         Field CHANNELS:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, BUFFER_SIZE:Int
  43.         Field FORMAT:Int, CHUNK_TIME:Int, HERTZ:Int, BITS:Int
  44.         Field WritePointer:Int, ReadPointer:Int, RingPointer:Int
  45.         Field WriteTime:Int, WatchTime:Int, InFormat:Int
  46.  
  47.         Field RingBuffer:TAudioSample,  Sound:TSound
  48.         Field InBuffer:TBank
  49.         Field InPtrShort:Short Ptr, RingPtrShort:Short Ptr
  50.         Field RingPtrInt:Int Ptr, InPtrInt:Int Ptr
  51.        
  52.         Field WatchThreadB:TThread
  53.         Field WITH_PROTECTION:Int    = 1  ' waits 20msec before buffer overrun
  54.        
  55.  
  56.         Function SetDriver(Driver$)
  57.         ' PUBLIC: Use this to...
  58.                 ' select one of the audio drivers. It needs to be FreeAudio
  59.                 ' on Windows and BlitzMax 1.5 needs to be FreeAudio DirectSound
  60.                 If MyDriver<>"" Return
  61.                 If Driver.contains("FreeAudio")=False
  62.                         Notify "wrong AudioDriver"
  63.                         End
  64.                 EndIf
  65.                 MyDriver = Driver
  66.                 SetAudioDriver(MyDriver)
  67.  
  68.         End Function
  69.  
  70.        
  71.        
  72.         Method CreateNew(Hertz:Int, UserFormat:Int=SF_STEREO16LE , Latency:Int=8)
  73.         ' PUBLIC: Use this to define the Ringbuffer...
  74.                         ' HERTZ should be a multiple of 1000 for CHUNK_TIME=10, 20, 40 or 50
  75.                         ' HERTZ can also be 44100 when CHUNK_TIME=20 or 40
  76.                         '
  77.                         ' UserFormat can be SF_MONO8 or SF_STEREO8 or SF_MONO16LE or SF_STEREO16LE
  78.                         '
  79.                         ' LATENCY can be from 1 to 32
  80.                         ' 2=extrem small, 4=normal size,  8-32..=secure sizes
  81.                         '      
  82.                         If MyDriver=""
  83.                                         Notify "No AudioDriver selected"
  84.                                         End
  85.                         EndIf
  86.                         Self.HERTZ=Hertz
  87.                         Self.FORMAT=SF_STEREO16LE
  88.                         Self.InFormat=UserFormat
  89.                         DefineBuffer Latency
  90.  
  91.                         ClearBuffer
  92.                         WatchTime=MilliSecs()
  93.                         PlaySound Sound
  94.                         Local WatchThread:TThread = CreateThread(WatchLoop,Self)
  95.         End Method
  96.  
  97.  
  98.  
  99.         Method IntervalSamples:Int()   
  100.         ' PUBLIC: Use this to...
  101.                 ' inform how many samples you should send each call    
  102.                 If  (InFormat=SF_MONO8) Or (InFormat=SF_MONO16LE)
  103.                         Return CHUNK_SIZE/2
  104.                 Else
  105.                         Return CHUNK_SIZE
  106.                 EndIf
  107.         End Method
  108.  
  109.        
  110.         Method IntervalTime:Int()
  111.         ' PUBLIC: Use this to...
  112.                 ' inform how long you should wait between calls (in msecs)     
  113.                 Return CHUNK_TIME
  114.         End Method      
  115.  
  116.  
  117.  
  118.         Method ShowPercent:Int()
  119.         ' PUBLIC: Use this to...
  120.                 ' inform how intensiv the buffer is filled 0% - 100%   
  121.                 Local diff:Int=WritePointer-Readpointer
  122.                 diff=diff*100/BUFFER_SIZE
  123.                 Return diff
  124.         End Method
  125.  
  126.  
  127.        
  128.         Method SendOne(Value:Int)
  129.         ' PUBLIC: Use this to...
  130.                 ' send one single sample value to the ringbuffer
  131.                 Global ShortArray:Short[CHUNK_SIZE]
  132.             Global Counter:Int
  133.  
  134.                 Select InFormat
  135.                         Case SF_MONO8                                    
  136.                                 Value=(Value-128)*128
  137.                                 ShortArray[Counter]   = Value
  138.                                 ShortArray[Counter+1] = Value
  139.                                 Counter=Counter+2
  140.                         Case SF_STEREO8                                          
  141.                                 Value=(Value-128)*128
  142.                                 ShortArray[Counter] = Value
  143.                                 Counter=Counter+1
  144.                         Case SF_MONO16LE
  145.                                 ShortArray[Counter]   = Value
  146.                                 ShortArray[Counter+1] = Value
  147.                                 Counter=Counter+2
  148.                         Case SF_STEREO16LE
  149.                                 ShortArray[Counter] = Value
  150.                                 Counter=Counter+1
  151.                 End Select
  152.                 If Counter = CHUNK_SIZE
  153.                         CheckBufferOverRun
  154.                         Transfer ShortArray
  155.                 Counter=0
  156.                 EndIf
  157.         End Method
  158.  
  159.  
  160.  
  161.         Method Send(AudioArray:Int[])
  162.         ' PUBLIC:  Use this to...
  163.                 ' send a couple of samples value to the ringbuffer
  164.                 Local ShortArray:Short[]
  165.                 Select InFormat
  166.                         Case SF_MONO8                                    
  167.                                         ShortArray= New Short[AudioArray.Length*2]
  168.                                         For Local i:Int=0 To AudioArray.Length-1
  169.                                                 Local v:Int = (AudioArray[i]-128)*128
  170.                                                 ShortArray[2*i]   = v
  171.                                                 ShortArray[2*i+1] = v
  172.                                         Next
  173.                         Case SF_STEREO8
  174.                                         ShortArray= New Short[AudioArray.Length]
  175.                                         For Local i:Int=0 To AudioArray.Length-1
  176.                                                 ShortArray[i] = (AudioArray[i]-128)*128
  177.                                         Next
  178.                         Case SF_MONO16LE
  179.                                         ShortArray= New Short[AudioArray.Length*2]
  180.                                         For Local i:Int=0 To AudioArray.Length-1
  181.                                                 ShortArray[2*i]   = AudioArray[i]
  182.                                                 ShortArray[2*i+1] = AudioArray[i]
  183.                                         Next
  184.                         Case SF_STEREO16LE
  185.                                          ShortArray= New Short[AudioArray.Length]
  186.                                         For Local i:Int=0 To AudioArray.Length-1
  187.                                                 ShortArray[i] = AudioArray[i]
  188.                                         Next
  189.                 End Select
  190.                 CheckBufferOverRun
  191.                 Transfer ShortArray
  192.         End Method     
  193.  
  194.  
  195. '
  196. '   E N D   O F   T H E   P U B L I C   F U N C T I O N S
  197. '
  198. ' ***************************************************************************
  199. '
  200. '    I N T E R N A L   F U N C T I O N S :
  201.  
  202. ?bmxng
  203. Private
  204. ?
  205.          Method Watch()
  206.                 If WatchTime<MilliSecs()
  207.                         WatchTime = WatchTime + CHUNK_TIME
  208.                         SendOneChunk
  209.                 EndIf
  210.         End Method
  211.  
  212.  
  213.  
  214.         Function WatchLoop:Object(data:Object)
  215.         Local RingBuffer:RingBufferClass = RingBufferClass(data)
  216.         If Not RingBuffer Then Return Null  
  217.                 Repeat
  218.                         Delay 3
  219.                         RingBuffer.Watch
  220.                 Forever
  221.         End Function
  222.        
  223.                
  224.         Method DefineBuffer(Latency:Int)
  225.                         CHUNK_TIME=20
  226.                         BITS=16
  227.                         CHANNELS=2
  228.                         CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000/2
  229.                         Print "CHUNK = " + chunk_size
  230.                          
  231.                         BUFFER_SAMPLES = 4*Latency * HERTZ * CHUNK_TIME             /1000
  232.                         BUFFER_SIZE    = BUFFER_SAMPLES * BITS/8 *CHANNELS  /2
  233.                        
  234.                         RingBuffer     = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  235.                         InBuffer       = CreateBank(BUFFER_SAMPLES*4)
  236.                         InPtrShort     = Short Ptr(InBuffer.Lock())
  237.                         InPtrInt       = Int Ptr(InPtrShort)
  238.                         RingPtrShort   = Short Ptr(RingBuffer.Samples)
  239.                         RingPtrInt     = Int Ptr(RingPtrShort)
  240.  
  241.  
  242. ?bmxng
  243.                         Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
  244. ?Not bmxng
  245.                         Local fa_sound:Int  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
  246. ?
  247.  
  248.                         Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  249.                         RingPointer    =  BUFFER_SIZE/2
  250.         End Method
  251.  
  252.  
  253.  
  254.  
  255.  
  256.  
  257.          Method SendOneChunk()
  258.                 LockMutex BufferMutex
  259.                         Local ReadPointerMod:Int = (ReadPointer Mod BUFFER_SIZE) /2                    
  260.                         Local r:Int=RingPointer/2
  261.                        
  262.                         If ReadPointer + CHUNK_SIZE > WritePointer
  263.                                 Local Maxi:Int = (WritePointer-ReadPointer)/2
  264.                                 For Local i:Int = 0 To Maxi-1
  265.                                         RingPtrInt[i+r] = InPtrShort[i+ReadPointerMod]
  266.                                 Next
  267.                                 Print "Underrun"
  268.                                 For Local i:Int = Maxi To CHUNK_SIZE/2-1
  269.                                         RingPtrInt[i+r]=0
  270.                                 Next
  271.                                 ReadPointer=ReadPointer + Maxi
  272.                         Else
  273.                                 For Local i:Int = 0 To CHUNK_SIZE/2-1
  274.                                         RingPtrInt[i+r] = InPtrInt[i+ReadPointerMod]
  275.                                 Next
  276.                                 ReadPointer=ReadPointer + CHUNK_SIZE
  277.                         EndIf
  278.                         RingPointer=(RingPointer + CHUNK_SIZE) Mod BUFFER_SIZE
  279.                 UnlockMutex BufferMutex
  280.         End Method
  281.  
  282.  
  283.  
  284.  
  285.  
  286.          Method ClearBuffer()
  287.                         For Local i:Int =0 To BUFFER_SIZE/2
  288.                                 RingPtrInt[i]=0
  289.                         Next   
  290.         End Method
  291.        
  292.  
  293.  
  294.  
  295.  
  296.        
  297.          Method Transfer(ShortArray:Short[])
  298.                 LockMutex BufferMutex
  299.                 Local WritePointerMod:Int =  WritePointer Mod BUFFER_SIZE
  300.                
  301.                 If (ShortArray.Length + WritePointerMod) <= BUFFER_SIZE
  302.                         'Print "in ptr"
  303.                         For Local i:Int=0 To ShortArray.Length-1
  304.                                 InPtrShort[i+WritePointerMod] = ShortArray[i]
  305.                         Next                   
  306.                 Else
  307.                         'Print " transfer in 2 teilen"
  308.                         Local Maxi:Int = BUFFER_SIZE-WritePointerMod
  309.                        
  310.                         For Local i:Int=0 To Maxi-1
  311.                                 InPtrShort[i+WritePointerMod] = ShortArray[i]
  312.                         Next
  313.                         For Local i:Int=Maxi To ShortArray.Length-1
  314.                                 InPtrShort[i-Maxi] = ShortArray[Maxi]
  315.                         Next
  316.                 EndIf
  317.                 WritePointer=WritePointer + ShortArray.Length
  318.                 UnlockMutex BufferMutex
  319.         End Method
  320.        
  321.        
  322.         Method CheckBufferOverRun()
  323.                 Return
  324.         ' private  cares about buffer overruns and report if you send to fast
  325.                 Local grade:Int
  326.                         If ShowPercent()>80
  327.                                 Print "RINGBUFFER: Buffer Overrun!"
  328.                                 If WITH_PROTECTION=0 Return
  329.                                 Print "RINGBUFFER: Protection ON! Wait for " + IntervalTime() + "msec"
  330.                                 Delay IntervalTime()
  331.                         EndIf
  332.         End Method
  333.  
  334.  
  335. End Type
  336.  
  337.  
  338.  


Some examples will follow...

Code Example "Some Noise":
Code: BlitzMax
  1. SuperStrict
  2. Import "RingBufferClass.bmx"
  3. Graphics 800,600
  4. RingBufferClass.SetDriver("FreeAudio DirectSound")
  5. Global RingBuffer:RingBufferClass = New RingBufferClass
  6. RingBuffer.CreateNew(12000, SF_MONO16LE)
  7.  
  8. Global SendSize:Int  = RingBuffer.IntervalSamples()
  9. Global SendTime:Int = RingBuffer.IntervalTime()
  10.  
  11. Global WriteTime:Int = MilliSecs()
  12. Repeat
  13.     Cls
  14.     SendSamples
  15.     Flip
  16. Until AppTerminate()
  17.  
  18. Function SendSamples()
  19.             If WriteTime>MilliSecs() Return
  20.             WriteTime =WriteTime + SendTime
  21.             Local AudioArray:Int[SendSize]
  22.             For Local i:Int=0 To SendSize-1
  23.                     AudioArray[i] = Rand(-1000,+1000)
  24.             Next
  25.             RingBuffer.Send AudioArray
  26. End Function
  27.  



Code Example "Stereo Sinus Mouse":
Code: BlitzMax
  1.  
  2. SuperStrict
  3. Import "RingBufferClass.bmx"
  4. Graphics 800,600
  5. RingBufferClass.SetDriver("FreeAudio DirectSound")
  6. Global RingBuffer:RingBufferClass = New RingBufferClass
  7. RingBuffer.CreateNew(48000, SF_STEREO16LE)
  8.  
  9. Global SendSize:Int  = RingBuffer.IntervalSamples()
  10. Global SendTime:Int = RingBuffer.IntervalTime()
  11.  
  12. Global WriteTime:Int = MilliSecs()
  13. Repeat
  14.     Cls
  15.         DrawText "Move your Mouse in X-dir to change the LEFT speaker", 100,100
  16.         DrawText "Move your Mouse in Y-dir to change the RIGHT speaker", 100,130
  17.     SendSamples
  18.     Flip
  19. Until AppTerminate()
  20.  
  21.  
  22. Global Arc1:Double,  Arc2:Double
  23.  
  24. Function SendSamples()
  25.         If WriteTime>MilliSecs() Return
  26.         WriteTime =WriteTime + SendTime
  27.         Local AudioArray:Int[SendSize]
  28.         For Local i:Int=0 To SendSize-1 Step 2
  29.                 arc1=arc1+MouseX()/100.0+1
  30.                 AudioArray[i] = Sin(arc1)*10000.0
  31.                
  32.                 arc2=arc2+MouseY()/100.0+1
  33.                 AudioArray[i+1] = Sin(arc2)*10000.0
  34.         Next
  35.         RingBuffer.Send AudioArray
  36. End Function
  37.  



Code Example "Moving free in an Audio-File":
(you need the file TextABC.ogg from attachment)
Code: BlitzMax
  1.  
  2. SuperStrict
  3. Import "RingBufferClass.bmx"
  4. Graphics 800,600
  5. RingBufferClass.SetDriver("FreeAudio")
  6. Global RingBuffer:RingBufferClass = New RingBufferClass
  7.  
  8. Global Source:TAudioSample=LoadAudioSample("TestABC.ogg")
  9.  
  10. RingBuffer.CreateNew(12000, SF_MONO16LE)
  11.  
  12. Global SendSize:Int  = RingBuffer.IntervalSamples()
  13. Global SendTime:Int = RingBuffer.IntervalTime()
  14.  
  15. Global WriteTime:Int = MilliSecs()
  16.  
  17. Global Pointer:Int ,LastMouse:Int, Speed:Int=1
  18.  
  19. Repeat
  20.     Cls
  21.         SetColor 255,255,255
  22.         DrawText "Mouse the Slider to navigate through the spoken ABC", 100,100
  23.         DrawRect 50,300,700,20
  24.         SetColor 1,1,1
  25.         DrawRect 52,302,696,16
  26.         SetColor 255,255,255
  27.         DrawOval 50+ Pointer*680/Source.Length/2,298,25,25     
  28.         SetColor 1,1,1
  29.         DrawOval 52+ Pointer*680/Source.Length/2,300,21,21     
  30.    SendSamples
  31.         If MouseDown(1)
  32.                 If MouseX()>49 And MouseX()<745
  33.                         If LastMouse<>MouseX()
  34.                                 LastMouse = MouseX()
  35.                                 Local p%=(MouseX()-50)*Source.Length*2/700
  36.                                 Pointer= p & $FFFFF4
  37.                                 Speed=2
  38.                         Else
  39.                                 Speed=1
  40.                         EndIf
  41.                 EndIf
  42.         Else
  43.                 Speed=1
  44.                 LastMouse=0
  45.         EndIf
  46.     Flip
  47. Until AppTerminate()
  48.  
  49.  
  50.  
  51. Function SendSamples()
  52.         If WriteTime>MilliSecs() Return
  53.         WriteTime =WriteTime + SendTime
  54.         Local AudioArray:Int[SendSize]
  55.         For Local i:Int=0 To SendSize-1
  56.                 Local V%= Source.Samples[Pointer+2*i*Speed]+Source.Samples[Pointer+2*i*Speed+1]*256
  57.                 AudioArray[i] = Source.Samples[Pointer+2*i*Speed]+Source.Samples[Pointer+2*i*Speed+1]*256
  58.         Next
  59.         RingBuffer.Send AudioArray
  60.         Pointer=(Pointer + 2*SendSize*Speed) Mod ((Source.Length*2)-2*SendSize)
  61. End Function
  62.  

more code sample will follow... see next post!
Title: FreeAudio RingBuffer SINGLE VALUE approach
Post by: Midimaster on April 16, 2021, 07:37:30
FreeAudio RingBuffer SINGLE VALUE approach

I adapted the RingbufferClass to old BlitzMax 1.50 and added some security functions for audio timing to prevent buffer overrun.

Now we have again a common version for both BlitzMax NG and BlitzMax 1.50. See post #59 for the source code

In cooperation with Baggey we developed a vintage computer approach of sound speakers like they were used in the 80th. The FreeAudioRingBuffer with its SINGLE-VALUE approach is best for those retro machines:

ZX Spectrum Sound Emulator
Code: BlitzMax
  1. SuperStrict
  2. Import "RingBufferClass.bmx"
  3.  
  4. Graphics 800,600
  5.  
  6. RingBufferClass.SetDriver("FreeAudio DirectSound")
  7. Global RingBuffer:RingBufferClass = New RingBufferClass
  8. RingBuffer.CreateNew(50000, SF_MONO8)
  9.  
  10. Global A:Byte, B:Byte=1, C:Byte, D:Byte=0, E:Byte=0
  11. Global IY:Int
  12.  
  13. ' Music Data
  14. Global data%[] = [80,128,129,80,102,103,80,86,87,50,86,87,50,171,203,50,43,51,50,43,51,50,171,203,50,51,64,50,51,64,50,171,203,50,128,129,50,128,129,50,102,103,50,86,87,50,96,86,50,171,192,50,43,48,50,43,48,50,171,192,50,48,68,50,48,68,50,171,192,50,136,137,50,136,137,50,114,115,50,76,77,50,76,77,50,171,192,50,38,48,50,38,48,50,171,192,50,48,68,50,48,68,50,171,192,50,136,137,50,136,137,50,114,115,50,76,77,50,76,77,50,171,203,50,38,51,50,38,51,50,171,203,50,51,64,50,51,64,50,171,203,50,128,129,50,128,129,50,102,103,50,86,87,50,64,65,50,128,171,50,32,43,50,32,43,50,128,171,50,43,51,50,43,51,50,128,171,50,128,129,50,128,129,50,102,103,50,86,87,50,64,65,50,128,152,50,32,38,50,32,38,50,128,152,50,38,48,50,38,48,50,0,0,50,114,115,50,114,115,50,96,97,50,76,77,50,76,153,50,76,77,50,76,77,50,76,153,50,91,92,50,86,87,50,51,205,50,51,52,50,51,52,50,51,205,50,64,65,50,102,103,100,102,103,50,114,115,100,76,77,50,86,87,50,128,203,25,128,0,25,128,129,50,128,203,255]
  15. Print( "DATA LENGTH: "+Len(data) )
  16.  
  17. CALL_37596()
  18.  
  19. Repeat
  20.         Delay 5
  21. Until AppTerminate()
  22. End
  23.  
  24.  
  25.  
  26. Function CALL_37596:Int()
  27. ' simulation of an old Z80 machine code:
  28.         While True
  29.                 A = data[IY]
  30.                 If A=255
  31.                         Return True
  32.                 End If
  33.                 C = A  
  34.                 B = 0
  35.                 A = 0
  36.                 D = data[IY+1]
  37.                 E = data[IY+2]
  38.                 Repeat              
  39.                         Repeat                    
  40.                                 OUT_254(254,A)
  41.                                 D :- 1
  42.                                 If D=0
  43.                                         D = data[IY+1]
  44.                                         A = A~24  
  45.                                 End If
  46.                                 E :- 1
  47.                                 If E=0
  48.                                         E = data[IY+2]                        
  49.                                 End If
  50.                                 B :- 1                         
  51.                         Until B=0
  52.                         C :- 1                 
  53.                 Until C=0
  54.                 IY :+ 3
  55.                 Print "Step in DATA=" +  iy
  56.         Wend
  57. End Function
  58.  
  59.  
  60. Function OUT_254(Port%, Value%)
  61.         Select Value & 8
  62.                 Case 0
  63.                         RingBuffer.SendONE 50
  64.                 Case 8
  65.                         RingBuffer.SendONE 200
  66.         End Select
  67. End Function
  68.  

You will finde the RingBufferClass.bmx here in post #59:
https://www.syntaxbomb.com/worklogs/done!-a-new-audio-out-approach-in-blitzmax-freeaudio-ringbuffer/msg347049187/#msg347049187
Title: Update BlitzMax FreeAudio RingBuffer
Post by: Midimaster on July 15, 2021, 10:23:26
I wrote an update version 1.2 of the Ringbuffer. It now works on BlitzMax NG and BlitzMax 1.50.

Also I added a security feature to prevent buffer onverruns.

The third improvement is a SINGLE VALUE support, where you now can send single samples values instead of bulked datas too.

The last improvement: The User needs not to care about registering the needed threads any more. The library does this automatically when created.

So a minimalistic example now only needs a few code lines:
Code: BlitzMax
  1. SuperStrict
  2. Import "RingBufferClass.bmx"
  3. RingBufferClass.SetDriver("FreeAudio DirectSound")
  4. Global RingBuffer:RingBufferClass = New RingBufferClass
  5. RingBuffer.CreateNew(50000, SF_MONO8)
  6. ' thats all
  7.  
  8. 'now send sample values:
  9. Global Audio:TAudioSample = .....
  10. For local i%=0 to ...
  11.       RingBuffer.SendONE Audio.Samples[i]
  12. Next
  13.  

You will find the source code of the new version 1.2 in post #59:
https://www.syntaxbomb.com/worklogs/done!-a-new-audio-out-approach-in-blitzmax-freeaudio-ringbuffer/msg347049187/#msg347049187


You will find an example of SINGLE VALUE approach in post #60:
https://www.syntaxbomb.com/worklogs/done!-a-new-audio-out-approach-in-blitzmax-freeaudio-ringbuffer/msg347049552/#msg347049552


Feel free to ask questions or request for additional features here.



Title: Re: Final Version of the FreeAudio-Ringbuffer
Post by: Baggey on July 24, 2021, 14:51:05
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.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.2 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"


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
  1. 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
  1. 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
  1. SuperStrict
  2. Type RingBufferClass
  3.         ' *** RingBufferClass.bmx  ***
  4.         ' A permant running Audio-Output using FreeAudio
  5.         ' --------------------------------------------------
  6.         ' Author: Midimaster at www.midimaster.com
  7.         ' V1.2 2021-07-15
  8.         ' see examples of use at https://www.syntaxbomb.com/index.php/topic,8377.0.html
  9.         ' -------------------------------------------------------------------
  10.         ' minimal example:
  11.         '   Import "RingBufferClassFinal.bmx"
  12.         '   RingBufferClass.SetDriver("FreeAudio....")
  13.         '   Global RingBuffer:RingBufferClass = New RingBufferClass
  14.         '   RingBuffer.CreateNew(HERTZ, FORMAT)
  15.         '   SendSize:INT  = RingBuffer.IntervalSamples()
  16.         '   SendTime:INT = RingBuffer.IntervalTime()
  17.         '   Global WriteTime = MilliSecs()
  18.         '   Function SendSamples
  19.         '               If WriteTime>MilliSecs() Return
  20.         '               WriteTime =WriteTime + SendTime
  21.         '               Local AudioArray:Int[SendSize]
  22.         '               For Local i:Int=0 To SendSize-1
  23.         '                       AudioArray[i] = any value....
  24.         '               Next
  25.         '               RingBuffer.Send AudioArray
  26.         '   End Function
  27.  
  28.         Global MyDriver$
  29.         Global BufferMutex:TMutex=CreateMutex()
  30.  
  31.         Field CHANNELS:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int, BUFFER_SIZE:Int
  32.         Field FORMAT:Int, CHUNK_TIME:Int, HERTZ:Int, BITS:Int
  33.         Field WritePointer:Int, ReadPointer:Int, RingPointer:Int
  34.         Field WriteTime:Int, WatchTime:Int, InFormat:Int
  35.  
  36.         Field RingBuffer:TAudioSample, InBuffer:TAudioSample, Sound:TSound
  37.         Field RingStream:TStream, InBufferStream:TStream
  38.         Field WatchThreadB:TThread
  39.        
  40.  
  41.         Function SetDriver(Driver$)
  42.         ' PUBLIC: Use this to...
  43.                 ' select one of the audio drivers. It needs to be FreeAudio
  44.                 ' on Windows and BlitzMax 1.5 needs to be FreeAudio DirectSound
  45.                 If MyDriver<>"" Return
  46.                 If Driver.contains("FreeAudio")=False
  47.                         Notify "wrong AudioDriver"
  48.                         End
  49.                 EndIf
  50.                 MyDriver = Driver
  51.                 SetAudioDriver(MyDriver)
  52.         End Function
  53.  
  54.        
  55.        
  56.         Method CreateNew(Hertz:Int, UserFormat:Int=SF_STEREO16LE , Latency:Int=8)
  57.         ' PUBLIC: Use this to define the Ringbuffer...
  58.                         ' HERTZ should be a multiple of 1000 for CHUNK_TIME=10, 20, 40 or 50
  59.                         ' HERTZ can also be 44100 when CHUNK_TIME=20 or 40
  60.                         '
  61.                         ' UserFormat can be SF_MONO8 or SF_STEREO8 or SF_MONO16LE or SF_STEREO16LE
  62.                         '
  63.                         ' LATENCY can be from 1 to 32
  64.                         ' 2=extrem small, 4=normal size,  8-32..=secure sizes
  65.                         '      
  66.                         If MyDriver=""
  67.                                         Notify "No AudioDriver selected"
  68.                                         End
  69.                         EndIf
  70.                         Self.HERTZ=Hertz
  71.                         Self.FORMAT=SF_STEREO16LE
  72.                         Self.InFormat=UserFormat
  73.                         DefineBuffer Latency
  74.                         ClearBuffer
  75.                         WatchTime=MilliSecs()
  76.                         PlaySound Sound
  77.                         Local WatchThread:TThread = CreateThread(WatchLoop,Self)
  78.         End Method
  79.  
  80.  
  81.  
  82.         Method IntervalSamples:Int()   
  83.         ' PUBLIC: Use this to...
  84.                 ' inform how many samples you should send each call    
  85.                 If  (InFormat=SF_MONO8) Or (InFormat=SF_MONO16LE)
  86.                         Return CHUNK_SIZE/4
  87.                 Else
  88.                         Return CHUNK_SIZE/2
  89.                 EndIf
  90.         End Method
  91.  
  92.        
  93.         Method IntervalTime:Int()
  94.         ' PUBLIC: Use this to...
  95.                 ' inform how long you should wait between calls (in msecs)     
  96.                 Return CHUNK_TIME
  97.         End Method      
  98.  
  99.  
  100.  
  101.  
  102.        
  103.         Method SendOne(Value:Int)
  104.         ' PUBLIC: Use this to...
  105.                 ' send one single sample value to the ringbuffer
  106.                 Global ShortArray:Short[IntervalSamples()*2]
  107.             Global Counter:Int
  108.  
  109.                 Select InFormat
  110.                         Case SF_MONO8                                    
  111.                                 Value=(Value-128)*128
  112.                                 ShortArray[Counter]   = Value
  113.                                 ShortArray[Counter+1] = Value
  114.                                 Counter=Counter+2
  115.                         Case SF_STEREO8                                          
  116.                                 Value=(Value-128)*128
  117.                                 ShortArray[Counter] = Value
  118.                                 Counter=Counter+1
  119.                         Case SF_MONO16LE
  120.                                 ShortArray[Counter]   = Value
  121.                                 ShortArray[Counter+1] = Value
  122.                                 Counter=Counter+2
  123.                         Case SF_STEREO16LE
  124.                                 ShortArray[Counter] = Value
  125.                                 Counter=Counter+1
  126.                 End Select
  127.                 If Counter=IntervalSamples()*2
  128.                         CheckBufferOverRun
  129.                         Transfer ShortArray
  130.                 Counter=0
  131.                 EndIf
  132.         End Method
  133.  
  134.  
  135.  
  136.         Method Send(AudioArray:Int[])
  137.         ' PUBLIC:  Use this to...
  138.                 ' send a couple of samples value to the ringbuffer
  139.                 Local ShortArray:Short[]
  140.                 Select InFormat
  141.                         Case SF_MONO8                                    
  142.                                         ShortArray= New Short[AudioArray.Length*2]
  143.                                         For Local i:Int=0 To AudioArray.Length-1
  144.                                                 Local v:Int = (AudioArray[i]-128)*128
  145.                                                 ShortArray[2*i]   = v
  146.                                                 ShortArray[2*i+1] = v
  147.                                         Next
  148.                         Case SF_STEREO8
  149.                                         ShortArray= New Short[AudioArray.Length]
  150.                                         For Local i:Int=0 To AudioArray.Length-1
  151.                                                 ShortArray[i] = (AudioArray[i]-128)*128
  152.                                         Next
  153.                         Case SF_MONO16LE
  154.                                         ShortArray= New Short[AudioArray.Length*2]
  155.                                         For Local i:Int=0 To AudioArray.Length-1
  156.                                                 ShortArray[2*i]   = AudioArray[i]
  157.                                                 ShortArray[2*i+1] = AudioArray[i]
  158.                                         Next
  159.                         Case SF_STEREO16LE
  160.                                          ShortArray= New Short[AudioArray.Length]
  161.                                         For Local i:Int=0 To AudioArray.Length-1
  162.                                                 ShortArray[i] = AudioArray[i]
  163.                                         Next
  164.                 End Select
  165.                 CheckBufferOverRun
  166.                 Transfer ShortArray
  167.         End Method     
  168.  
  169.  
  170. '
  171. '   E N D   O F   T H E   P U B L I C   F U N C T I O N S
  172. '
  173. ' ***************************************************************************
  174. '
  175. '    I N T E R N A L   F U N C T I O N S :
  176. ?bmxng
  177. Private
  178. ?
  179.          Method Watch()
  180.         ' PUBLIC: Use this to...
  181.                 ' inform how many samples you should send each call    
  182.                 If WatchTime<MilliSecs()
  183.                         WatchTime = WatchTime + CHUNK_TIME
  184.                         SendOneChunk
  185.                 EndIf
  186.         End Method
  187.  
  188.  
  189.  
  190.         Function WatchLoop:Object(data:Object)
  191.         Local RingBuffer:RingBufferClass = RingBufferClass(data)
  192.         If Not RingBuffer Then Return Null  
  193.                 Repeat
  194.                         Delay 3
  195.                         RingBuffer.Watch
  196.                 Forever
  197.         End Function
  198.        
  199.                
  200.         Method DefineBuffer(Latency:Int)
  201.                         CHUNK_TIME=20
  202.                         BITS=16
  203.                         CHANNELS=2
  204.                         CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
  205.                         BUFFER_SAMPLES = 4*Latency * HERTZ * CHUNK_TIME             /1000
  206.                         BUFFER_SIZE    = BUFFER_SAMPLES * BITS/8 *CHANNELS
  207.                        
  208.                         RingBuffer     = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  209.                         InBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
  210.  
  211. ?bmxng
  212.                         Local fa_sound:Byte Ptr  = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
  213. ?Not bmxng
  214.                         Local fa_sound:Int       = fa_CreateSound( BUFFER_SAMPLES-1, BITS, CHANNELS, HERTZ, RingBuffer.Samples, $80000000 )
  215. ?
  216.  
  217.                         Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
  218.                         RingStream     = CreateRamStream(RingBuffer.Samples , Buffer_size,True,True)
  219.                         InBufferStream = CreateRamStream(InBuffer.Samples,Buffer_size,True,True)
  220.                         RingPointer    =  BUFFER_SIZE/4
  221.         End Method
  222.  
  223.  
  224.          Method SendOneChunk()
  225.                 LockMutex BufferMutex
  226.                         Local ReadPointerMod:Int = ReadPointer Mod BUFFER_SIZE
  227.                         InBufferStream.Seek ReadPointerMod
  228.                         RingStream.Seek RingPointer
  229.                         If ReadPointer + CHUNK_SIZE > WritePointer
  230.                                 Local Maxi:Int=WritePointer-ReadPointer
  231.                                 For Local i:Int = 0 To Maxi-1
  232.                                                 RingStream.WriteByte InBufferStream.ReadByte()
  233.                                 Next
  234.                                 For Local i:Int = Maxi To CHUNK_SIZE-1
  235.                                                 RingStream.WriteByte 0
  236.                                 Next
  237.                                 ReadPointer=ReadPointer + Maxi
  238.                         Else
  239.                                 For Local i:Int = 0 To CHUNK_SIZE-1
  240.                                         RingStream.WriteByte InBufferStream.ReadByte()
  241.                                 Next
  242.                                 ReadPointer=ReadPointer + CHUNK_SIZE
  243.                         EndIf
  244.                         RingPointer=(RingPointer + CHUNK_SIZE) Mod (BUFFER_SIZE)
  245.                 UnlockMutex BufferMutex
  246.         End Method
  247.  
  248.  
  249.          Method ClearBuffer()
  250.                         For Local i:Int =0 To BUFFER_SIZE
  251.                                 RingBuffer.Samples[i]=0
  252.                         Next   
  253.         End Method
  254.        
  255.        
  256.          Method Transfer(ShortArray:Short[])
  257.                 LockMutex BufferMutex
  258.                 Local WritePointerMod:Int = WritePointer Mod BUFFER_SIZE
  259.                 InBufferStream.Seek WritePointerMod
  260.                 If ShortArray.Length*2 + (WritePointerMod) <= BUFFER_SIZE
  261.                         For Local i:Int=0 To ShortArray.Length-1
  262.                                 InBufferStream.WriteShort ShortArray[i]
  263.                         Next                   
  264.                 Else
  265.                         Local Maxi:Int = BUFFER_SIZE-WritePointerMod
  266.                        
  267.                         For Local i:Int=0 To Maxi/2-1
  268.                                 InBufferStream.WriteShort ShortArray[i]
  269.                         Next
  270.                         InBufferStream.Seek 0
  271.                         For Local i:Int=Maxi/2 To ShortArray.Length-1
  272.                                 InBufferStream.WriteShort ShortArray[i]
  273.                         Next
  274.                 EndIf
  275.                 WritePointer=WritePointer + ShortArray.Length*2
  276.  
  277.                 UnlockMutex BufferMutex
  278.         End Method
  279.        
  280.        
  281.         Method CheckBufferOverRun()
  282.         ' private  cares about buffer overruns and report if you send to fast
  283.                 Local grade:Int
  284.                         Local diff:Int=WritePointer-Readpointer
  285.                         diff=diff*100/BUFFER_SIZE
  286.                        
  287.                         If diff>80
  288.                                 Print "RINGBUFFER: Prevent Buffer Overrun! Wait for " + IntervalTime() + "msec"
  289.  
  290.                                 Delay IntervalTime()
  291.                                 'Print WritePointer + " " + Readpointer + " " + (WritePointer-Readpointer) + " " + BUFFER_SIZE
  292.                                 'Print diff + ":Int"
  293.                         EndIf
  294.         End Method
  295. End Type
  296.  


Some examples will follow...

Code Example "Some Noise":
Code: BlitzMax
  1. SuperStrict
  2. Import "RingBufferClass.bmx"
  3. Graphics 800,600
  4. RingBufferClass.SetDriver("FreeAudio DirectSound")
  5. Global RingBuffer:RingBufferClass = New RingBufferClass
  6. RingBuffer.CreateNew(12000, SF_MONO16LE)
  7.  
  8. Global SendSize:Int  = RingBuffer.IntervalSamples()
  9. Global SendTime:Int = RingBuffer.IntervalTime()
  10.  
  11. Global WriteTime:Int = MilliSecs()
  12. Repeat
  13.     Cls
  14.     SendSamples
  15.     Flip
  16. Until AppTerminate()
  17.  
  18. Function SendSamples()
  19.             If WriteTime>MilliSecs() Return
  20.             WriteTime =WriteTime + SendTime
  21.             Local AudioArray:Int[SendSize]
  22.             For Local i:Int=0 To SendSize-1
  23.                     AudioArray[i] = Rand(-1000,+1000)
  24.             Next
  25.             RingBuffer.Send AudioArray
  26. End Function
  27.  



Code Example "Stereo Sinus Mouse":
Code: BlitzMax
  1.  
  2. SuperStrict
  3. Import "RingBufferClass.bmx"
  4. Graphics 800,600
  5. RingBufferClass.SetDriver("FreeAudio DirectSound")
  6. Global RingBuffer:RingBufferClass = New RingBufferClass
  7. RingBuffer.CreateNew(48000, SF_STEREO16LE)
  8.  
  9. Global SendSize:Int  = RingBuffer.IntervalSamples()
  10. Global SendTime:Int = RingBuffer.IntervalTime()
  11.  
  12. Global WriteTime:Int = MilliSecs()
  13. Repeat
  14.     Cls
  15.         DrawText "Move your Mouse in X-dir to change the LEFT speaker", 100,100
  16.         DrawText "Move your Mouse in Y-dir to change the RIGHT speaker", 100,130
  17.     SendSamples
  18.     Flip
  19. Until AppTerminate()
  20.  
  21.  
  22. Global Arc1:Double,  Arc2:Double
  23.  
  24. Function SendSamples()
  25.         If WriteTime>MilliSecs() Return
  26.         WriteTime =WriteTime + SendTime
  27.         Local AudioArray:Int[SendSize]
  28.         For Local i:Int=0 To SendSize-1 Step 2
  29.                 arc1=arc1+MouseX()/100.0+1
  30.                 AudioArray[i] = Sin(arc1)*10000.0
  31.                
  32.                 arc2=arc2+MouseY()/100.0+1
  33.                 AudioArray[i+1] = Sin(arc2)*10000.0
  34.         Next
  35.         RingBuffer.Send AudioArray
  36. End Function
  37.  



Code Example "Moving free in an Audio-File":
(you need the file TextABC.ogg from attachment)
Code: BlitzMax
  1.  
  2. SuperStrict
  3. Import "RingBufferClass.bmx"
  4. Graphics 800,600
  5. RingBufferClass.SetDriver("FreeAudio")
  6. Global RingBuffer:RingBufferClass = New RingBufferClass
  7.  
  8. Global Source:TAudioSample=LoadAudioSample("TestABC.ogg")
  9.  
  10. RingBuffer.CreateNew(12000, SF_MONO16LE)
  11.  
  12. Global SendSize:Int  = RingBuffer.IntervalSamples()
  13. Global SendTime:Int = RingBuffer.IntervalTime()
  14.  
  15. Global WriteTime:Int = MilliSecs()
  16.  
  17. Global Pointer:Int ,LastMouse:Int, Speed:Int=1
  18.  
  19. Repeat
  20.     Cls
  21.         SetColor 255,255,255
  22.         DrawText "Mouse the Slider to navigate through the spoken ABC", 100,100
  23.         DrawRect 50,300,700,20
  24.         SetColor 1,1,1
  25.         DrawRect 52,302,696,16
  26.         SetColor 255,255,255
  27.         DrawOval 50+ Pointer*680/Source.Length/2,298,25,25     
  28.         SetColor 1,1,1
  29.         DrawOval 52+ Pointer*680/Source.Length/2,300,21,21     
  30.    SendSamples
  31.         If MouseDown(1)
  32.                 If MouseX()>49 And MouseX()<745
  33.                         If LastMouse<>MouseX()
  34.                                 LastMouse = MouseX()
  35.                                 Local p%=(MouseX()-50)*Source.Length*2/700
  36.                                 Pointer= p & $FFFFF4
  37.                                 Speed=2
  38.                         Else
  39.                                 Speed=1
  40.                         EndIf
  41.                 EndIf
  42.         Else
  43.                 Speed=1
  44.                 LastMouse=0
  45.         EndIf
  46.     Flip
  47. Until AppTerminate()
  48.  
  49.  
  50.  
  51. Function SendSamples()
  52.         If WriteTime>MilliSecs() Return
  53.         WriteTime =WriteTime + SendTime
  54.         Local AudioArray:Int[SendSize]
  55.         For Local i:Int=0 To SendSize-1
  56.                 Local V%= Source.Samples[Pointer+2*i*Speed]+Source.Samples[Pointer+2*i*Speed+1]*256
  57.                 AudioArray[i] = Source.Samples[Pointer+2*i*Speed]+Source.Samples[Pointer+2*i*Speed+1]*256
  58.         Next
  59.         RingBuffer.Send AudioArray
  60.         Pointer=(Pointer + 2*SendSize*Speed) Mod ((Source.Length*2)-2*SendSize)
  61. End Function
  62.  

more code sample will follow... see next post!

Trying the 'Moving Free in an Audio-File Code' im getting noise at the (start or at the end) or possible everytime the audio selector moves or is up dated on my system.
Moving slider quickly "I hear the Minions"  :)) I shall be reading Further.

Baggey
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Baggey on July 24, 2021, 15:50:27
if We still recive this message are we losing sound DATA? I assume we haved filled the buffer were waiting for it to playout so we can add data's again.

How did you add the 3D effect of RED to the post?

Okay just got this one we use I'm just glowing
USE  {glow=red,2,300}I'm just glowing{/glow} Change sqigly brackets for square brackets  :D

Quote
*)the new version 1.2 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"

Quote
Feel free to ask questions or request for additional features here.

How do we apply a control over volume in the RingBuffer? A Fluke of the ZX Spectrum is if you turn the Speaker bit on and the Mic bit on at the same time the Electonic circuit back feeds and amplifies the sound very Slightly! Could this be achieved  :-\

Baggey
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Midimaster on July 25, 2021, 09:41:40
if We still recive this message are we losing sound DATA? I assume we haved filled the buffer were waiting for it to playout so we can add data's again.

This new message...
"RINGBUFFER: Prevent Buffer Overrun! Wait for 20msec"

...of the ringbuffer does not lose datas! This message only will inform you, that you tried to overfill the buffer and now the buffer reacts with a little break of 20msec with the purpose not to lose your datas. It is only a information in DEBUG mode, that the OP should control his timing in filling the buffer.

In case of overruns a ringbuffer can follow two strategies:

1.
Inform the user that the buffer cannot process the datas and now the user has to care about waiting for a later moment to repeat the sending of this packet.

2.
Waiting automatic for a little time until the buffer can receice the sended packet. This frees the user of thinking about timing, but blocks the computer for 20msec.

The second is the strategy of my ringbuffer. But the user should take the message for serious. In a perfect final app, a buffer overrun should not happen anyway.


Quote
How do we apply a control over volume in the RingBuffer? A Fluke of the ZX Spectrum is if you turn the Speaker bit on and the Mic bit on at the same time the Electonic circuit back feeds and amplifies the sound very Slightly! Could this be achieved

At the moment the FreeAudio-Ringbuffer works like a "at-the-very-last-moment"-audioport. It does not process any datas, but sending them with low latency to the speakers. Thats also the reason, why the buffer is so small. It only has buffer for 20msec of datas.

You could add functions into the methods Send() or SendOne() by yourself. But you can also manipulate the volume in the datas, before sending them. In your Z80-player you use the fix values 50 and 200 to simulate a SQUARE-Wave. Because you use SF_MONO8 the volume would work like this:

Code: [Select]
128 and 128 means 0 volume
127 and 129 means volume 1
118 and 138 means volume 10
 28 and  228 means volume 100
the maximum combination is ...
  0 and 255 means volume 127
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on July 25, 2021, 10:44:16
Puts on swimsuit and dive in...
Quote
...of the ringbuffer does not lose datas
data is the plural of datum
Datas is a Brazilian municipality in the north-center of the state of Minas Gerais

as a professional you know this?

Quote
128 and 128 means 0 volume
127 and 129 means volume 1
118 and 138 means volume 10
 28 and  228 means volume 100
the maximum combination is ...
  0 and 255 means volume 127

Now without being snarky... WTF!

This has got to be some of the crappiest code/explanation I have EVER seen... period. And I've see a lot...

let's assume that the explanation is correct and that your nonsensical volume revolves around 128 being 0.
Surely it would help everyone (including yourself) to write a tiny method to wrap all the crap up into something like:

SetVolume( volume:int ) ' where volume is 0=no volume to 127 full volume

Thirdly. The ring buffer
evidently it complain if you send it too little data and also if you send it too much data... So it pauses while you get to figure out what the heck it's doing now? <- that's just terrible programming.

Your ring buffer should poll back (a callback) to your app that it wants data of a certain kind and size. if it doesn't get it then it should be able to handle it.

i am certainly not having a go at your programming - but the two users who are attempting to use your code are constantly having issues. which need detailed explanation of the strangest kind (see volume above)


Ok. Here's a simple lesson for you:
if your users are having issues - it is NOT their fault. It is your fault for
a. not explaining clearly what your code needs
b. the code itself is so poorly written it can't be understood

There seems to be some form of underlying additional complexity going on.
The ring buffer itself should be simple taking up less that 50 lines of code...

I went back to look over my own code (because it works flawlessly I never need to check it)
Here is the header for the ring buffer
Code: [Select]
Namespace audio

#Import "ringbuffer.h"

Extern

Class RingBuffer Extends Void
Field readPointer:Int
Field writePointer:Int
Method Handle:Void Ptr()
Method WriteSamples( samples:Double Ptr, sampleCount:Int )
Function Callback( a:Void Ptr, b:UByte Ptr, byteCount:Int )
Function Create:RingBuffer()
End

and here's the .h file itself
Code: [Select]
#pragma once
#include <deque>
#include <mutex>
#include <algorithm>

typedef double Sample;

class RingBuffer{

std::mutex mutex;
std::deque<Sample> buffer;

public:
int readPointer = 0;
int writePointer = 0;


void WriteSamples(Sample *samples, int count){
mutex.lock();
for(int i=0; i<count; i++){
buffer.push_back( samples[i] );
}
writePointer += count;
mutex.unlock();
}


void readSamples(short *dest, int sampleCount){
mutex.lock();
int available = buffer.size();
if (available >= sampleCount){
for(int i=0; i<sampleCount; i++){
Sample s = buffer.front();
buffer.pop_front();
dest[i] = 32767*std::max(-1.0, std::min(s, 1.0));
}
readPointer += sampleCount;
}
mutex.unlock();
}

static void Callback(void *a, unsigned char *b, int c){
memset(b, 0, c);
auto pipe = (RingBuffer*)a;
int sampleCount = c/2;
short *dest = (short *)b;
pipe->readSamples(dest, sampleCount);
}

void *Handle(){
return (void *)this;
}

static RingBuffer *Create(){
return new RingBuffer();
}
};

Dont even attempt to compile it - you wont be able to.

What it shows that this stuff is simple. you set it up and it works - the rest you build on top of this.

Getting back to the volume crapology
Code: [Select]
method SetVolume( inVolume:int )
newVolume = inVolume + 128
end method
isn't that a better solution? adding a clamp or bounds checks would be even better. but it's a simple thing that would stop your users being confused...
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on July 25, 2021, 10:57:09
oh I missed this one- its a classic:  :o
Quote
but sending them with low latency to the speakers

NO, no BIG NO, and NO again.

At no stage do you send ANYTHING to the speakers - you app does not even know anything about speakers. it knows about audio drivers - they handle the internal transfer of audio data to whatever output is defined. and this can be redefined outside of your app at any time. so in theory and app which looses driver output for a given output can crash - but that is another story.

In essence you app says - hey driver what have you got connected?
the driver responds with an amount of attatched devices - usually starting from 0. although minus numbers may be fed back to the driver for the default device.
you then queery the driver with the device number, and the driver will give you a string representation of the device
You usually attach speakers to the 'Line Out' - sometimes depending on your system you might even have internal speakers

Here's a quick list of the attatched sound devices on my system
(https://vjointeractive.files.wordpress.com/2021/07/screenshot-2021-07-25-at-10.52.30.png)

depending on the audio driver I use, some of these might dissapear.

One other thing to be really aware of - each time you run your app, it is possible that these 'devices' will be in different positions and have different numbers - it's you who must keep track and play nice with them.

But NO... You never send anything to the speakers - you send data to the audio driver and it handles all of that...
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on July 25, 2021, 11:28:20
audio programming is low level - you must be precise with your explanations.

your app speaks to the audio driver - the driver is the interface between the hardware and the system/you
the driver is set up to speak to an attached device - this is at a hardware level

How you deal with the driver is up to you - but in essence. it is driver:GIVE ME DATA... you: here you go...
if the data is wrong or missing the driver will either get upset and crash or just make stuff up (noise)
The driver are not too intelligent and wont care about your data - that is your responsibility.

There is no one simple way that works - this is very low level stuff and gets nasty very quickly.

I find that the best way to deal with it is to try and get the core working in whatever way is the tightest and works.
Then build a simple interface on top of this. on top of that you build you audio systems and your user interfaces, etc.

I have found that a good ring buffer is the best way to deal with things.
The ring buffer callback when it wants feeding and also has a forward buffer so it is never hungry

How do I set up the ring buffer:
Code: [Select]
const FRAGMENT_SIZE:double = 512 '<- a single channel size
const FRAGMENT_SIZE2:double = FRAGMENT_SIZE * 2 '<- x2 because we are dealing with stereo
const WRITE_AHEAD:double = FRAGMENT_SIZE * 6 '<- the forward buffer is always kept x3 fed with data
' const FREQUENCY:double = 48000 '<- true fairlight frequency - actually it is really 96000 and then divided to get the correct frequencies (engineer told me how this works)
const FREQUENCY:double = 44100 '<- base frequency to keep consistency with previous versions


in a thread a have an updateaudio to keep the buffers filled
Code: [Select]
Method UpdateAudio()
_runTime += 1
Local buffered:long
Local buffer:double[]
While True
buffered = ringBuffer.writePointer - ringBuffer.readPointer
If buffered >= WRITE_AHEAD Exit
buffer = FillAudioBuffer( FRAGMENT_SIZE )
ringBuffer.WriteSamples( Varptr buffer[0], FRAGMENT_SIZE2 )
Wend

The one that you can see is the buffers and audio data is stored in memory blocks of doubles (2x for stereo)

doubles give you HUGE amount of control over the output audio. it's got amazing precision, and you can doo all sorts of very quick but simple stuff like realtime volume, etc.

How do I deal with audio files?
A. Simple
1. you convert EVERYTHING into a simple single format when you import the audio (and do the same when you write the audio back to a file)
2. the core internal sound format is doubles or floats (you can use either but use only one) with the general volume profile being -1 to 1

so reading out data to the buffer with a volume would be something line
Code: [Select]
volume:float = 0 .. 1
begin
leftbuffer = leftsample * volume
rightbuffer = rightsample * volume
next buffer position
next sample position
end

Now I know there have been issues with playhead (this is the current position in the current sample)
Lets take the code above
Code: [Select]
volume:float = 0 .. 1
sndPos = start of sample
_playhead:long '<- links to an external variable we can look at to get the current position of the playhead
begin
leftbuffer = leftsample[sndPos] * volume
rightbuffer = rightsample[sndPos] * volume

_playhead = (sndPos Mod sample.Length)

next buffer position
sndPos += next sample position offset
end


Now I'm not saying that this code is good or correct. but it is simple. it is easy to read even for someone who has very little experience with audio.
It is also very small and tight and therefore FAST....
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on July 25, 2021, 12:00:29
I've got to add 'just one more thing'....  :o

T I M I NG....

Your machine is faster than mine is, and my machine is faster than Qubes, etc, etc

The buffer system should be running at a single defined speed (usually 44100) there is really no reason to use different speeds, etc.

But... You have to deal with timing systems yourself  - usually by interrogating the system timer Millisecs (1000 ticks per second) or (if I recall correctly microsecs which is 1000000 per second - i'd need to check that one)

So (lets assume it was for a spectrum emulator), you would need to have some mechanism that corrects for the computer speed and system speed giving a stable result - it might be that you build a z80 system running at a fixed speed and use that to feed the buffers?

In my own experience I deal with high precision audio synthesis, with correct note output being what I watch closely...

I have static sample buffers and read from these to the audio buffers in realtime modifying the output as needed.

Although I have direct control into the audio systems, I don't run these at buffer sped but generally at around 60-70 times a second - that is enough for giving time to the system and not overloading it.

UI stuff is handled completely separate from the audio systems. It can read and modify, display the sample data etc. but playback is all being done without a gui.
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on July 25, 2021, 14:17:37
OK guys we have a personal response. so I will make sure everyone else see this:
Quote
I dont know, what you want, but you cannot ignore the fact that the Audio-format SF_MONO8 is defined as:

Zero-Level (Silence) is 128
values below 127 are equivalent to negative values in a 32bit-float-approach
values above 129 are equivalent to positiv values in a 32bit-float-approach

Value 0 is equivalent to -1 a 32bit-float-approach
Value 255 is equivalent to +1 a 32bit-float-approach

If Baggey is simulating a 8bit machine he only has BYTES!!!! and he has to handle with it. So if he wants to create a SQUARE WAVE of full volume he has to use the 0 and the 255. And if he wants to use the half volume he has to use the 64 and 196.

By the way... You are extremly arrogant. I know of course that the Ringbuffer does not communicate directly to the speaker. AND YOU KNOW TOO, THAT I OF COURSE KNOW THIS. I wrote this only because of the context of a Z80, where the port256 perhaps was really the last step towards the speaker. So Baggey can see the buffer as the speaker itself.

And thank you for your explanation of DATAS. This helped others a lot in understanding my text. Please continue correcting my english. It looks you have a lot of time...

Now if I was to respond It would be something along the lines of before: E.G

1. it is irrelevant how you deal with emulating something - it's the end result that count - so you could use stereo at 128bits to emulate an 8 bit system. the best approach is to take a nice simple and easy way. using signed 8bits can be a complex way to deal with things. it would be better to take a step back look at the system and find a nice simple way to do it

2. the above volume code is absolute pants! you cannot in anyway justify it

3. This person 'knows' what is going on and knows the code is terrible The quote "I know of course that the Ringbuffer does not communicate directly to the speaker" Brilliant - then why did you say otherwise. if you are so wise, what the bugger are you doing telling people to do this do that (deliberately knowing that what you are saying is wrong)

4."By the way... You are extremly arrogant.". But at least I am trying to tell people how things work instead of telling them utter rubbish in the hope that they understand what is going on - even though what you tell them (you deliberately know) is wrong.

Just remember it was you who said they didn't understand c++, didn't understand this and that. and kept complaining... And then you decide that you are the font of all knowledge relating to audio stuff. You then decide that you now know everything (and tell people to do what you say and tell them deliberately incorrect information). And then complain when others have serious issues with you code because it's (I think the word is) Arcane. <- something that is old, complex and preferably secret...

It should also be again repeated - Audio is low level, it is complex and nasty. the best way with it is to be clear and simple. I've at least tried to give a different approach with this in mind - You on the other hand want to create the most complex solution to a simple problem. feed inaccurate information and then sit on your (obviously) gilded throne and relish when people can't figure out what the hell you mean. mysterious variables that appear out of nowhere, volume that starts at 128 and can go in both directions with the same result.

It also seems that the only time you actually begin to take the time and try to explain something with a little more clarity is when you are provoked to do so.

Maybe you should take some time and look at what is being said...

I will end with another direct quote from you:
Quote
I know of course that the Ringbuffer does not communicate directly to the speaker.
Could you please explain to everyone why you said it did?
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on July 25, 2021, 14:51:36
Do you know what folks - I just went back and reread what I and other said and thought I would bring this up:
Quote
128 and 128 means 0 volume
127 and 129 means volume 1

Now.... (wheres my swimsuit...)
lets take this apart 128 = no volume and 129 = volume 1
That's not actually correct - infact it's completely incorrect.

If we feed ANY static 8 bit value we will get silence. silence = no sound. so feeding any repeating single value will be silent!
So that means that any value is 0 volume!!!!

OK i'm being pedantic and deliberately snarky here - but the fact remains that this is correct.

Why?
A. because it is the relationship between the values that causes the sound - any single repeating number will be silence!

Let's assume we are feeding a repeating square wave - we could have the 2 values being 0 and 255. but we also need to know if the DRIVER is set up to accept signed or unsigned values.

if set up to accept signed values then we can only send -127 to 128 with 0 being the center point
if unsigned then values from 0 to 255 are acceptable
byte vs ubyte

Lets (just for the same of an example) accept we are dealing with unsigned values (a common 8bit audio data format)

we can send 0 and 128 and get half volume, 0 and 64 and get quarter volume 0 and 255 and get full volume
Why?

Because It's not the value but the difference between the values that counts. This is a fundamental truth.

You could send 0 and 128, or 128 and 255, or 64 and 191 - the audio result would be exactly the same - it is the difference between the values that counts.

If you really want to be pedantic it is the relationship between voltages and the movement of the speakers back and forth that creates the sound.

But it is much simpler to think about audio with 0 being the center point. That way you can visualise sin waves going from -1 to 1 around 0.

And in that same breath - it is much simpler to think of volume being from 0 to a high number (1 is a good number) because that is the way we (as humans) think of volume 0 = no volume, 1 or 100 being full volume.

Why would 1 be a good number to have a the high volume?
A.
- Lets assume you have a +- value. Let's say 5.
- if we multiply 5 x 1 = 5
- if we multiply 5 x 2 = 10
- if we multiply 5 x 0.5 = 2.5

nice and simple maths but what have we done?
- in the first case (x1) the output audio is the same as the input
- in the second case (2x) the output audio is now twice as loud (remember the correlation of difference between values)
- in the third example (x0.5) the output is now halves so is half as loud

It's a key reason to potentially use a bigger data structure (floats vs bytes, etc) and convert down when needed or just stick to floats, etc. You get all of these nice things for free - nothing complicated, nothing extra needed.

(if we kept to a strict 8bits, we need lots more strange math, shifts, and other unpleasant stuff that needs explaining to get the same result - in some cases you trade complexity for tight code and fast results)

But as it has been pointed out - I know nothing about audio and I'm also arrogant
But at least I will try to tell you why and tell you the truth...
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Midimaster on July 25, 2021, 15:39:54
I did not read your last 2 post... but: You are right, Adam! I now see that you are right! Can we now stop this discussion?

For all others,who like me believed that there is a defined MidPoint-Value of 128 in SF_MONO8:

You are not alone! Also Microsoft believes this:
On page 60 of the microsoft bulletin about PCM-format ( and hundreds of other documents) you can find this information:
http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf
(https://www.syntaxbomb.com/worklogs/done!-a-new-audio-out-approach-in-blitzmax-freeaudio-ringbuffer/?action=dlattach;attach=4727)



Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: iWasAdam on July 26, 2021, 05:09:49
thankyou fo this amazing clarity of changing the subject.

we are now dealing with midpoints now - oh a see...   ;D

My you must be correct
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Baggey on August 06, 2021, 20:40:39
Hi, Midimaster

Ive been experimenting with you're code and it is almost working for me! i am interested in your other idea of the microseconds function! Ive altered values and i am starting to get good results with short sounds at the expense of long sounds! And Vise Versa. Timming is more crucial than i thought for sending and recieving one Byte at a time with SpecBlitz.
Please keep up with the good WORK!

Kind Regards Baggey
Title: Re: DONE! A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Midimaster on August 07, 2021, 07:31:27
I published a new update of the RingBufferClass. Now you can download the Version 1.3 from post #59:
https://www.syntaxbomb.com/index.php/topic,8377.msg347049187.html#msg347049187


What is new?

ShowPercent
I added a ShowPercent() function where you no can check how full the buffer is. The value is from 0% to 100%. If the level reaches 80% the overruns protection can take pauses of 20msec to slowdown your too fast sending of data
Code: BlitzMax
  1. Global RingBuffer:RingBufferClass = New RingBufferClass
  2. ....
  3. Print "Level of ring buffer = " + RingBuffer.ShowPercent + "%"
  4.  


WITH_PROTECTION

Therefore I added a new flag WITH_PROTECTION:Int, which is default ON. But you can switch these pauses and run constantly (which now may risk buffer overruns)
Code: BlitzMax
  1. Global RingBuffer:RingBufferClass = New RingBufferClass
  2. RingBuffer.WITH_PROTECTION=0
  3.  


REPEAT_LAST_SAMPLE

The idea of one user to repeat the last data instead of sending SILENCE in cases, where the user sended to little data, turned out not to make any hearable difference. Both (sending last data or sending 0) had the same effect of  silence. so I did not add this flag.


Speed improvements

With consequent use of INTEGER POINTERs I could optimize the performance of the class with faktor 10. I removed my first idea of transfering the data with streams. Now all transfers are based on direct RAM access.


SendOne()

Data sent with SendOne() are now processed 10 times as often as before. So now every 100 user Data they are transmitted to the FreeAudio-Driver. This has no hearable effect and is only a test.

Title: Found a bug
Post by: Midimaster on August 09, 2021, 01:29:56
I updated the FreeAudioRingBuffer to version 1.3.2

there was a bug in method Transfer():
Code: BlitzMax
  1. Method Transfer(ShortArray:Short[])
  2.         LockMutex BufferMutex
  3.         Local WritePointerMod:Int =  WritePointer Mod BUFFER_SIZE
  4.         If (ShortArray.Length + WritePointerMod) <= BUFFER_SIZE
  5.                 For Local i:Int=0 To ShortArray.Length-1
  6.                         InPtrShort[i+WritePointerMod] = ShortArray[i]
  7.                 Next                   
  8.         Else
  9.                 Local Maxi:Int = BUFFER_SIZE-WritePointerMod
  10.                 For Local i:Int=0 To Maxi-1
  11.                         InPtrShort[i+WritePointerMod] = ShortArray[i]
  12.                 Next
  13.                 For Local i:Int=Maxi To ShortArray.Length-1
  14.                         'InPtrShort[i]=ShortArray[i+Maxi]   ' <-------  **** HERE ****
  15.                         InPtrShort[i-Maxi]=ShortArray[i]
  16.                 Next
  17.         EndIf
  18.         WritePointer=WritePointer + ShortArray.Length
  19.         UnlockMutex BufferMutex
  20. End Method

Quote
... Data sent with SendOne() are now processed 10 times as often as before. So now every 100 user Data they are transmitted to the FreeAudio-Driver. This has no hearable effect and is only a test.
The idea of sending very sort chunks in  SendOne() brought no advantages, so I returned to the old code:
Code: BlitzMax
  1.         Method SendOne(Value:Int)
  2.         ' PUBLIC: Use this to...
  3.                 ' send one single sample value to the ringbuffer
  4.                 'Global ShortArray:Short[100]            <-------  **** HERE ****
  5.                 Global ShortArray:Short[CHUNK_SIZE]
  6.             Global Counter:Int
  7.  
  8.                 Select InFormat
  9.                         ....
  10.                 End Select
  11.                 'If Counter = 100                            <-------  **** HERE ****
  12.                 If Counter = CHUNK_SIZE
  13.                         CheckBufferOverRun
  14.                         Transfer ShortArray
  15.                 Counter=0
  16.                 EndIf
  17.         End Method
  18.  



The Updated Source Code
... is here:
https://www.syntaxbomb.com/index.php/topic,8377.msg347049187.html#msg347049187
SimplePortal 2.3.6 © 2008-2014, SimplePortal