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
Quote from: Scaremonger on March 22, 2021, 21:32:40
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:
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
Quote from: Midimaster on March 23, 2021, 10:35:35
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) Select

Self.buffer = New TBank
Self.buffer.Resize( bufferLength * 4 ) 'SizeOf( Int(0) ) )
Local audioSample:TAudioSample = CreateStaticAudioSample(buffer.Lock(), GetBufferLength(), freq, format)

'driver specific sound creation
CreateSound(audioSample)
....


'=== CONTAINING FREE AUDIO SPECIFIC CODE ===
?bmxng
Method CreateSound:Byte Ptr(audioSample:TAudioSample)
?Not bmxng
Method CreateSound:Int(audioSample:TAudioSample)
?
'not possible as "Loadsound"-flags are not given to
'fa_CreateSound, but $80000000 is needed for dynamic sounds
Rem
$80000000 = "dynamic" -> dynamic sounds stay in app memory
sound = LoadSound(audioSample, $80000000)
endrem

'LOAD FREE AUDIO SOUND
'?bmxng
Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
'?Not bmxng
' Local fa_sound:Int = int(fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 ))
'?
'"audioSample" is ignored in the module, so could be skipped
'sound = TFreeAudioSound.CreateWithSound( fa_sound, audioSample)
sound = TFreeAudioSound.CreateWithSound( fa_sound, Null)
End Method


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) Select

Local format:Int = SF_STEREO16LE
Local bits:Int = 16
Local freq:Int = 44100
Local channels:Int = 2

Local bufferLength:Int = 1024
Local audioBufferBank:TBank = New TBank
audioBufferBank.Resize( bufferLength * 4 ) 'SizeOf( Int(0) ) )

Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)


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
Quote from: Derron on March 23, 2021, 12:16:20...so cut down:

Derron, you speak in riddles to me...

When I do this little code:
Code (BlitzMax) Select
Graphics 800,600
Local AudioSample:TAudioSample = CreateAudioSample(44100, 44100, SF_MONO16LE)
For Local i%=0 To 44100-1
AudioSample.samples[i]=Sin(i)*1000.0
Next
Local sound:TSound =LoadSound(AudioSample)
PlaySound Sound
Repeat
Flip 1
Until AppTerminate()

...it works as expected.

Now to your code:
Code (BlitzMax) Select
Graphics 800,600
Local audioSample:TAudioSample = CreateAudioSample(44100, 441000, SF_MONO16LE)
For Local i%=0 To 44100-1
AudioSample.samples[i]=Sin(i)*1000.0
Next

Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, 16, 1, 44100, audioSample.samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)

PlaySound Sound
Repeat

Until AppTerminate()

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


Quote from: Midimaster on March 23, 2021, 12:49:59
Now to your code:
Code (BlitzMax) Select
Graphics 800,600
Local audioSample:TAudioSample = CreateAudioSample(44100, 441000, SF_MONO16LE)
For Local i%=0 To 44100-1
AudioSample.samples[i]=Sin(i)*1000.0
Next

...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) Select

SuperStrict
Framework Brl.StandardIO
Import Brl.Audio
Import Brl.FreeAudioAudio

Import Brl.GLMax2D
Import Brl.Bank
Import random.xoshiro

'open window
Graphics 800, 600
SetAudioDriver("FreeAudio")


'mono sound
rem
Local AudioSample:TAudioSample = CreateAudioSample(44100, 44100, SF_MONO16LE)
For Local i%=0 To 44100-1
AudioSample.samples[i] = Sin(i)*1000.0
Next
Local sound:TSound = LoadSound(AudioSample)
PlaySound Sound
endrem


Local format:Int = SF_MONO16LE 'SF_STEREO16LE
Local bits:Int = 16
Local freq:Int = 44100
Local channels:Int = 2
'Local bufferLength:Int = 1024
Local bufferLength:Int = 44100
Local audioBufferBank:TBank = New TBank
audioBufferBank.Resize( bufferLength )

Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)

'change sound after creation
'For Local i:int = 0 Until audioBufferBank.capacity()
' AudioSample.samples[i] = Sin(i)*1000.0
'Next

'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



PlaySound sound

Repeat
Cls

Flip
Until KeyHit(KEY_ESCAPE) Or AppTerminate()


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) Select

SuperStrict
Framework Brl.StandardIO
Import Brl.Audio
Import Brl.FreeAudioAudio

Import Brl.GLMax2D
Import Brl.Bank
Import random.xoshiro 'I prefer that over brl.RandomDefault

'open window
Graphics 800, 600
SetAudioDriver("FreeAudio")



Local format:Int = SF_MONO16LE 'SF_STEREO16LE
Local bits:Int = 16
Local freq:Int = 44100
Local channels:Int = 2
'Local bufferLength:Int = 1024
Local bufferLength:Int = 44100
Local audioBufferBank:TBank = New TBank
audioBufferBank.Resize( bufferLength )

Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length, bits, channels, freq, audioSample.samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)

FillSoundBuffer(audioBufferBank)


PlaySound sound

Repeat
Cls
DrawText("SPACE for random sound data", 0,0)
If KeyHit(KEY_SPACE) Then FillSoundBuffer(audioBufferBank)
Flip
Until KeyHit(KEY_ESCAPE) Or AppTerminate()



Function FillSoundBuffer(bank:TBank)
Local tone:int = Rand(0,1000)
'or do it by manipulating the memory, not the "samples"
For Local i:int = 0 Until bank.capacity()
bank.PokeByte(size_t(i), int(Sin(i)* tone))
Next
End Function
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) Select

Local format:Int = SF_STEREO16LE
Local bits:Int = 16
Local freq:Int = 44100
Local channels:Int = 2
Local bufferLength:Int = 1024 * 4 '4kb buffer
Local audioBufferBank:TBank = New TBank
audioBufferBank.Resize( bufferLength )

Local audioSample:TAudioSample = CreateStaticAudioSample(audioBufferBank.Lock(), bufferLength, freq, format)
Local fa_sound:Byte Ptr = fa_CreateSound( audioSample.length / 32, bits, channels, freq, audioSample.samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)

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) Select

Local format:Int = SF_STEREO16LE
Local bits:Int = 16
Local freq:Int = 44100
Local channels:Int = 2
Local sampleCount:Int = 1000
Local sampleSize:Int = 8
Local sampleData:TBank = CreateBank(sampleCount * sampleSize)

Local fa_sound:Byte Ptr = fa_CreateSound( int(sampleData.size()), bits, channels, freq, sampleData.Lock(), $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null )

FillSoundBuffer(sampleData)



And you might even be able to skip the "TSound" too
Code (BlitzMax) Select

Local format:Int = SF_STEREO16LE
Local bits:Int = 16
Local freq:Int = 44100
Local channels:Int = 2
Local sampleCount:Int = 1000
Local sampleSize:Int = 8     'think that equals  BytesPerSample[format] * channels
Local sampleData:TBank = CreateBank(sampleCount * sampleSize)

Local fa_sound:Byte Ptr = fa_CreateSound( int(sampleData.size()), bits, channels, freq, sampleData.Lock(), $80000000 )
'start playing
Local fa_channel:Int = fa_PlaySound( fa_sound,  0, 0)
'in case you need the TChannel (for volume adjustment etc)
'Local channel:TChannel = TFreeAudioChannel.CreateWithChannel( fa_channel )

FillSoundBuffer(sampleData)


Edit - maybe "length" is "size of block divided by bytersPerSample" ?
Code (BlitzMax) Select

Local fa_sound:Byte Ptr = fa_CreateSound( int(sampleData.size() / BytesPerSample[format]), bits, channels, freq, sampleData.Lock(), $80000000 )


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
Quote from: Derron on March 23, 2021, 14:44:06
I took your  code and added some stuff here and there:.....
Code (BlitzMax) Select

SuperStrict
Framework Brl.StandardIO
Import Brl.Audio
Import Brl.FreeAudioAudio

Import Brl.GLMax2D
Import Brl.Bank


'open window
Graphics 800, 600
SetAudioDriver("FreeAudio")
.....


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?
    '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) Select
SuperStrict
Graphics 800, 600
SetAudioDriver("FreeAudio")


Global Vorlage:TAudioSample=LoadAudioSample("test.ogg")

Local audioSample:TAudioSample = CreateAudioSample(8820, 22050, SF_MONO16LE)

Local fa_sound:Byte Ptr = fa_CreateSound( 8820, 16, 1, 22050, audioSample.samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)


Global Zeit%=MilliSecs()
PlaySound sound
Zeit=MilliSecs()+50
Local Zeiger%=0
Global lfd%=0
Repeat
Cls
If Zeit<MilliSecs()
zeit=zeit+20
Print Zeiger
For Local i:Int = 0 Until 882
AudioSample.samples[zeiger+i] = Vorlage.Samples[lfd]
lfd=lfd+1
Next
Zeiger=(Zeiger +882) Mod 17640
EndIf
Flip 0
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) Select
SuperStrict
Graphics 800, 600
SetAudioDriver("FreeAudio")
Global WritePointer:Int, ReadPointer:Int, WriteTime:Int
Global BITS:Int, CHANNELS:Int, BUFFER_SIZE:Int, CHUNK_SIZE:Int, BUFFER_SAMPLES:Int

Const HERTZ:Int=22050

Const CHUNK_TIME:Int = 20 'msec
Const FORMAT:Int     = SF_MONO16LE

Select FORMAT
Case SF_MONO16LE
BITS:Int=16
CHANNELS:Int=1
CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
BUFFER_SAMPLES = CHUNK_SIZE*2
BUFFER_SIZE    = BUFFER_SAMPLES * CHANNELS * BITS/8
Default
End
End Select

' derron's part:
Global Source:TAudioSample=LoadAudioSample("test.ogg")
Global Buffer:TAudioSample = CreateAudioSample(BUFFER_SIZE/2, HERTZ, FORMAT)
Local fa_sound:Byte Ptr = fa_CreateSound( BUFFER_SIZE/2, BITS, CHANNELS, HERTZ, Buffer.Samples, $80000000 )
Local sound:TSound = TFreeAudioSound.CreateWithSound( fa_sound, Null)

' now put 40msec Latency into the Buffer:
SendOneChunk()
SendOneChunk()

'already start playback with an empty buffer
PlaySound sound
WriteTime=MilliSecs()

Repeat
Cls
If WriteTime<MilliSecs()
' this cares about really 20msec timing:
WriteTime=WriteTime + CHUNK_TIME
Print "Write to " + WritePointer + " at time: " + WriteTime
SendOneChunk
EndIf
Flip 0
Until AppTerminate()
End

Function SendOneChunk()
' put a amount of sampls into the buffer:
For Local i:Int = 0 Until CHUNK_SIZE
Buffer.samples[WritePointer+i] = Source.Samples[ReadPointer]
ReadPointer=ReadPointer+1
Next
WritePointer=(WritePointer + CHUNK_SIZE) Mod BUFFER_SIZE
End Function
     

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

        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:

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) Select
SuperStrict

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

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

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

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

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

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


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


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


' now put 40msec Latency into the Buffer:
SendOneChunk()
SendOneChunk()

'already start playback with an empty buffer
PlaySound sound
WriteTime=MilliSecs()
Local StartTime%=MilliSecs()
Local z1$,z2$,z3$
Repeat
Cls
DrawText "Move the mouse to pan between left and right",100,100
DrawText "LEFT",10,300
DrawText "MIDDLE",300,300
DrawText "RIGHT",700,300
DrawTabText "Write to:", WritePointer  , 300,400
DrawTabText "at time:", (WriteTime-StartTime) , 300,430
DrawTabText "read from:", ReadPointer , 300,460

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


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

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

' mouse volume:
If i Mod 4=0 Or FORMAT=SF_MONO16LE
value=value*volume
Else
value=value*(1-volume)
EndIf
If value<0
value=value+65535
EndIf

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


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) Select
Graphics 800,600
Local Drivername$[9]
Local Nr%
For Local a:String = EachIn AudioDrivers()
Print "Use Nr=" + Nr + " to use  Driver: " + a
Drivername[Nr]=a
Nr=Nr+1
Next

'*** Select driver here:***********

Nr=1

'*********************************
SetAudioDriver DriverName[Nr]

Global Test:TSound=LoadSound("Test_5_Clicks.ogg",SOUND_LOOP)
PlaySound Test
Global StartTime%=MilliSecs()
Global LastLatency%, Avarage%, Rounds%
Repeat
Cls
SetColor 255,255,255
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
DrawText "Checking audio driver: " + DriverName[Nr] ,200,70
DrawText "1. Listen to the ticks",200,90
DrawText "2. Feel the timing...",200,110
DrawText "3. Exactly at the 5th push LEFT MOUSE or the Key [X]",200,130
DrawText "RESULTS:",200,200
DrawText "   Last Lastency = " + LastLatency + " msec",200,230
If Rounds>0
DrawText "Avarage Lastency = " + Int(Avarage/rounds)+ " msec",200,250
EndIf
DrawText "click here:",200,370
DrawText "Press [R] to reset values",500,570
DrawRect 200,400,100,100
SetColor 1,1,1
DrawRect 201,401,98,98
If MouseDown(1)
Print latency(MilliSecs()-StartTime)
Repeat
Until MouseDown(1)=False
EndIf

If KeyDown(KEY_X)
Print latency(MilliSecs()-StartTime)
Repeat
Until KeyDown(KEY_X)=0
ElseIf KeyDown(KEY_R)
Rounds=0
Avarage=0
LastLatency=0
Repeat
Until KeyDown(KEY_X)=0
EndIf
Flip 0
Until AppTerminate()

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

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
Quote from: iWasAdam on March 25, 2021, 11:25:31
...One the ring buffer is set up latency should be the same across different devices...

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

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

by the way...

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

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
Quote from: Midimaster on March 25, 2021, 09:55:07
It would be time to check, which driver causes which latency. Latency is the time the driver needs to really bring the sound to the speakers.

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

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

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


bye
Ron
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...

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


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

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


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

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


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

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

bye
Ron
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
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) Select

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

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

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




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

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

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

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

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

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


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


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

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



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

Global LastLatency%, Avarage%, Rounds%


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

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




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

End


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

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

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

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

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

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




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


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

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


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


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

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


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


bye
Ron
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) Select
RingBufferClass.SetDriver("FreeAudio DirectSound")
Global RingBuffer:RingBufferClass=New RingBufferClass
RingBuffer.CreateNew(Format, Hertz)
SendSize= RingBuffer.HowManySamples()

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

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


feed it with sound:
Code (BlitzMax) Select

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

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

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

Lets have a look into the Class

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

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

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

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

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


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

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

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


Ready to run example

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

Code (BlitzMax) Select
Global SendSize% , ReadPointer%

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

Const SEND_TIME:Int = 20 'msec

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



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

Local WriteTime%=MilliSecs()


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

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


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


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

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

'_________________________________________________________________________________

Type RingBufferClass
Global MyDriver$

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

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

Field InBuffer:TAudioSample


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


Method CreateNew(Format%, Hertz%)
If MyDriver=""
Notify "No AudioDriver selected"
End
EndIf
Self.HERTZ=Hertz
Self.FORMAT=Format
DefineBuffer
RingPointer=(3*CHUNK_SIZE)
WatchTime=MilliSecs()
PlaySound Sound
End Method


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


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


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

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

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


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

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

Else
Local Maxi% = BUFFER_SIZE-WritePointerMod

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


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

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

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


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


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

...

End method


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

Yes,  I see what you mean.

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

This will make that a lot easier. 
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Baggey on March 28, 2021, 11:37:44
Quote from: iWasAdam on March 25, 2021, 10:44:12
@Derron - cant you just dump the steaming pile that is soloud in the bin where it belongs?
soloud is just a whole load of wrappers around another set of stuff NONE OF IT DOCUMENTED OR HAVING ANY NG EXAMPLES

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

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

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


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


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

Love that comment!  ;) :))

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

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

Watching this thread with interest!

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

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

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

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

Kind Regards Baggey
Title: Re: A new Audio-Out Approach in BlitzMax PortAudio
Post by: Midimaster on March 28, 2021, 12:50:11
Quote from: Baggey on March 28, 2021, 11:37:44
...
As Ring buffer is what i think i need to access for my Blitz Max Spectrum emulator to play sound properl'y. Writing sample's in realtime as they occur from the OUT Function.
...
Look forward to using something like this but it will need samples and an easy way to setup in BlitzMax.
...
Kind Regards Baggey

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

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

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

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

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

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


This is theoretic! At the moment I have not implemented SF_MONO8 as Format and the use of the DELAY is not very elegant. This would interrupt the emulator for a to long time.
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) Select

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

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

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


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

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

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

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

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


Now start two Threads:

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

Code (BlitzMax) Select

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

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

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



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

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



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

Code (BlitzMax) Select

SuperStrict

Global SendSize% , ReadPointer%

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

Global  SEND_TIME:Int, raus%=0

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

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

Global WriteTime%=MilliSecs()

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

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


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

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

Flip 0
Until AppTerminate()
End


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


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


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

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

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

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

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

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


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


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


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

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


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


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

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


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


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

InBufferStream.Seek WritePointerMod

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

Else
Local Maxi% = BUFFER_SIZE-WritePointerMod

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


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


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








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) Select
RingBuffer.CreateNew(Source.Format, 22000, 16) 


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

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

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


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


Perhaps this is a Windows version behavior? And on your computer it is not necessary?
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....


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


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


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

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

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

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



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


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


bye
Ron



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:
ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)

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



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


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



Title: Re: A new Audio-Out Approach in BlitzMax FreeAudio RingBuffer
Post by: Derron on March 30, 2021, 10:02:18
Quote from: Midimaster on March 30, 2021, 09:39:42
this works for 16bit:
ReadPointer=(ReadPointer+SendSize*2) Mod ((Source.length-SendSize)*2)

ReadPointer describes where to read from "source" ?

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

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

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


bye
Ron
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) Select
RingBuffer.Send AudioArray
Your amount of sending can vary, but in total you should keep an eye on the optimal amount.



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



The INT-Array or the SINGLE VALUE contains one sample value in one cell.
If you send 8bit-Samples the values need to be
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) Select
SuperStrict
Type RingBufferClass
' A permant running Audio-Output using FreeAudio
' --------------------------------------------------
' Author: Midimaster at www.midimaster.com
' V1.3.2  2021-08-09
' see examples of use at https://www.syntaxbomb.com/index.php/topic,8377.0.html
' This is Public Domain
' -------------------------------------------------------------------
' History:
' 1.3
'     added a ShowPercent() function
'     added WITH_PROTECTION flag default=ON
'     added REPEAT_LAST_SAMPLE flag default=OFF
'     speed improvements
'
' 1.2
'     added a SendOne() function
'     added a Buffer Overruns protection
'     now with internal thread
' -------------------------------------------------------------------
' minimal example:
'   RingBufferClass.SetDriver("FreeAudio....")
'   Global RingBuffer:RingBufferClass = New RingBufferClass
'   RingBuffer.CreateNew(HERTZ, FORMAT)
'   SendSize:INT  = RingBuffer.IntervalSamples()
'   SendTime:INT = RingBuffer.IntervalTime()
'   Global WriteTime = MilliSecs()
'   Function SendSamples
' If WriteTime>MilliSecs() Return
' WriteTime = WriteTime + SendTime
' Local AudioArray:Int[SendSize]
' For Local i:Int=0 To SendSize-1
' AudioArray[i] = any value....
' Next
' RingBuffer.Send AudioArray
'   End Function

Global MyDriver$
Global BufferMutex:TMutex=CreateMutex()

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

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

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


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

End Function



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

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



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


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



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



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

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



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


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

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



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


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

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

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


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

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






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

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





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






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

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

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


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


End Type





Some examples will follow...

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

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

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

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




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


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

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

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


Global Arc1:Double,  Arc2:Double

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

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




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


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

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

RingBuffer.CreateNew(12000, SF_MONO16LE)

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

Global WriteTime:Int = MilliSecs()

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

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



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


more code sample will follow... see next post!
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) Select
SuperStrict
Import "RingBufferClass.bmx"

Graphics 800,600

