SFML community forums

Help => Audio => Topic started by: sima on November 06, 2021, 03:18:06 pm

Title: SFML no sound playing with soundbuffers stored in ResourceHolder
Post by: sima on November 06, 2021, 03:18:06 pm
Hi,

i'm currently working on a small game with sfml.

For resource loading and holding i'm using the ResourceHolder described in the SFML Game
Development Book: https://github.com/SFML/SFML-Game-Development-Book/blob/master/02_Resources/Include/Book/ResourceHolder.hpp (https://github.com/SFML/SFML-Game-Development-Book/blob/master/02_Resources/Include/Book/ResourceHolder.hpp)

Inside a SoundManager class i'm loading different sounds to this ResourceHolder. This SoundManager has a playSound function when i want to play a sound.

    void SoundManager::playSound(SoundType soundToPlay) {
        auto buffer = sounds.get(soundToPlay);
        auto sound = sf::Sound(buffer);
        sound.play();
    }

but i don't hear any sounds. Do i have to store different sound objects for every sound in a map? I also tried to use a sf::Sound object as a class member, but this also didn't worked.

Using this ResourceHolder my textures are loaded correctly.

Thank you for you help
Title: Re: SFML no sound playing with soundbuffers stored in ResourceHolder
Post by: kojack on November 06, 2021, 08:21:33 pm
Your playSound function creates a Sound object, but it is a local variable. So when the end of playSound is reached, the Sound object goes out of scope and is deleted. The destructor of Sound stops the sound from playing.
You need to keep the Sound object alive for it to keep playing the sound.
Title: Re: SFML no sound playing with soundbuffers stored in ResourceHolder
Post by: sima on November 07, 2021, 09:05:07 am
Thanks for your reply.
I tried to use a private class member sf::Sound sound but this also didn't worked.
Currently i have the following structure:
In the class header file:
class SoundManager {
    private:

        res::ResourceHolder<sf::SoundBuffer, SoundType> soundBuffers;
        sf::Music music;
        sf::Sound sound;

        std::map<SoundType, std::string> soundFiles = {
                {SoundType::JUMP,   "../resources/sounds/jump.ogg"},
                [...]
        };


then in a load function all of my sounds are loaded into the soundBuffers:
bool SoundManager::load() {

        for (auto[k, v]: soundFiles) {
            try {
                soundBuffers.load(k, v);

            } catch (std::runtime_error &e) {
                return false;
            }
        }
        return true;
    }


    void SoundManager::playSound(SoundType soundToPlay) {
        auto buffer = soundBuffers.get(soundToPlay);
        auto soundOption = soundOptions[soundToPlay];
        sound.setLoop(soundOption.loop);
        sound.setVolume(soundOption.volume);

        std::cout << "Play sound";
        sound.play();

    }


And i checked everything, my load function is called and the playSound function is called.
Title: Re: SFML no sound playing with soundbuffers stored in ResourceHolder
Post by: kojack on November 07, 2021, 10:18:46 am
The new one has two bugs:
- buffer isn't assigned to the sound object (you'll need a sound.setBuffer(buffer) in there when it starts playing)
- auto buffer = soundBuffers.get(soundToPlay); will cause a similar scope destruction issue as before

For the second one, soundBuffers.get(soundToPlay) will return a reference to the SoundBuffer object. But the auto keyword ignores references. So buffer will be a SoundBuffer (a clone of the one in the resourceholder), not a reference to one. This clone is a local variable, and will go out of scope at the end of the function. SoundBuffers, on destruction, remove themselves from any Sound object playing them.
Changing the line to auto &buffer = soundBuffers.get(soundToPlay); should fix it. (So buffer will be a reference to a SoundBuffer that keeps existing)

Although another problem will arise. You need one Sound object for every simultaneous playing sound. If your manager has just the one Sound member, you can only play one sound at a time.

In my own sound manager, I create a new Sound object every time I play a sound. I store it in a collection. Every frame I scan the collection to find all sounds that have finished playing, and delete them.

It's a bit crappy, but it works for what I needed.
sound_manager.h:
#pragma once
#include <SFML/Audio.hpp>

namespace kage
{
        namespace SoundManager
        {
                sf::Sound *playSound(const std::string &filename);
                void update();
                void preload(const std::string& filename);
        }
}
 

sound_manager.cpp
#include <map>
#include <string>
#include "kage/sound_manager.h"


namespace kage
{
        namespace SoundManager
        {
                std::map<std::string, sf::SoundBuffer*>& getSoundBuffers()
                {
                        static std::map<std::string, sf::SoundBuffer*> s_soundBuffers;
                        return s_soundBuffers;
                }
               
                std::vector<sf::Sound*>& getSounds()
                {
                        static std::vector<sf::Sound*> s_sounds;
                        return s_sounds;
                }

                sf::Sound *playSound(const std::string &filename)
                {
                        sf::SoundBuffer *sb;
                        std::map<std::string, sf::SoundBuffer *>::iterator it = getSoundBuffers().find(filename);
                        if (it == getSoundBuffers().end())
                        {
                                sb = new sf::SoundBuffer;

                                if (!sb->loadFromFile(filename))
                                {
                                        delete sb;
                                        return 0;
                                }
                                getSoundBuffers()[filename] = sb;
                        }
                        else
                        {
                                sb = it->second;
                        }

                        sf::Sound *s = new sf::Sound(*sb);
                        s->play();
                        return s;
                }

                void update()
                {
                        for (int i = 0; i < getSounds().size(); ++i)
                        {
                                if (getSounds()[i]->getStatus() == sf::SoundSource::Stopped)
                                {
                                        delete getSounds()[i];
                                        getSounds().erase(getSounds().begin() + i);
                                        --i;
                                }
                        }
                }

                void preload(const std::string& filename)
                {
                        sf::SoundBuffer* sb;
                        std::map<std::string, sf::SoundBuffer*>::iterator it = getSoundBuffers().find(filename);
                        if (it == getSoundBuffers().end())
                        {
                                sb = new sf::SoundBuffer;

                                if (!sb->loadFromFile(filename))
                                {
                                        delete sb;
                                        return;
                                }
                                getSoundBuffers()[filename] = sb;
                        }
                }
        }
}

(Before anybody mentions, yes, preload and playSound duplicate a big chunk of code. I added preload later and didn't get around to refactoring the code)

When a non-looping sound has a status of sf::SoundSource::Stopped, it has finished playing and can be removed. Or you could use a pool system, reuse sounds that have stopped instead of making new ones.

This is an older version, my newer code uses the Entt (Entity Component System) to handle sounds and check if they are playing. But that's still a work in progress, and would be confusing if you aren't familiar with Entt (it's awesome, works great with SFML).
Title: Re: SFML no sound playing with soundbuffers stored in ResourceHolder
Post by: sima on November 07, 2021, 12:15:04 pm
Thank you!
I didn't see that I returned a copy in the ResourceHolder instead of the reference.
I could build it now with unique_ptr.
Title: Re: SFML no sound playing with soundbuffers stored in ResourceHolder
Post by: kojack on November 07, 2021, 01:50:21 pm
Cool. :)

The reference/copy thing is something to be careful of in other parts of SFML too, such as Texture and Sprite (which work a bit like SoundBuffer and Sound).