Play Samples in a Queue

Started by Krischan, October 06, 2017, 09:43:56

Previous topic - Next topic

Krischan

Hi, I have a small problem and could need a hint. I want to add already loaded Samples to a Queue, play this Queue and delete them from the Queue in a row after they have been played. Example:

Global sample:TSample
Global samplelist:TList = CreateList()
Global channel:TChannel

[Loop Begin]

   AddSample(1)
   AddSample(2)
   AddSample(3)
   AddSample(4)

   [...some code...]

   PlaySampleQueue()

[Loop End]

It is working so far but the problem is: because I'm using a "Repeat...Delay 1...Until Not ChannelPlaying(channel)" algorithm, the window freezes while a sample is playing. Any ideas for a more elegant solution? If it is possible I don't want to rely on external DLLs, just vanilla Blitzmax.

' ----------------------------------------------------------------------------
' Queued Sample Type
' ----------------------------------------------------------------------------
Type TSample

Field id:Int

' create an object
Method New(id:Int)

Self.id = id
ListAddLast(samplelist, Self)

End Method

' destroy an object
Method Delete()

ListRemove(samplelist, Self)

End Method

End Type

' ----------------------------------------------------------------------------
' Adds a sample to the Sample playing Queue
' ----------------------------------------------------------------------------
Function AddSample(id:Int)

Local s:TSample = New TSample
s.id = id

End Function



' ----------------------------------------------------------------------------
' Plays the Sample Queue
' ----------------------------------------------------------------------------
Function PlaySampleQueue()

For Local s:TSample = EachIn samplelist

PlaySample(s.id)

Repeat

Delay 1

Until Not ChannelPlaying(channel)

s = Null

Next

ClearList(samplelist)

End Function

' ----------------------------------------------------------------------------
' plays a sample for a given ID
' ----------------------------------------------------------------------------
Function PlaySample(id:Int = 0)

If channel Then StopChannel(channel)

If id = 1 Then channel = CueSound(sounddrum)
If id = 2 Then channel = CueSound(soundcash)
If id = 3 Then channel = CueSound(soundhorn)
If id = 4 Then channel = CueSound(soundwhoo)

' play only if channel is free
If Not ChannelPlaying(channel) Then

ResumeChannel channel

EndIf

End Function
Kind regards
Krischan

Windows 10 Pro | i7 9700K@ 3.6GHz | RTX 2080 8GB]
Metaverse | Blitzbasic Archive | My Github projects

TomToad

Don't have time right now to give a thorough explanation, but briefly:

Look into using events and hooks.  Create a timer that fires say 60 times a second (more or less, balancing overall performance over how much delay you find acceptable between samples), then in the hook you can check if the sample is still playing or not.

Another thing to do, look into multithreading.  Have a separate thread monitor the sample, then have it trigger a callback when the sample is finished.
------------------------------------------------
8 rabbits equals 1 rabbyte.

GW

Whats wrong with managing the channel queue inside your main loop?  why the while loop? do you need precise timing? 
It seems from your description that this could be handled in the main loop


pseudo:
If channel playing is false and queue list is not empty then pop the sample from a list and play it: do this every main loop cycle



Derron

#3
as GW said: check if your channel is still playing something - and if not handle that accordingly (play next sample).




BUT you will surely have some crackling if your samples are "fading into each other" as you might have some 1-2ms without playing something:


0ms: loop1: nothing played, start sample 1
...
after 100ms: loop90: still playing sample1 (2ms left to play, little hickup coming)
after 105ms: loop91: no longer playing (since 3ms there is nothing played) - start next sample
after 106ms: loop92: sample 2 is playing




This is one of the reasons why you should consider filling a kind of "streaming buffer" on your own.
I had some posts about it on the old bmax-site (with skidracer coding something).


The idea is to manually update a stream/buffer-object with your sample data. So eg. your buffer is 5 second long - then you only need to update it every eg. second and add the new data (partially on buffer parts "already played"). That buffer-object is then loaded as sound which is played repeatingly on a channel (so it runs forever and ever - until you manually stop it).
That updating should either be done manually (in your loop ,every x seconds) or - way better - in an extra thread.
Your benefit of "NG" is, that it is by default "threaded" (or was that planned...hmm at least it will be that way somewhen).
So you might think of handling that updates in "C" meanwhile (starting a thread on app start - waiting for things to do).


There is no "sample" yet but the code itself might be still bring up some new ideas for you:
https://github.com/GWRon/Dig/blob/master/base.sfx.soundstream.bmx




If a simple "crossfader" is of interest for you, have a look there:
https://github.com/GWRon/Dig/blob/master/base.sfx.soundmanager.rtaudio.bmx
(relies on rtAudio and its ogg-streaming-capabilities - there is a soLoud and "alsa" variant too)






bye
Ron

Krischan

Ok thanks, with your help I think I found a simple solution: added a "active" field to the type to flag the current sample and altered the PlaySampleQueue Function which is called every EVENT_TIMERTICK. Now I can add tons of (voice) Samples and they play step by step without any further delays for the program, great!

