2017-12-21 14:47:39

Hi folks, especially programmers,
I have a small debate yesterday with my friend about programming.
We have discussed many topics, but one interested me much. So, to introduce our conflict, read this piece of c++ code:

//Code #1
#include <bgt_kit/bgt_kit.hpp>
timer steptimer;
int steptime=250;
sound step;

void gameloop()
{
while (true)
{
if (key_pressed(KEY_W) and steptimer.elapsed()>=steptime)
{
step.load("Sounds/step"+std::to_string(random(1, 3))+".ogg");
step.play();

steptimer.restart();
}
Sleep(100);
}
}

int main()
{
bgt_kit_init();
gameloop();
}

This is how I manage step sounds now, I saw it in many example codes for bgt, so considered it as normal. My friend however was shocked, saying it is inefficient and harsh for harddrives.
After bit of discussion, I thought out this modification:

//Code #2
#include <bgt_kit/bgt_kit.hpp>
timer steptimer;
int steptime=250;
std::unordered_map<std::string, sound> audiomanager;

void gameloop()
{
while (true)
{
if (key_pressed(KEY_W) and steptimer.elapsed()>=steptime)
{
audiomanager["Sounds/step"+std::to_string(random(1, 3))+".ogg"].play();

steptimer.restart();
}
Sleep(100);
}
}

void load_sounds()
{
std::vector<std::string> files=find_files("Sounds/");
for (int i=0;i<files.size();i++) {
audiomanager["sounds/"+files[i]]=sound();
audiomanager["Sounds/"+files[i]].load("Sounds/"+files[i]);
}
}

int main()
{
bgt_kit_init();
load_sounds();
gameloop();
}

Then I realized, that this solution does not allow playing one sound more times, bad for sounds of steps. So I thought out a newone, in this form:

//Code #3
#include <bgt_kit/bgt_kit.hpp>
timer steptimer;
int steptime=250;
sound step;
std::unordered_map<std::string, std::string> audiostorage;

void gameloop()
{
while (true)
{
if (key_pressed(KEY_W) and steptimer.elapsed()>=steptime)
{
step.load_from_memory(audiostorage["Sounds/step"+std::to_string(random(1, 3))+".ogg"]);
step.play();

steptimer.restart();
}
Sleep(100);
}
}

void load_sounds()
{
std::vector<std::string> files=find_files("Sounds/");
for (int i=0;i<files.size();i++) {
file reader;
reader.open("Sounds/"+files[i], "rb");
audiostorage["sounds/"+files[i]]=reader.read();
reader.close();
}
}

int main()
{
bgt_kit_init();
load_sounds();
gameloop();
}

And one second after it I added one modification in Code #4, which I will not write here, because only thing changed is, that method load_from_memory does not grab string as a parameter, but pointer to its beginning and its length, so there is no need for copy full data twice - onetime to the load-from_memory method, second to the sound object by that method.

So, what do you think? Which code piece is best to use? Does loading of all sounds to the memory make sense, or it is premature optimization?
I would be happy to hear your opinions.

Best regards

Rastislav

Thumbs up

2017-12-21 16:53:57

Especially when your game will grow, unloading and reloading sounds in quick succession is very inefficient. Therefor what I usually do (in python, bgt automatically knows when you try to load the same sound more then once, so this won't apply to bgt):
Check if the sound is present in a dictionary of all sounds loaded,
if present, set the libaudioverse buffer to the buffer in the dictionary
if not present, load the sound into the buffer dictionary and then set the buffer of the sound object to the buffer in the dictionary.
I personally find this a good compromise between optimization and loading times, when you preload all the sounds before a game starts it will take quite a while. It will also ensure that no sounds are loaded that aren't needed, if you have multiple levels for example. The only massive disadvantage to this system is that it's not comfortable to run on external drives: if your game does not need to load data from disk for a long time, the drive may stop spinning, and when an unloaded sound is needed the drive needs to spin back up again, which is especially annoying in multiplayer games. It might be a good idea to give the users a choice in the matter (if it's a large game). If you only have a couple of sounds, just preload them all.

Roel
golfing in the kitchen

Thumbs up

2017-12-21 18:29:17

Something to add to post 2: if you have long sounds, like music, that's more than 3 minutes long, best to stream it. Streaming goes at the speed of the drive it's getting the data from, but that's not much of an issue any more.

"On two occasions I have been asked [by members of Parliament!]: 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out ?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question."    — Charles Babbage.

Thumbs up

2017-12-23 01:54:57

If you are creating a BGT kit in C++, you should definitely consider implementing BGT's cloning feature as well as porting the sound_pool include class.

For games, you definitely need to make sure that all the sounds that are going to be needed in a specific level or area are loaded into memory and stay there for the entire duration of the game round before they are first played. Even mainstream games do that. Well, some of them load just the core and the rest is later loaded dynamically on demand, but the sounds do usually stay in memory afterwards anyway until the entire garbage from a finished game round is purged.

Especially in the blind community where many people are still using pretty outdated or low end hardware, and often mechanical drives that spin slower than they necessarily could have, it's always better to put some more stress on memory rather than on the CPU (if decoding the sounds on load) and hard drive.

Lukas

Thumbs up

2017-12-23 21:18:56

So probably Code #3 with some way to distinguish needed sounds?
@Lukas: what do you mean by 'BGT's cloning feature'?
Yeah, I am developing bgt kit, in each language which I am planning to seriously use, avoiding Python. In c++ it have lots of features, including Soundpool and phononpool, which is currently in progress to provide immersive hrtf with BGT like notation. That is btw also the reason why I am considering to preload sounds to the memory, it is needed to convert them into the raw data to work properly with Phonon, so this can save cpu performance used for conversions.

Greetings

Rastislav

Thumbs up

2017-12-23 22:18:45

Yeah, and this is similar to what the BGT cloning feature does. When you first load a given sound, it stores its metadata in memory somehow, don't ask me how exactly, so that every other time you load that same sound with the same name and characteristics, it remembers it and no longer reads the raw data from disk any more but rather only fetches the actual sound from its stored memory representation, no matter whether the newly loaded copy comes from the same or a different location on disk, from a pack file, from memory or anywhere else. However, for this to work properly, you need to keep at least one sound object in memory which stores that sound at all times. That's why the preload approach works well in conjunction with this feature to help prevent CPU or hard drive overload. As soon as you close the very last available sound object holding this very sound, BGT forgets that metadata in order to help save memory because it assumes this specific sound is no longer needed for now.

At the start of a game round in all of my projects, I always load every single sound I know I will need into an array of sound objects called preloads, and I only purge this array (resize it to 0) at the end of the game round, so I reliably know all these sounds will always be there no matter what. Then, I manipulate the actual individual in-game sounds through the sound pool and in no other way any more.

Hope this helps,
Lukas

Thumbs up

2017-12-26 02:52:34

The first time you load a sound, BGT decodes and decrypts the sound as necessary and stores it in memory. If you then create another sound object and load the same sound with it, BGT knows this is what you're doing and so you get a second sound object which simply points to the previously loaded sound. This way you don't reload sounds (ogg vorbis decoding, decryption and disk I/O are heavy duty tasks) and you don't double the amount of memory consumption.

Some ideas for doing this in C++:
Create a class which can store a pointer to some raw memory, the size of that memory buffer and an atomic reference counter.
Use an unordered map which stores references to instances of this object as values and sound file names as keys.
When load is called on a sound object, check to see if the provided filename has already been entered as a key into that dictionary. If it has, pull that instance and point your sound object's reader head at the already prepared audio bytes in that object. Otherwise, load the sound, create the preload object, add it to the map under the sound filename and point your sound object at the newly created data. Either way, atomically increment the preload object's reference counter.

When the sound is closed, decrement the preload object's atomic reference counter. If it reaches 0, remove it from the preload map and destroy it.
In BGT, if you load two different files containing the same audio data (even if they are  files with the same name in different locations), BGT loads them separately.
To do otherwise would require hashing or some other means of comparing the two files to insure they are the same, which would negate much of the performance gain earned by doing this in the first place.

Proud to be the official hosting provider for http://www.vgstorm.com!

Thumbs up

2017-12-27 00:40:08

@lukas

Why would you destroy your preload array at all? is it to save memory? If a program doesn't use specific resources for a long time, Windows will move it's data to the page file. So is there any advantage to do it that way?

Roel
golfing in the kitchen

Thumbs up

2017-12-28 03:41:50

Yeah, I basically do it that way just to save memory as soon as it's not needed, nothing else. One of my games has to use up an entire gig of RAM, so freeing it immediately rather than on exit or waiting for the system to do it on its own definitely makes a difference in that case. Plus, I'm not sure what conditions exactly have to be met for the system to consider the resources unused, and if this scenario is thus even applicable to most BGT games.Lukas

Thumbs up