Guide to playing sounds?

Started by Yellownakji, August 08, 2018, 22:32:45

Previous topic - Next topic

Yellownakji

So, i've been reading through the manual with no luck...  I could use a pointer.

I have:

MySound:Tsound
MyChannel:Tchannel

I tried loading a .WAV and a .OGG from the root folder as well as loading a .OGG from binary, which is what i need to do in the end.   I did double check and save the bank...  it's a valid OGG.

--

What i did:

MySound = Loadsound(music.wav)

if keyhit(key_4)
MyChannel = Playsound(MySound)
end if

--

Returns invalid or null object exception?

Krischan

Use Cuesound and Resumechannel.

Code (Blitzmax) Select
SuperStrict

Import brl.DirectSoundAudio

Global VOLUME:Int = 50
Global CHANNEL:TChannel = AllocChannel()

Graphics 800, 600

Local SAMPLE:TSound = LoadSound("voc/Welcome Commander.ogg")

While Not AppTerminate()

If KeyHit(KEY_ESCAPE) Then End

If KeyHit(KEY_4) Then PlaySample(SAMPLE)

Wend

End

Function PlaySample(sample:TSound)

If ChannelPlaying(CHANNEL) Then StopChannel(CHANNEL)
CHANNEL = CueSound(SAMPLE)
SetChannelVolume(CHANNEL, VOLUME / 100.0)
ResumeChannel CHANNEL

End Function
Kind regards
Krischan

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

fielder

#2
Quote from: Yellownakji on August 08, 2018, 22:32:45
MySound = Loadsound(music.wav)
Whatttt? :)

on Windows:  (making WAV inside the EXE when compiled.. better to use OGG)

Incbin "music.wav"
SetAudioDriver("DirectSound")
music:TSound=LoadSound("incbin::music.wav")
PlaySound music
delay(5000)

Yellownakji

#3
Quote from: fielder on August 09, 2018, 07:00:43
Quote from: Yellownakji on August 08, 2018, 22:32:45
MySound = Loadsound(music.wav)
Whatttt? :)

on Windows:  (making WAV inside the EXE when compiled.. better to use OGG)

Incbin "music.wav"
SetAudioDriver("DirectSound")
music:TSound=LoadSound("incbin::music.wav")
PlaySound music
delay(5000)



Uhh... no.  I'm not going to INCBIN a WAV.   They're way to big;  I don't want to have it decompress into RAM then have to be loaded again for an Audio.   That's why i made my own binary format to begin with..  |  I am not sure what the "delay" part of the code if for either as i already have my own precise delta timer.

--

Anyways, i didn't get to update yesterday but i managed to work it out.

--

Import BRL.Audio

global MySound:Tsound = Loadsound("music.ogg") 'Actually loads a TBank in my code tho because reading from my binary.
global MyChannel:Tchannel = AllocChannel()  (Thanks Krischan.. didn't realize this bit)
MyChannel = Playsound(Mysound)
Mychannel.setvolume(0.02) '0.02 because this F***** is loud.

--

Thanks.

Derron

@ allocchannel

Code (BlitzMax) Select

#PlaySound starts a sound playing through an audio channel.
If no @channel is specified, #PlaySound automatically allocates a channel for you.
end rem
Function PlaySound:TChannel( sound:TSound,channel:TChannel=Null )
Return sound.Play( channel )
End Function


-> AllocChannel creates a channel (if possible)
-> calling "MyChannel = PlaySound(Mysound)" will create another channel
-> calling "PlaySound(Mysound, Mychannel)" will use the "Mychannel" (if not null)


Means: no need to use "AllocChannel" except you want to reuse specific ones (two music channels for crossfading + some sfx channels).



@ volume
Maybe you should consider normalizing your audio files in advance.


bye
Ron

Yellownakji


Krischan

Let me explain: I had a nasty bug playing queued samples and without the initial Allocchannel for the master Channel the code always ran into a crash later. In the case described here it could be obsolete to use it.
Kind regards
Krischan

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

Derron

Quote from: Yellownakji on August 09, 2018, 20:52:13
Typical Derron..  ::)
? I think I was less "elaborative"/"outreaching" than in other cases... so not typical for me ;-)


Quote from: Krischan on August 09, 2018, 23:55:38
Let me explain: I had a nasty bug playing queued samples and without the initial Allocchannel for the master Channel the code always ran into a crash later. In the case described here it could be obsolete to use it.
This sounds like a different issue.
As said: "Allocchannel" (tries to) pre-allocates a new TChannel for you. PlaySound() accepts a second (optional) parameter which defines whether to (try to) create a new TChannel or use the given one in the second parameter.

