Flux audio personnalisés
Un flux audio ? C'est quoi ça ?
Un flux (en anglais stream) audio est similaire à une musique (vous vous souvenez de la classe sf::Music
?). Il a
pratiquement les mêmes fonctions et se comporte de la même manière. La seule différence est qu'un flux audio ne jouera pas un fichier audio : il va
jouer une source audio perso que vous vous chargerez de fournir. En d'autres termes, définir votre propre flux audio vous permet de jouer
n'importe quoi d'autre qu'un fichier : un flux internet, une musique générée par votre programme, un format non supporté par SFML, etc.
En fait, la classe sf::Music
est simplement un type particulier de flux audio, spécialisé dans la lecture de fichiers.
Comme nous parlons ici de flux, nous aurons affaire à des données audio qui ne peuvent pas être complètement chargées en mémoire, mais
qui seront plutôt chargées au fur et à mesure qu'elles sont jouées. Si votre son peut être intégralement chargé et tenir en mémoire sans
problème, alors oubliez les flux audio : chargez le simplement dans un sf::SoundBuffer
et utilisez un sf::Sound
pour le jouer.
sf::SoundStream
Pour définir votre propre flux audio, vous devez hériter de la classe de base abstraite sf::SoundStream
. Celle-ci possède deux
fonctions virtuelles que vous devrez redéfinir : onGetData
et onSeek
.
class MyAudioStream : public sf::SoundStream
{
virtual bool onGetData(Chunk& data);
virtual void onSeek(sf::Time timeOffset);
};
onGetData
est appelée par la classe de base à chaque fois qu'elle est à court d'échantillons audio et a donc besoin de nouvelles
données à jouer. Vous devez lui fournir de nouveaux échantillons audio en remplissant le paramètre data
:
bool MyAudioStream::onGetData(Chunk& data)
{
data.samples = /* mettez ici un pointeur vers les nouvelles données audio */;
data.sampleCount = /* mettez ici le nombre d'échantillons pointés */;
return true;
}
Vous devez renvoyer true
lorsque tout est ok, ou false
s'il faut stopper le flux, soit parce qu'une erreur est
survenue, soit parce qu'il n'y a tout simplement plus de données audio à jouer.
SFML crée une copie interne des échantillons audio dès que onGetData
a fini, donc vous n'avez pas besoin de garder en mémoire les
données que vous renvoyez, elles peuvent être effacées immédiatement après.
onSeek
est exécutée lorsque la fonction publique setPlayingOffset
est appelée. Son but est de changer la position de lecture
courante dans la source de données. Le paramètre est une valeur de temps représentant la nouvelle position, relativement au début du son (et non à
la position courante). Cette fonction est parfois impossible à implémenter, par rapport au type du flux ; dans ce cas laissez-la vide et indiquez
aux utilisateurs de votre classe que changer la position de lecture n'est pas supporté.
Désormais votre classe est quasiment prête à fonctionner. La seule chose que sf::SoundStream
doit encore savoir, est le nombre
de canaux ainsi que le taux d'échantillonnage du flux, afin de pouvoir le restituer correctement. Pour transmettre ces paramètres à la classe de
base, vous devez appeler la fonction protégée initialize
dès que ceux-ci sont connus (donc très probablement lorsque le flux est
chargé/initialisé).
// l'endroit où ceci est fait, dépend complètement de comment votre classe est conçue
unsigned int channelCount = ...;
unsigned int sampleRate = ...;
initialize(channelCount, sampleRate);
Attention aux threads
Les flux audio étant toujours joués dans un thread, il est important de savoir ce qui se passe exactement, et où.
onSeek
est appelée directement par la fonction setPlayingOffset
, elle est donc toujours exécutée dans le thread qui
a appelé cette dernière. Par contre, la fonction onGetData
sera appelée regulièrement tant que le flux sera en train d'être joué,
dans un autre thread créé par SFML. En conséquence, si votre flux utilise des données qui peuvent être accédées de manière concurrente
(c'est-à-dire en même temps) à la fois par le thread appelant et par le thread de lecture (celui créé par SFML), vous devez les protéger (avec un mutex
ou autre) afin d'éviter les accès concurrents, qui pourraient causer des comportements indéterminés -- données audio corrompues, crashs, etc.
Si vous n'êtes pas suffisamment à l'aise avec les problèmes liés aux threads, vous pouvez faire un saut par le tutoriel correspondant.
Utilisation de votre flux audio
Maintenant que vous avez défini votre propre classe de flux audio, voyons comment l'utiliser. En fait, c'est très similaire à ce qui est expliqué
dans le tutoriel sur sf::Music. Vous pouvez
contrôler la lecture avec les fonctions play
, pause
, stop
et setPlayingOffset
. Vous pouvez aussi
jouer avec les propriétés du son, telles que le volume ou le pitch. Vous pouvez vous référer à la documentation de l'API ou aux autres tutoriels
audio pour plus de détails.
Un example simple
Voici un exemple très simple de classe de flux audio, qui joue les données d'un buffer audio. Une telle classe est bien entendu complètement inutile, mais le but ici est de se focaliser sur la manière dont les données audio sont gérées par la classe, peu importe d'où elles viennent.
#include <SFML/Audio.hpp>
#include <vector>
// flux audio qui joue un buffer déjà chargé
class MyStream : public sf::SoundStream
{
public:
void load(const sf::SoundBuffer& buffer)
{
// extraction des échantillons audio du buffer, vers notre propre conteneur
m_samples.assign(buffer.getSamples(), buffer.getSamples() + buffer.getSampleCount());
// remise à zéro de la position de lecture
m_currentSample = 0;
// initialisation de la classe de base
initialize(buffer.getChannelCount(), buffer.getSampleRate());
}
private:
virtual bool onGetData(Chunk& data)
{
// nombre d'échantillons à renvoyer à chaque fois que cette fonction est appelée ;
// dans une implémentation plus robuste, on utiliserait plutôt une durée fixe
// plutôt qu'un nombre arbitraire d'échantillons
const int samplesToStream = 50000;
// affectation des prochains échantillons à jouer
data.samples = &m_samples[m_currentSample];
// a-t-on atteint la fin du son ?
if (m_currentSample + samplesToStream <= m_samples.size())
{
// fin non atteinte : on envoie les échantillons et on continue
data.sampleCount = samplesToStream;
m_currentSample += samplesToStream;
return true;
}
else
{
// fin atteinte : on envoie ce qu'il reste d'échantillons et on stoppe la lecture
data.sampleCount = m_samples.size() - m_currentSample;
m_currentSample = m_samples.size();
return false;
}
}
virtual void onSeek(sf::Time timeOffset)
{
// calcul du numéro d'échantillon correspondant, en fonction du taux d'échantillonnage et du nombre de canaux
m_currentSample = static_cast<std::size_t>(timeOffset.asSeconds() * getSampleRate() * getChannelCount());
}
std::vector<sf::Int16> m_samples;
std::size_t m_currentSample;
};
int main()
{
// chargement d'un buffer audio à partir d'un fichier
sf::SoundBuffer buffer;
buffer.loadFromFile("sound.wav");
// initialisation et lecture de notre flux
MyStream stream;
stream.load(buffer);
stream.play();
// on le laisse jouer jusqu'à la fin
while (stream.getStatus() == MyStream::Playing)
sf::sleep(sf::seconds(0.1f));
return 0;
}