Loupe

[C#] Créer un jeu avec FMOD – Bases du jeu et analyse spectrale du signal

 

Bonjour à tous et bienvenue dans cette série d’articles consacrées à FMOD et son utilisation dans une application Windows Store.

Le but de ces articles sera de créer un petit jeu en utilisant quelques fonctionnalités de la librairie FMOD. Nous utiliserons pour cela le langage C#/XAML avec MVVM Light.

Partie 1 - Introduction et mise en place

Partie 2 – Bases du jeu et analyse spectrale du signal

Partie 3 – Spectrogramme et plateau de jeu

Principe du jeu

Pour ce deuxième article, on va s’intéresser à notre nouveau but ultime : faire un jeu !

Le but sera très simple : le joueur charge sa chanson préférée et la joue. En analysant la chanson en temps réel, des monstres apparaitront à différents endroits en fonction de différents paramètres (ex: basse, medium, aiguë). Chaque monstre ira de la gauche vers la droite de l’écran (pour les sons sortant du canal gauche) et inversement pour les sons sortant du canal droit. Le but sera donc de les tuer (en cliquant dessus) avant qu’ils n’atteignent le milieu de l’écran.

Chargement d’une chanson

On commence par la base, le chargement et la lecture de la chanson désirée par le joueur. Dans notre jeu, on se limitera au format mp3 qui est le plus courant. On reprend le projet précédent et on va le modifier comme suit :

MainViewModel.cs
private bool _isPlaying;
public bool IsPlaying
{
    get { return _isPlaying; }
    set
    {
        _isPlaying = value;
        RaisePropertyChanged("IsPlaying");
        PlaySoundCommand.RaiseCanExecuteChanged();
        StopSoundCommand.RaiseCanExecuteChanged();
    }
}

private StorageFile _currentSong;
public StorageFile CurrentSong
{
    get { return _currentSong; }
    set
    {
        _currentSong = value;
        RaisePropertyChanged("CurrentSong");
    }
}

private RelayCommand _loadSoundCommand;
public RelayCommand LoadSoundCommand
{
    get
    {
        if (_loadSoundCommand == null)
            _loadSoundCommand = new RelayCommand(LoadSound);
        return _loadSoundCommand;
    }
}

private RelayCommand _playSoundCommand;
public RelayCommand PlaySoundCommand
{
    get
    {
        if (_playSoundCommand == null)
            _playSoundCommand = new RelayCommand(PlaySound, CanPlaySound);
        return _playSoundCommand;
    }
}

private RelayCommand _stopSoundCommand;
public RelayCommand StopSoundCommand
{
    get
    {
        if (_stopSoundCommand == null)
            _stopSoundCommand = new RelayCommand(StopSound, CanStopSound);
        return _stopSoundCommand;
    }
}

public async void LoadSound()
{
    _system.update();
    FileOpenPicker openPicker = new FileOpenPicker();
    openPicker.ViewMode = PickerViewMode.List;
    openPicker.FileTypeFilter.Add(".mp3");
    openPicker.SuggestedStartLocation = PickerLocationId.MusicLibrary;

    CurrentSong = await openPicker.PickSingleFileAsync();
    if (CurrentSong != null)
    {
        try
        {
            using(var stream = await CurrentSong.OpenAsync(FileAccessMode.Read))
	   {
            byte[] buffer = new byte[stream.Size];
            await stream.ReadAsync(buffer.AsBuffer(), (uint)buffer.Length, InputStreamOptions.None);
            await Task.Yield();
                    
            _soundInfo = new CREATESOUNDEXINFO { length = (uint)buffer.Length };
            _soundInfo.cbsize = Marshal.SizeOf(_soundInfo);
            if ((_result = _system.createSound(buffer, MODE.SOFTWARE | MODE.CREATESAMPLE | MODE.OPENMEMORY,
					 ref _soundInfo, ref _sound)) != RESULT.OK)
                return;

    	   }
            PlaySoundCommand.RaiseCanExecuteChanged();
        }
        catch (FileNotFoundException)
        {
            MessageDialog md = new MessageDialog("File not found", "Error");
            md.ShowAsync();
        }
    }
}

public bool CanPlaySound()
{
    return _sound != null && !IsPlaying;
}

public void PlaySound()
{
    IsPlaying = true;
    if (_channel == null)
    {
        if ((_result = _system.playSound(CHANNELINDEX.FREE, _sound, false, ref _channel)) != RESULT.OK)
            return;
    }
    else
    {
        if ((_result = _system.playSound(CHANNELINDEX.REUSE, _sound, false, ref _channel)) != RESULT.OK)
            return;
    }
}

public bool CanStopSound()
{
    return IsPlaying;
}

public void StopSound()
{
    if (_channel != null)
    {
        IsPlaying = false;
        _channel.stop();
    }
}

Pour le chargement de la chanson, on utilise le FileOpenPicker avec quelques options comme les filtrage du type de fichier, le dossier de départ, … On récupère en sortie un StorageFile qui nous donne accès au fichier. Il ne nous reste plus qu’à récupérer les données du fichier sous forme de tableau de byte afin de pouvoir créer un nouveau son avec FMOD. En comparaison à l’article précédent, nous avons besoin d’un CREATESOUNDEXINFO afin de renseigner quelques informations indispensable à la création (la taille du flux, la taille de la structure). On notera l’utilisation de nouveaux flags MODE.CREATESAMPLE | MODE.OPENMEMORY indiquant qu’il s’agit de la création d’un son grâce à des données brutes.

Quelques changements aussi du côté de notre vue :

MainPage.xaml
<Border BorderThickness="2"
        BorderBrush="Orange"
        Background="Gray"
        CornerRadius="0,0,10,10"
        Margin="20,-2,0,0">
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center">
            <Button Content="Charger..."
                    Command="{Binding LoadSoundCommand}"
                    Width="150"
                    Margin="0,0,50,0" />
            <Button Content="Jouer"
                    Command="{Binding PlaySoundCommand}"
                    Visibility="{Binding IsPlaying, Converter={StaticResource BooleanToVisibilityConverter},
				 ConverterParameter=True}"
                    Width="150" />
            <Button Content="Arrêter"
                    Command="{Binding StopSoundCommand}"
                    Visibility="{Binding IsPlaying, Converter={StaticResource BooleanToVisibilityConverter}}"
                    Width="150" />
        </StackPanel>

        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    Visibility="{Binding IsPlaying, Converter={StaticResource BooleanToVisibilityConverter}}">
            <TextBlock Text="En cours : " />
            <TextBlock Text="{Binding CurrentSong.DisplayName}"
                        Margin="2,0,0,0" />
        </StackPanel>
    </StackPanel>