Now let's assume you always do this:
bla = AllocChannel()
sound = PlaySound(file)

This means you always do this:
- try to allocate a channel
- allocate another channel and play a sound on it


I found out some time ago, that there is some kind of "channel limitation", means you cannot eg. play 100 simultaneous things (music + individual explosion sounds of 99 dying space ships after an Area-of-Effect-damage). So maybe your "Alloc Channel" somehow overcomes the issue (returning another channel).
I cannot remember if the application crashed or if other sounds stopped playing or the new sound did not play ... I assume it crashed as I else did not have written a "Channel Pool" taking care of this issue and leading to "stop playing an old sound".

Your "AllocChannel()" might return "null" - did you check for this?


Nonetheless (and months ago I "promoted" it somewhere else already (blitzmax.com?)):

Source:
https://github.com/GWRon/Dig/blob/master/base.sfx.channelpool.bmx
Example:
https://github.com/GWRon/Dig/blob/master/samples/channelpool/channelpool.bmx

In essence it does nothing more than "cycling" through a limited amount of channels to avoid reaching the artificial limit for channels (they _should_ be software mixed but hmm hmm). Is the pool-limit reached, it will use one of the previously used ones. The more audio you play, the less it will be recognizeable if one of the channel's audio is stopped/replaced.

Benefit of the class is, that you can protect certain channels  (eg. background music, a channel playing a dialogue's voice-over, ...) and you can easily "name" channels ("bullets", "voiceover", ...) as you know best, what sounds can get muted if you play a new one (have "bullets1" and "bullets2" and you achieve to at least hear two different bullets at the same time).


People who had issues with TChannel:
http://mojolabs.nz/posts.php?topic=104159
http://mojolabs.nz/posts.php?topic=103890
http://mojolabs.nz/posts.php?topic=107675


Might not 100% be related to it - but maybe it helps.


bye
Ron

Krischan

#8
I don't want to hijack this thread but the initial question has been answered already, so Derron here is my Samplequeue code. The goal was to load a bunch of Samples in a more human readable way using TMaps with a text key for each sample, add them to a playing queue TList and play them one by one until the queue is empty and the ability to play a "unqueued" sample instantaneously if needed. Keys 1-3 add samples to the queue and SPACE plays the unexpected sample.

It's not the most elegant way to do this but it's working like I need it - as long as I don't uncomment the Allocchannel in the Globals :-) From my point of view I'm always using a single channel only with Cuesound/Resumesound and need to allocate it first or there is no channel at all, right?

Code (Blitzmax) Select
SuperStrict

Import brl.DirectSoundAudio

' Types
Global SAMPLE:TSample = New TSample
Global QUEUE:TSamplequeue = New TSamplequeue

' Sound Globals
Global CHANNEL:TChannel = AllocChannel()
Global SAMPLEMAP:TMap = CreateMap()
Global SAMPLEQUEUE:TList = CreateList()
Global VOLUME:Int = 50

Graphics 800, 600

' Load and add Samples to TMap
SAMPLE.AddSample(LoadSound("test/Welcome Commander.ogg"), "WELCOME")
SAMPLE.AddSample(LoadSound("test/Scan completed.ogg"), "COMPLETE")
SAMPLE.AddSample(LoadSound("test/Terraformable.ogg"), "TERRAFORM")

While Not AppTerminate()

Cls

If KeyHit(KEY_ESCAPE) Then End

' Add Samples to Queue
If KeyHit(KEY_1) Then QUEUE.AddQueue("WELCOME")
If KeyHit(KEY_2) Then QUEUE.AddQueue("COMPLETE")
If KeyHit(KEY_3) Then QUEUE.AddQueue("TERRAFORM")

If KeyHit(KEY_SPACE) Then QUEUE.PlaySample("WELCOME")

' queue handling, check for objects and play them one by one
QUEUE.PlayQueue()

' output survey
DrawText "Elements in Queue: " + SAMPLEQUEUE.count(), 0, 0
Local i:Int = 0
For Local s:TSampleQueue = EachIn SAMPLEQUEUE

If i = 0 Then

DrawText "Playing Sample: " + s.SAMPLE.desc, 0, 15

Else

DrawText "Queued Sample #" + i + ": " + s.SAMPLE.desc, 0, 15 + (i * 15)

EndIf

i:+1

Next

Flip

Wend

End

' ----------------------------------------------------------------------------
' Single Sample
' ----------------------------------------------------------------------------
Type TSample

Field sound:TSound
Field desc:String

Method AddSample(sound:TSound, desc:String)

Local s:TSample = New TSample
s.sound = sound
s.desc = desc

MapInsert(SAMPLEMAP, desc, s)

End Method

End Type


' ----------------------------------------------------------------------------
' Queued Sample Type
' ----------------------------------------------------------------------------
Type TSampleQueue

Field sample:TSample
Field active:Int


' ----------------------------------------------------------------------------
' Adds a sample to the Sample playing Queue
' ----------------------------------------------------------------------------
Method AddQueue(desc:String)

Local sample:TSample = TSample(MapValueForKey(SAMPLEMAP, desc))

If TSample(sample) Then

Local s:TSampleQueue = New TSampleQueue
s.sample = sample

ListAddLast(SAMPLEQUEUE, s)

Print "Sample added to Queue: " + desc

EndIf

End Method

' ----------------------------------------------------------------------------
' Plays the Sample Queue
' ----------------------------------------------------------------------------
Method PlayQueue()

For Local s:TSampleQueue = EachIn SAMPLEQUEUE

If TSample(s.sample) Then

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

s.active = True
PlayQueueSample(s.sample)

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

ListRemove(SAMPLEQUEUE, s)
s = Null

EndIf

EndIf

Next

End Method


' ----------------------------------------------------------------------------
' plays a single sample on demand, instanteneous
' ----------------------------------------------------------------------------
Method PlaySample(desc:String)

Local s:TSample = TSample(MapValueForKey(SAMPLEMAP, desc))

If TSample(s) Then

Print "Play Single Sample: " + desc

If ChannelPlaying(CHANNEL) Then StopChannel(CHANNEL)

CHANNEL = CueSound(s.sound)

' set channelvolume
SetChannelVolume(CHANNEL, VOLUME / 100.0)
ResumeChannel CHANNEL

EndIf

End Method



' ----------------------------------------------------------------------------
' plays a single queue sample
' ----------------------------------------------------------------------------
Method PlayQueueSample(s:TSample)

Print "Play Queue Sample: " + s.desc

If ChannelPlaying(CHANNEL) Then StopChannel(CHANNEL)

CHANNEL = CueSound(s.sound)

' set channelvolume
SetChannelVolume(CHANNEL, VOLUME / 100.0)
ResumeChannel CHANNEL

End Method

End Type
Kind regards
Krischan

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

Derron

#9
You do not hijack the thread - as you seem (at least in my opinion) to do it wrong as yellownakji does.


Code (BlitzMax) Select

Rem
bbdoc: Cue a sound
returns: An audio channel object
about:
Prepares a sound for playback through an audio channel.
To actually start the sound, you must use #ResumeChannel.
If no @channel is specified, #CueSound automatically allocates a channel for you.
#CueSound allows you to setup various audio channel states such as volume, pan, depth
and rate before a sound actually starts playing.
End Rem
Function CueSound:TChannel( sound:TSound,channel:TChannel=Null )
Return sound.Cue( channel )
End Function


You have a global "CHANNEL:TChannel". You check if that plays and stop that if it is playing.
Then you use "CueSound" without passing a "channel to use" in the second param. So "CueSound" creates a _new_ channel and returns this then. So "CHANNEL = CueSound(bla)" assigns a _new_ channel to the global one.

You will not hear differences, as you always stop a previously playing channel. Nonetheless you create a new channel each time.

You could easily append "CHANNEL" to the CueSound/PlaySound ... commands to stay with the exact same channel. "CueSound(sound, CHANNEL)"

If you do not believe me: you could always print the memory address of CHANNEL by
print string(CHANNEL)
or
print CHANNEL.ToString()

If it was the same CHANNEL each time, the memory address wont change, else it will change on each "CueSound()".


Your "Queue" might have a use - but most of the time you want an immediate sound effect ("explosion") not an "wait until explosion 1 finished"-thing.
What happens if you added a repeating-sound? the channel will never be stopping.


Side annotation:
Code (BlitzMax) Select

               For Local s:TSampleQueue = EachIn SAMPLEQUEUE
                               
                        If TSample(s.sample) Then
               
                                If (Not ChannelPlaying(CHANNEL)) And s.active = False Then
                               
                                        s.active = True
                                        PlayQueueSample(s.sample)
                                       
                                Else If (Not ChannelPlaying(CHANNEL)) And s.active = True Then
                               
                                        ListRemove(SAMPLEQUEUE, s)
                                        s = Null
               
                                EndIf
                               
                        EndIf
                Next

- you do 2 times check if the channel is active
- once you play a sample you continue checking others in the queue
- you set "s = Null" - a local variable which is no longer used in that for-loop
- active is set to true, but never to false!?


so it could be shortened to:
Code (BlitzMax) Select

               If not ChannelPlaying(CHANNEL)
                        For Local s:TSampleQueue = EachIn SAMPLEQUEUE
                                 If not s.sample then continue 'skip
                                 If s.active = False
                                        s.active = True
                                        PlayQueueSample(s.sample)
                                Else
                                        ListRemove(SAMPLEQUEUE, s)
                                EndIf
                                exit 'handled, finish the for-loop
                        Next
               Endif

Now this exposes that your code does something wrong - or misses to do something.
- if the channel is not playing then "ListRemove" is called on each PlayQueue() command
- active stays "true" all the time


Either you truncated your code - or some parts do not work as intented (or just "by luck") - or I am not understanding what is intented to happen.


bye
Ron

Krischan

Thanks Derron - I've missed the Cuesound Channel option, damn. Like I said it isn't the most elegant solution but it worked and I had a lot of problems to get it running - I couldn't find any example like this so I had to try it on my own - how would you solve the quest?

It's a tool for Explorers in Elite Dangerous which adds a voice assistant by scanning the game's Journal logs and gives audible feedback to the player. In my use case I need to play Samples in a Queue (or call it a stack) as new events can happen while a sample is playing and I need to queue them that no event gets missing in the meantime which is different from a game with shooting sounds and explosions. I've split up the spoken text in different elements to re-arrange the parts later using this method. But I don't have looped samples there so this is not a problem.

The nullified s was garbage I forgot to clean.  I've added your suggestions and split the channel in two to play the non-queued samples in a second channel (for example a click sound when you press a button).

Code (Blitzmax) Select
SuperStrict

Import brl.DirectSoundAudio

' Types
Global SAMPLE:TSample = New TSample
Global QUEUE:TSamplequeue = New TSamplequeue

' Sound Globals
Global MASTERCHANNEL:TChannel = AllocChannel()
Global QUEUECHANNEL:TChannel = AllocChannel()
Global SAMPLEMAP:TMap = CreateMap()
Global SAMPLEQUEUE:TList = CreateList()
Global MASTERVOLUME:Int = 100
Global QUEUEVOLUME:Int = 50

Graphics 800, 600

' Load and add Samples to TMap
SAMPLE.AddSample(LoadSound("test/Welcome Commander.ogg"), "WELCOME")
SAMPLE.AddSample(LoadSound("test/Scan completed.ogg"), "COMPLETE")
SAMPLE.AddSample(LoadSound("test/Terraformable.ogg"), "TERRAFORM")
SAMPLE.AddSample(LoadSound("test/Click.ogg"), "CLICK")

While Not AppTerminate()

Cls

If KeyHit(KEY_ESCAPE) Then End

' Add Samples to Queue
If KeyHit(KEY_1) Then QUEUE.AddQueue("WELCOME")
If KeyHit(KEY_2) Then QUEUE.AddQueue("COMPLETE")
If KeyHit(KEY_3) Then QUEUE.AddQueue("TERRAFORM")

If KeyHit(KEY_SPACE) Then QUEUE.PlaySample("CLICK")

' queue handling, check for objects and play them one by one
QUEUE.PlayQueue()

' output survey
DrawText "Elements in Queue: " + SAMPLEQUEUE.count(), 0, 0
Local i:Int = 0
For Local s:TSampleQueue = EachIn SAMPLEQUEUE

If i = 0 Then

DrawText "Playing Sample: " + s.SAMPLE.desc, 0, 15

Else

DrawText "Queued Sample #" + i + ": " + s.SAMPLE.desc, 0, 15 + (i * 15)

EndIf

i:+1

Next

Flip

Wend

End

' ----------------------------------------------------------------------------
' Single Sample
' ----------------------------------------------------------------------------
Type TSample

Field sound:TSound
Field desc:String

Method AddSample(sound:TSound, desc:String)

Local s:TSample = New TSample
s.sound = sound
s.desc = desc

MapInsert(SAMPLEMAP, desc, s)

End Method

End Type


' ----------------------------------------------------------------------------
' Queued Sample Type
' ----------------------------------------------------------------------------
Type TSampleQueue

Field sample:TSample
Field active:Int


' ----------------------------------------------------------------------------
' Adds a sample to the Sample playing Queue
' ----------------------------------------------------------------------------
Method AddQueue(desc:String)

Local sample:TSample = TSample(MapValueForKey(SAMPLEMAP, desc))

If TSample(SAMPLE) Then

Local s:TSampleQueue = New TSampleQueue
s.SAMPLE = SAMPLE

ListAddLast(SAMPLEQUEUE, s)

Print "Sample added to Queue: " + desc

EndIf

End Method

' ----------------------------------------------------------------------------
' Plays the Sample Queue
' ----------------------------------------------------------------------------
Method PlayQueue()

If Not ChannelPlaying(QUEUECHANNEL) Then

For Local s:TSampleQueue = EachIn SAMPLEQUEUE

If Not s.SAMPLE Then Continue

If s.active = False Then

s.active = True

PlayQueueSample(s.SAMPLE)

Else

ListRemove(SAMPLEQUEUE, s)

EndIf

Exit

Next

EndIf

End Method


' ----------------------------------------------------------------------------
' plays a single sample on demand, instanteneous
' ----------------------------------------------------------------------------
Method PlaySample(desc:String)

Local s:TSample = TSample(MapValueForKey(SAMPLEMAP, desc))

If TSample(s) Then

Print "Play Single Sample: " + desc

If ChannelPlaying(MASTERCHANNEL) Then StopChannel(MASTERCHANNEL)

CueSound(s.sound, MASTERCHANNEL)

' set channelvolume
SetChannelVolume(MASTERCHANNEL, MASTERVOLUME / 100.0)
ResumeChannel MASTERCHANNEL

EndIf

End Method



' ----------------------------------------------------------------------------
' plays a single queue sample
' ----------------------------------------------------------------------------
Method PlayQueueSample(s:TSample)

Print "Play Queue Sample: " + s.desc

If ChannelPlaying(QUEUECHANNEL) Then StopChannel(QUEUECHANNEL)

CueSound(s.sound, QUEUECHANNEL)

' set channelvolume
SetChannelVolume(QUEUECHANNEL, QUEUEVOLUME / 100.0)
ResumeChannel QUEUECHANNEL

End Method

End Type
Kind regards
Krischan

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

Derron

#11
Pay attention to "concurrent list modification" - not an issue for everybody, but it happened multiple times in my game TVTower, so Brucey gave me a little clap ... and I created an array holding "no longer valid objects" which I then remove _after_ iterating over the list.
Why? we discussed this here in the forums already - and BlitzMax NG TList already contains some fixes but for vanilla it might happen that if you iterate over a list - and then remove something within this iteration, then it might skip items as the iterator borks up.
In our case here, this means that it might skip a valid entry (it removed one, iterator borks up and jumps over the "previously next item").


Also you have it defined as "method" while you only access external variables ... so it is a "function" ...
if you used it as "method" you could have "QUEUECHANNEL" and "SAMPLEQUEUES" be fields/globals of the class.
TSampleQueue.queues is then a list containing all "TSampleQueue" instances and so on.

Means instead of "SAMPLEQUEUES:TList" you would access "TSampleQueue.queues".
PlayQueue would need to run the current "default queue" - or you need to pass it what queue to handle...
... but to not overly complicate stuff for now I will concentrate on the content of "PlayQueue":

Code (BlitzMax) Select

       Method PlayQueue()
                If ChannelPlaying(QUEUECHANNEL) Then return 'nothing to do
               
                'remove old queue items
                local toRemove:TSampleQueue[]
                For Local s:TSampleQueue = EachIn SAMPLEQUEUE
                        If s.active then toRemove :+ [s]
                Next
                For Local s:TSampleQueue = EachIn toRemove
                        SAMPLEQUEUE.Remove(s)
                Next
             
                'enqueue next possible sample in the queue
                For Local s:TSampleQueue = EachIn SAMPLEQUEUE
                        If Not s.SAMPLE Then Continue
                               
                        PlayQueueSample(s.SAMPLE)
                        s.active = True
                        Exit
                Next
        End Method


And it might even be better to have "s.active = True" in the method actually playing "s". That way you ensure to have it "true" as soon as it get played, not just when "PlayQueue" handles it.


bye
Ron