' ----------------------------------------------------------------------------
' Plays the Sample Queue
' ----------------------------------------------------------------------------
Function PlaySampleQueue()

For Local s:TSample = EachIn samplelist

If (Not ChannelPlaying(channel)) And s.active = False Then

s.active = True
PlaySample(s.id)

Else If (Not ChannelPlaying(channel)) And s.active = True Then

ListRemove(samplelist, s)
s = Null

EndIf

Next

End Function


' ----------------------------------------------------------------------------
' Queued Sample Type
' ----------------------------------------------------------------------------
Type TSample

Field id:Int
Field active:Int

' create an object
Method New(id:Int)

Self.id = id
ListAddLast(samplelist, Self)

End Method

' destroy an object
Method Delete()

ListRemove(samplelist, Self)

End Method

End Type
Kind regards
Krischan

Windows 10 Pro | i7 9700K@ 3.6GHz | RTX 2080 8GB]
Metaverse | Blitzbasic Archive | My Github projects

Derron

Dunno if "vanilla" optimizes it, but


ListRemove(list, entry)
could be replaced with
list.Remove(entry)


It saves the wrapping convencience (and procedural-style) function call.






Question:

   ' destroy an object
   Method Delete()
      ListRemove(samplelist, Self)
   End Method

Should this say "remove from list when setting the entry to null" ?
If yes: "Delete()" will only be called if it can get garbage collected. To be garbage-collectable it must not be referenced from other objects. In that case the TList contains a "TLink" which contains a reference to the TSample-entry.
Means, Delete() should never get called - at least I think it should behave that way.


As you call "ListRemove" in your "PlaySampleQueue()" that will do already.


bye
Ron

Krischan

Thanks for the hint. The problem is that I still have problems with memory leaks and don't fully understand the concept behind freeing objects, sometimes it works and sometimes it doesn't and so I'm fireing with all guns on these helpless objects :P Is there any good tutorial around?
Kind regards
Krischan

Windows 10 Pro | i7 9700K@ 3.6GHz | RTX 2080 8GB]
Metaverse | Blitzbasic Archive | My Github projects

Derron

You do not need a tutorial:
- if you reference an object. make sure to dereference it when trying to get rid of it (aka garbage collected)
- if you do circular references, manage them correctly


I somehow assume you have circular references in your code.
car.onStreet = street
street.cars:TList = {..., car}


if you now remove "car" from steet.cars (via "street.cars.remove(car)") and then remove the "street" via "street = null" you created a memory leak as "car.onStreet" is still keeping "street" alive (via "onStreet"-referencing the "street" object).


GC can never run successfully on that object as it cannot access "car" anymore (variable is nulled, street.cars is also no longer existing/accessible).




In BlitzMax you cannot have "weak links" so you might decouple things and use "collections". So instead of storing "car" in "street.cars" you:
- have car.ID (a unique ID of course)
- have a collection of cars ("CarsCollection") with "CarsCollection.Get(ID)" returning the car-object (or null if not existing)
- have street.cars containing IDs rather than the objects


This way you could also get rid of "car.onStreet" as you just store "streetID" rather than the "street"-object within your car-object: "car.onStreetID = street.ID"


This means neither "car" nor "street" should really be needed to know the other object-type/class at all. If they do not need to "do" something with the other class/type then they just save the "onStreetID" or "cars" so others (with more knowledge) could handle that.
Imagine you have "person" - a class knowing about streets and cars (it imports the cars.bmx and streets.bmx). They could retrieve the car object from the CarCollection which has the ID of the first car on the street the person is just walking on. This car object then could be used like "car.Enter(person.ID)" (see - another decoupling - alternatively "person" must extend from a basic "entity"-class which the cars.bmx could import too, in that case "car.enter(person)" would work without trouble).


Disadvantage of this decoupling is, that you always need to ask the collection for the object (at least once per "do something with it").


bye
Ron

Krischan

Thanks for the tips, I will consider this in the future.

Another question about memory: as far as I've learned from old threads in the Blitzbasic Archives and my own observations it seems that there is no memory leak in my code. But - I am not sure about that. I always watched the memory usage of the task in the Task Manager and noticed that the values only increase there but never decrease after freeing objects.

So I've created a test for myself to learn about freeing memory. In this example I use a simple Type field and fill it with a lot of random numbers in Long format and free it again, and repeat it two more times to see if the peak increases or not (it doesn't). Then I've added a complex function I found to retrieve the approximate memory consumption values from the Windows Task Manager. You can see that the memory becomes free within Blitzmax according to GCMemAlloced() function but stays at 500MB according to the Task Manager (and that's what I see in the Task Manager, too).

So the conclusion is that memory gets allocated in Windows but never freed again like it reserves it for later use? But why is that? And why reports Blitzmax a peak at 360MB while the Task Manager reports 500MB at the same time? I don't understand that.

SuperStrict

Framework brl.basic
Import brl.retro
Import brl.eventqueue
Import brl.timer
Import maxgui.drivers

Extern "Win32"

  Function GetCurrentProcess()
  Function LoadLibraryW(Name:Short Ptr)
  Function GetProcAddress:Byte Ptr(Lib:Int, Name:Byte Ptr)

EndExtern

Local Lib:Int = LoadLibraryW ("Psapi.dll".ToWString ())
Global GetProcessMemoryInfo (Process:Int, MemCounters:Byte Ptr, size:Int) = GetProcAddress (Lib:Int, "GetProcessMemoryInfo".ToCString ())

Type TProcessMemoryCounters

  Field size:Int
  Field PageFaultCounter:Int
  Field PeakWorkingSetSize:Int
  Field WorkingSetSize:Int
  Field QuotaPeakPagedPoolUsage:Int
  Field QuotaPagedPoolUsage:Int
  Field QuotaPeakNonPagedPoolUsage:Int
  Field QuotaNonPagedPoolUsage:Int
  Field PagefileUsage:Int
  Field PeakPagefileUsage:Int
 
End Type

Type TObject

Field obj:Double

Method New()

ListAddLast(list, Self)

End Method

End Type


Const width:Int = 640
Const Height:Int = 400
Const Objects:Int = 10000000

Global window:TGadget = CreateWindow("Test", DesktopWidth() / 2 - width / 2, DesktopHeight() / 2 - height / 2, width, height)
Global winlog:TGadget = CreateTextArea(0, 0, width - 24, height - 80, window)

Global list:TList = CreateList()
Global timer:TTimer = CreateTimer(1)
Global peak:Int

Global PMC:TProcessMemoryCounters = New TProcessMemoryCounters

' forced clean memory before first run
GCCollect()
MemoryUsage()

' first run
MemTest()
MemoryUsage()

' second run
MemTest()
MemoryUsage()

' third run
MemTest()
MemoryUsage()

' after the test runs monitor usage once per second
While True

WaitEvent()

Select EventID()

Case EVENT_WINDOWCLOSE
End

Case EVENT_APPTERMINATE
End

Case EVENT_TIMERTICK
MemoryUsage()

End Select

Wend

End

' Memory fill and free test
Function MemTest()

For Local i:Int = 0 To Objects

Local o:TObject = New TObject
o.obj = Rand(2 ^ 63)

If GCMemAlloced() > peak Then peak = GCMemAlloced()

If i Mod 100000 = 0 Then

AddTextAreaText(winlog, "Added 100.000 Objects " + FormatINT(GCMemAlloced()) + "~n")
RedrawGadget(Window)

EndIf

Next

Local i:Int = 0

For Local o:TObject = EachIn list

ListRemove(list, o)
o = Null

i:+1

If i Mod 100000 = 0 Then

AddTextAreaText(winlog, "Freeing 100.000 Objects " + FormatINT(GCMemAlloced()) + "~n")
RedrawGadget(Window)

EndIf

Next

i = Null
ClearList(list)

End Function

' output an int with dividing dots
Function FormatINT:String(Value:String, div:String = ".")

Local i:Int
Local out:String
Local Length:Int = Len(Value)

For i = Length - 1 To 0 Step - 1

out = Mid(Value, 1 + i, 1) + out
If i And Not ((Length - i) Mod 3) Then out = div + out

Next

Return out

End Function

' output memory usage
Function MemoryUsage:Int()

Local taskmem:String = Formatint(GetTaskMem())
Local peakmem:String = FormatINT(peak)
Local out:String = "Blitzmax Memory used: " + FormatINT(GCMemAlloced()) + " | Task Manager Memory used: " + taskmem + " | Peak: " + peakmem + "~n"

AddTextAreaText(winlog, out)
SetStatusText(Window, out)

End Function

' get memory usage of current task from task manager
Function GetTaskMem:Int()

PMC.size = SizeOf TProcessMemoryCounters
GetProcessMemoryInfo (GetCurrentProcess () , PMC, SizeOf TProcessMemoryCounters)

Return PMC.WorkingSetSize

End Function
Kind regards
Krischan

Windows 10 Pro | i7 9700K@ 3.6GHz | RTX 2080 8GB]
Metaverse | Blitzbasic Archive | My Github projects

Derron

Using RAM isn't as bad as it sounds. On Linux it is nothing to worry if eg. programs are kept in RAM even if not running - as long as there is free RAM available and no swapping is needed everything is ok.


So maybe Windows keeps the RAM reserved for your program - until another program needs more memory.




Another thing to consider - and way more likely - is that the occupied memory does _not_ correspond to the memory the garbage collector of your programm "manages". If your application loads some libraries, then they occupy RAM - memory you can not "garbage collect". You only can collect your custom created data (your "blitzmax objects").


Which is why a "hello world" occupies more than the memory needed to hold (string)"hello world".




bye
Ron