</Border>

On peut à présent, charger une chanson, la jouer, la stopper et afficher son nom. On passe à l’analyse maintenant !

Analyse spectrale

Il est temps maintenant d’analyser la chanson pour voir quel type de son est joué à l’instant t. Pour cela, nous allons utiliser l’analyse spectrale du signal et FMOD va bien nous aider grâce à la méthode getSpectrum(). Le principe est le suivant, à chaque intervalle de temps (50ms dans notre cas) nous récupèrerons le spectre, le normaliserons puis l’analyserons.

Récupération du spectre

MainViewModel.cs
public const int SPECTRUM_SIZE = 512;
public float[] TotalSpectrum { get; set; }
public float[] LeftSpectrum { get; set; }
public float[] RightSpectrum { get; set; }

public async void Analyse()
{
    while (IsPlaying)
    {
        _system.update();

        _channel.getSpectrum(LeftSpectrum, SPECTRUM_SIZE, 0, DSP_FFT_WINDOW.RECT);
        _channel.getSpectrum(RightSpectrum, SPECTRUM_SIZE, 1, DSP_FFT_WINDOW.RECT);

        // Get max spectrum value
        float maxSpectrum = float.NegativeInfinity;
        for (int i = 0; i < SPECTRUM_SIZE; i++)
        {
            TotalSpectrum[i] = (LeftSpectrum[i] + RightSpectrum[i]) / 2;
            if (TotalSpectrum[i] > maxSpectrum)
                maxSpectrum = TotalSpectrum[i];
        }

        for (int i = 0; i < SPECTRUM_SIZE; i++)
        {
            LeftSpectrum[i] = LeftSpectrum[i] / maxSpectrum;
            RightSpectrum[i] = RightSpectrum[i] / maxSpectrum;
            TotalSpectrum[i] = TotalSpectrum[i] / maxSpectrum;
        }

        await Task.Delay(50);
    }
}

Dans un premier temps, on récupère les spectres des canaux gauche et droit (pour un son stéréo par exemple). Ce spectre de fréquence sera composé de 512 valeurs. Très grossièrement, les 100 premières valeurs du tableau pourrait composer la plage des graves, les médiums entre 100 et 200, les aigües entre 200 et 512.

Ensuite, il convient de normaliser les résultats afin de faciliter le traitement. Cela consiste à dire que la plus grande valeur sera 1 et pour l’obtenir il suffit de diviser chaque valeur du tableau par la plus grande valeur trouvée.

Nous sommes enfin prêt à exploiter nos spectres afin de trouver quelles plages sont présentes à l’instant t et à placer les monstres en fonction des résultats.

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus