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

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

Previous topic - Next topic

Midimaster

#60
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
...back from Egypt

Midimaster

#61
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.



...back from Egypt

Baggey

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
Running a PC that just Aint fast enough!? i7 Quad core 16GB ram 1TB SSD and NVIDIA Quadro K620 . DID Technology stop! Or have we been assimulated!

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

Baggey

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
Running a PC that just Aint fast enough!? i7 Quad core 16GB ram 1TB SSD and NVIDIA Quadro K620 . DID Technology stop! Or have we been assimulated!

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

Midimaster

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

...back from Egypt

iWasAdam

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

iWasAdam

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


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

iWasAdam

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

iWasAdam

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.

iWasAdam

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?

iWasAdam

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

Midimaster

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




...back from Egypt

iWasAdam

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

Baggey

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
Running a PC that just Aint fast enough!? i7 Quad core 16GB ram 1TB SSD and NVIDIA Quadro K620 . DID Technology stop! Or have we been assimulated!

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

Midimaster

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.

...back from Egypt