RingBufferClass.SetDriver("FreeAudio DirectSound")
Global RingBuffer:RingBufferClass = New RingBufferClass
RingBuffer.CreateNew(50000, SF_MONO8)

Global A:Byte, B:Byte=1, C:Byte, D:Byte=0, E:Byte=0
Global IY:Int

' Music Data
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]
Print( "DATA LENGTH: "+Len(data) )

CALL_37596()

Repeat
Delay 5
Until AppTerminate()
End



Function CALL_37596:Int()
' simulation of an old Z80 machine code:
While True
A = data[IY]
If A=255
Return True
End If
C = A 
B = 0
A = 0
D = data[IY+1]
E = data[IY+2]
Repeat             
Repeat                     
OUT_254(254,A)
D :- 1
If D=0
D = data[IY+1]
A = A~24 
End If
E :- 1
If E=0
E = data[IY+2]                       
End If
B :- 1 
Until B=0
C :- 1
Until C=0
IY :+ 3
Print "Step in DATA=" +  iy
Wend
End Function


Function OUT_254(Port%, Value%)
Select Value & 8
Case 0
RingBuffer.SendONE 50
Case 8
RingBuffer.SendONE 200
End Select
End Function


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) Select

SuperStrict
Import "RingBufferClass.bmx"
RingBufferClass.SetDriver("FreeAudio DirectSound")
Global RingBuffer:RingBufferClass = New RingBufferClass
RingBuffer.CreateNew(50000, SF_MONO8)
' thats all

'now send sample values:
Global Audio:TAudioSample = .....
For local i%=0 to ...
      RingBuffer.SendONE Audio.Samples[i]
Next


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
Quote from: 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.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) Select
RingBuffer.Send AudioArray
Your amount of sending can vary, but in total you should keep an eye on the optimal amount.



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



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

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

  • unsigned-SHORT

  • signed-SHORT

  • signed-INTEGER


So here is the Class: RingBufferClass.bmx for BlitzMax NG and BlitzMax 1.50
Code (BlitzMax) Select
SuperStrict
Type RingBufferClass
' *** RingBufferClass.bmx  ***
' A permant running Audio-Output using FreeAudio
' --------------------------------------------------
' Author: Midimaster at www.midimaster.com
' V1.2 2021-07-15
' see examples of use at https://www.syntaxbomb.com/index.php/topic,8377.0.html
' -------------------------------------------------------------------
' minimal example:
'   Import "RingBufferClassFinal.bmx"
'   RingBufferClass.SetDriver("FreeAudio....")
'   Global RingBuffer:RingBufferClass = New RingBufferClass
'   RingBuffer.CreateNew(HERTZ, FORMAT)
'   SendSize:INT  = RingBuffer.IntervalSamples()
'   SendTime:INT = RingBuffer.IntervalTime()
'   Global WriteTime = MilliSecs()
'   Function SendSamples
' If WriteTime>MilliSecs() Return
' WriteTime =WriteTime + SendTime
' Local AudioArray:Int[SendSize]
' For Local i:Int=0 To SendSize-1
' AudioArray[i] = any value....
' Next
' RingBuffer.Send AudioArray
'   End Function

Global MyDriver$
Global BufferMutex:TMutex=CreateMutex()

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

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


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



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



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


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





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

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



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


'
'   E N D   O F   T H E   P U B L I C   F U N C T I O N S
'
' ***************************************************************************
'
'    I N T E R N A L   F U N C T I O N S :
?bmxng
Private
?
Method Watch()
' PUBLIC: Use this to...
' inform how many samples you should send each call
If WatchTime<MilliSecs()
WatchTime = WatchTime + CHUNK_TIME
SendOneChunk
EndIf
End Method



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


Method DefineBuffer(Latency:Int)
CHUNK_TIME=20
BITS=16
CHANNELS=2
CHUNK_SIZE     = HERTZ * CHUNK_TIME * CHANNELS * BITS/8/1000
BUFFER_SAMPLES = 4*Latency * HERTZ * CHUNK_TIME             /1000
BUFFER_SIZE    = BUFFER_SAMPLES * BITS/8 *CHANNELS

RingBuffer     = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)
InBuffer       = CreateAudioSample(BUFFER_SAMPLES, HERTZ, FORMAT)

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

Sound          = TFreeAudioSound.CreateWithSound( fa_sound, Null)
RingStream     = CreateRamStream(RingBuffer.Samples , Buffer_size,True,True)
InBufferStream = CreateRamStream(InBuffer.Samples,Buffer_size,True,True)
RingPointer    =  BUFFER_SIZE/4
End Method


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


Method ClearBuffer()
For Local i:Int =0 To BUFFER_SIZE
RingBuffer.Samples[i]=0
Next
End Method


Method Transfer(ShortArray:Short[])
LockMutex BufferMutex
Local WritePointerMod:Int = WritePointer Mod BUFFER_SIZE
InBufferStream.Seek WritePointerMod
If ShortArray.Length*2 + (WritePointerMod) <= BUFFER_SIZE
For Local i:Int=0 To ShortArray.Length-1
InBufferStream.WriteShort ShortArray[i]
Next
Else
Local Maxi:Int = BUFFER_SIZE-WritePointerMod

For Local i:Int=0 To Maxi/2-1
InBufferStream.WriteShort ShortArray[i]
Next
InBufferStream.Seek 0
For Local i:Int=Maxi/2 To ShortArray.Length-1
InBufferStream.WriteShort ShortArray[i]
Next
EndIf
WritePointer=WritePointer + ShortArray.Length*2

UnlockMutex BufferMutex
End Method


Method CheckBufferOverRun()
' private  cares about buffer overruns and report if you send to fast
Local grade:Int
Local diff:Int=WritePointer-Readpointer
diff=diff*100/BUFFER_SIZE

If diff>80
Print "RINGBUFFER: Prevent Buffer Overrun! Wait for " + IntervalTime() + "msec"

Delay IntervalTime()
'Print WritePointer + " " + Readpointer + " " + (WritePointer-Readpointer) + " " + BUFFER_SIZE
'Print diff + ":Int"
EndIf
End Method
End Type



Some examples will follow...

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

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

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

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




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


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

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

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


Global Arc1:Double,  Arc2:Double

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

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




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


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

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

RingBuffer.CreateNew(12000, SF_MONO16LE)

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

Global WriteTime:Int = MilliSecs()

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

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



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


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

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"

QuoteFeel 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
Quote from: 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.

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.


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

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?

Quote128 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

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

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

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

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:
QuoteI 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) Select
Global RingBuffer:RingBufferClass = New RingBufferClass
....
Print "Level of ring buffer = " + RingBuffer.ShowPercent + "%"



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) Select
Global RingBuffer:RingBufferClass = New RingBufferClass
RingBuffer.WITH_PROTECTION=0



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) Select
Method Transfer(ShortArray:Short[])
LockMutex BufferMutex
Local WritePointerMod:Int =  WritePointer Mod BUFFER_SIZE
If (ShortArray.Length + WritePointerMod) <= BUFFER_SIZE
For Local i:Int=0 To ShortArray.Length-1
InPtrShort[i+WritePointerMod] = ShortArray[i]
Next
Else
Local Maxi:Int = BUFFER_SIZE-WritePointerMod
For Local i:Int=0 To Maxi-1
InPtrShort[i+WritePointerMod] = ShortArray[i]
Next
For Local i:Int=Maxi To ShortArray.Length-1
'InPtrShort[i]=ShortArray[i+Maxi]   ' <-------  **** HERE ****
InPtrShort[i-Maxi]=ShortArray[i]
Next
EndIf
WritePointer=WritePointer + ShortArray.Length
UnlockMutex BufferMutex
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) Select
Method SendOne(Value:Int)
' PUBLIC: Use this to...
' send one single sample value to the ringbuffer
'Global ShortArray:Short[100]            <-------  **** HERE ****
Global ShortArray:Short[CHUNK_SIZE]
    Global Counter:Int

Select InFormat
....
End Select
'If Counter = 100                            <-------  **** HERE ****
If Counter = CHUNK_SIZE
CheckBufferOverRun
Transfer ShortArray
          Counter=0
EndIf
End Method




The Updated Source Code
... is here:
https://www.syntaxbomb.com/index.php/topic,8377.msg347049187.html#msg347049187