Partager via


Touch and Go

Diffusion audio en continu dans Windows Phone

Charles Petzold

Télécharger l'exemple de code

Charles PetzoldQu'ils soient exécutés sur un ordinateur de bureau, sur le Web ou dans votre main, les programmes informatiques ont parfois besoin de lire des sons ou de la musique. La plupart du temps, l'audio est totalement codé dans des fichiers MP3 ou WMA. Cette approche présente un avantage important : le système d'exploitation lui-même sait généralement décoder et lire ces fichiers. L'application peut ensuite se concentrer sur un travail relativement plus simple qui consiste à fournir une interface utilisateur destinée aux pauses, aux redémarrages et peut-être au parcours des différentes pistes.

La vie n'est toutefois pas toujours aussi simple. Il arrive qu'un programme ait besoin de lire un fichier audio dont le format n'est pas pris en charge par le système d'exploitation, voire de générer des données audio dynamiquement, peut-être pour implémenter une synthèse de musique électronique.

Dans le jargon de Silverlight et Windows Phone, ce processus est connu sous le nom de « diffusion audio en continu ». Au moment de l'exécution, l'application fournit un flux d'octets qui comprennent les données audio. Cela se produit via une classe dérivée de MediaStreamSource qui transmet, à la demande, les données audio au lecteur audio du système d'exploitation. Le système d'exploitation Windows Phone 7.5 peut diffuser de l'audio en continu en arrière-plan, et je vais vous montrer comment procéder.

Dériver de MediaStreamSource

La première étape essentielle de la génération dynamique des données audio dans un programme Windows Phone consiste à dériver de la classe abstraite MediaStreamSource. Le code impliqué est relativement confus à certains endroits. Par conséquent, au lieu d'écrire un code totalement nouveau, il est préférable de copier le code de quelqu'un d'autre.

Le projet SimpleAudioStreaming disponible dans le code source téléchargeable de cet article montre une approche possible. Ce projet contient un dérivé de MediaStreamSource nommé Sine440AudioStreamSource qui se contente de générer une onde sinusoïdale à 440 Hz. Il s'agit de la fréquence correspondant au A au-dessus du C central généralement utilisé comme norme de réglage.

MediaStreamSource comporte six méthodes abstraites qu'un dérivé doit remplacer, mais seules deux d'entre elles sont essentielles. La première est OpenMediaPlayer, dans laquelle vous devez créer deux objets Dictionary et un objet List, ainsi que définir le type de données audio fournies par la classe et divers paramètres qui décrivent ces données. Les paramètres audio sont représentés sous forme de champs d'une structure WAVEFORMATEX Win32 avec tous les nombres multioctets au format Little-Endian (octet le plus significatif en premier) et convertis en chaîne.

Je n'ai jamais utilisé MediaStreamSource pour autre chose que de l'audio au format PCM (Pulse-Code Modulation, modulation d'impulsion codée), qui est utilisé pour la majorité des flux non compressés, dont des CD et le format de fichier Windows WAV. L'audio PCM comprend des échantillons de taille constante à un taux constant nommé taux d'échantillonnage. Pour le son de qualité CD, vous utiliserez 16 bits par échantillon et un taux d'échantillonnage de 44 100 Hz. Vous pouvez choisir un canal pour un son mono ou deux canaux pour la stéréo.

La classe Sine440AudioStreamSource code en dur un seul canal et une taille d'échantillon de 16 bits, mais elle permet de spécifier le taux d'échantillonnage comme argument de constructeur.

En interne, le pipeline audio gère une mémoire tampon des données audio, dont la taille est spécifiée par la propriété AudioBufferLength de MediaStreamSource. Le paramètre par défaut est de 1 000 ms, mais vous pouvez le définir jusqu'à un minimum de 15 ms. Pour garder cette mémoire tampon pleine, la méthode GetSampleAsync du dérivé de MediaStreamSource est appelée. Votre travail consiste à récupérer un ensemble de données audio dans MemoryStream et d'appeler ReportGetSampleCompleted.

La classe Sine440AudioStreamSource est codée en dur pour fournir 4 096 échantillons par appel. Avec un taux d'échantillonnage de 44 100, cela fait à peine 1/10e de seconde d'audio par appel. Il est nécessaire de jouer avec cette valeur et la propriété AudioBufferLength si vous implémentez un synthétiseur contrôlé par l'utilisateur qui doit répondre rapidement aux entrées utilisateur. Pour une latence minimale, il est conseillé d'avoir une mémoire tampon de petite taille. Attention cependant à ce que cette taille ne soit pas trop réduite, faute de quoi vous risquez d'avoir des blancs lors de la lecture.

La figure 1 illustre l'implémentation Sine440AudioStreamSource du remplacement de GetSampleAsync. Dans la boucle, une valeur sinusoïdale de 16 bits est obtenue depuis un appel de la méthode Math.Sin adaptée à la taille du court morceau de code suivant :

short amplitude = (short)(short.MaxValue * Math.Sin(angle));

Figure 1 La méthode GetSampleAsync dans Sine440AudioStreamSource

protected override void GetSampleAsync(MediaStreamType mediaStreamType)
{
  // Reset MemoryStream object
  memoryStream.Seek(0, SeekOrigin.Begin);
  for (int sample = 0; sample < BufferSamples; sample++)
  {
    short amplitude = (short)(short.MaxValue * Math.Sin(angle)); 
    memoryStream.WriteByte((byte)(amplitude & 0xFF));
    memoryStream.WriteByte((byte)(amplitude >> 8));
    angle = (angle + angleIncrement) % (2 * Math.PI);
  }
  // Send out the sample
  ReportGetSampleCompleted(new MediaStreamSample(mediaStreamDescription,
    memoryStream,
    0,
    BufferSize,
    timestamp,
    mediaSampleAttributes));
  // Prepare for next sample
  timestamp += BufferSamples * 10000000L / sampleRate;
}

Cette amplitude est ensuite divisée en 2 octets et stockée dans MemoryStream, avec l'octet inférieur en premier. Pour la stéréo, chaque échantillon requiert deux valeurs de 16 bits, alternant entre la gauche et la droite.

D'autres calculs sont possibles. Vous pouvez passer d'une onde sinusoïdale à une onde en dent de scie en définissant l'amplitude de cette façon :

short amplitude = (short)(short.MaxValue * angle / Math.PI + short.MinValue);

Dans les deux cas, une variable nommée « angle » s'étend de 0 à 2π rad (ou 360 degrés) afin de référencer un cycle unique d'une forme d'onde spécifique. Après chaque échantillon, l'angle est augmenté de la valeur angleIncrement, une variable calculée plus tôt dans la classe selon la fréquence du taux d'échantillonnage et la fréquence de la forme de l'onde à générer, qui est codée en dur ici avec 440 Hz :

angleIncrement = 2 * Math.PI * 440 / sampleRate;

Vous remarquerez qu'à mesure que la fréquence de la forme d'onde générée se rapproche de la moitié du taux d'échantillonnage, angleIncrement se rapproche de π ou de 180 degrés. À la moitié du taux d'échantillonnage, la forme d'onde générée repose simplement sur deux échantillons par cycle et le résultat est une onde carrée au lieu d'une courbe sinusoïdale régulière. Toutefois, toutes les harmoniques de cette onde carrée sont supérieures à la moitié du taux d'échantillonnage.

Remarquez également que vous ne pouvez pas générer de fréquences supérieures à la moitié du taux d'échantillonnage. Si vous essayez, vous générerez en fait des « alias » situés sous la moitié du taux d'échantillonnage. La moitié du taux d'échantillonnage est connue sous le nom de fréquence Nyquist, nommée d'après Harry Nyquist, un ingénieur qui travaillait pour AT&T lorsqu'il a publié un article précurseur sur la théorie de l'information en 1928. Cet article a posé les bases de la technologie d'échantillonnage audio.

Pour le CD audio, un taux d'échantillonnage de 44 100 Hz a été choisi partiellement parce que la moitié de 44 100 est supérieure à la limite la plus élevée de l'audition humaine, généralement de 20 000 Hz.

À part la classe Sine440AudioStreamSource, le reste du projet SimpleAudioStreaming est relativement simple : le fichier MainPage.xaml contient un élément multimédia nommé mediaElement et le remplacement OnNavigatedTo de MainPage appelle SetSource sur cet objet avec une instance du dérivé de MediaStreamSource :

mediaElement.SetSource(new Sine440AudioStreamSource(44100));

Au départ, j'avais mis cet appel dans le constructeur du programme, mais j'ai découvert que la lecture de musique ne pouvait pas reprendre sans désactiver le programme si vous vous étiez éloigné de celui-ci avant d'y revenir.

La méthode SetSource de MediaElement est la même méthode que vous appelez si vous souhaitez que MediaElement lise un fichier de musique référencé avec un objet Stream. L'appel de SetSource démarre normalement la lecture du son, mais cet élément MediaElement a sa propriété AutoPlay définie sur false et un appel de Play est donc également nécessaire. Le programme comprend dans la barre d'application des boutons de lecture et de pause qui effectuent ces opérations.

Bien que le programme soit en cours d'exécution, vous pouvez contrôler le volume depuis le contrôle de volume universel du téléphone, mais si vous terminez le programme ou si vous vous en éloignez, le son s'arrête.

Passer en arrière-plan

Il est également possible d'utiliser ce même dérivé de MediaStreamSource pour lire des sons ou de la musique en arrière-plan. La musique d'arrière-plan continue à être lue sur le téléphone si vous vous éloignez du programme, voire si vous le terminez. Pour l'audio d'arrière-plan, vous pouvez utiliser le contrôle de volume universel du téléphone afin de contrôler le volume, mais également pour interrompre et redémarrer l'audio et, le cas échéant, avancer jusqu'à d'autres pistes ou revenir à certaines.

Vous serez ravi de découvrir qu'une grande partie de ce que vous avez appris au cours de l'article publié le mois dernier dans cette rubrique (msdn.microsoft.com/magazine/hh781030) reste applicable pour la diffusion audio en continu en arrière-plan. Dans cette rubrique, j'ai décrit comment lire des fichiers de musique en arrière-plan. Vous créez un projet de bibliothèque contenant une classe dérivée d'AudioPlayerAgent. Visual Studio va générer cette classe pour vous si vous ajoutez un projet de type Windows Phone Audio Playback Agent.

L'application doit avoir une référence à la DLL contenant l'AudioPlayerAgent, mais n'accède pas à cette classe directement. En revanche, l'application accède à la classe BackgroundAudioPlayer pour définir un objet initial AudioTrack et appeler Play et Pause. Souvenez-vous que la classe AudioTrack a un constructeur qui vous permet de spécifier un titre de piste, un artiste et un nom d'album, ainsi que d'indiquer les boutons qui doivent être activés sur le contrôle de volume universel.

En règle générale, le premier argument du constructeur AudioTrack est un objet Uri qui indique la source du fichier de musique que vous souhaitez lire. Pour que cette instance AudioTrack lise de l'audio en continu plutôt qu'un fichier de musique, définissez ce premier argument de constructeur sur null. Dans ce cas, BackgroundAudioPlayer recherche une classe dérivée d'AudioStreamingAgent dans une DLL référencée par le programme. Vous pouvez créer une DLL dans Visual Studio en ajoutant un projet de type Windows Phone Audio Streaming Agent.

La solution SimpleBackgroundAudioStreaming disponible dans le code téléchargeable de cet article montre comment procéder. La solution contient le projet d'application et deux projets de bibliothèque, l'un nommé AudioPlaybackAgent qui contient une classe AudioPlayer dérivée d'AudioPlayerAgent et l'autre nommé AudioStreamAgent qui contient une classe AudioTrackStreamer dérivée d'AudioStreamingAgent. Le projet d'application contient des références à ces deux projets de bibliothèque, mais il ne tente pas d'accéder aux classes réelles. Pour les raisons que j'ai abordées au cours de l'article précédent, cette façon de procéder est futile.

Permettez-moi d'insister à nouveau sur le fait que l'application doit contenir des références aux DLL d'agent d'arrière-plan. Il est facile d'omettre ces références car vous ne verrez aucune erreur, mais la musique ne sera pas lue.

Une grande partie de la logique de SimpleBackgroundAudioStreaming est la même que pour un programme qui lit des fichiers de musique en arrière-plan, à l'exception du fait que lorsque BackgroundAudioPlayer tente de lire une piste avec un objet Uri null, la méthode OnBeginStreaming du dérivé d'AudioStreamingAgent est appelée. Voici comment je gère cet appel de façon extrêmement simple :

protected override void OnBeginStreaming(AudioTrack track, AudioStreamer streamer)
{
  streamer.SetSource(new Sine440AudioStreamSource(44100));
}

Et voilà ! Il s'agit de la même classe Sine440AudioStreamSource que celle décrite précédemment, mais elle est désormais incluse dans le projet AudioStreamAgent.

Bien que le programme SimpleBackgroundAudioStreaming crée une seule piste, vous pouvez avoir plusieurs objets de piste et combiner des objets AudioTrack qui font référence à des fichiers de musique et ceux qui utilisent la diffusion en continu. Vous remarquerez que l'objet AudioTrack est un argument du remplacement d'OnBeginStreaming. Vous pouvez donc utiliser cette information pour personnaliser la MediaStreamSource que vous souhaitez utiliser pour cette piste. Afin de vous permettre d'indiquer des informations complémentaires, AudioTrack a une propriété Tag que vous pouvez définir sur n'importe quelle chaîne.

Création d'un synthétiseur

Une courbe sinusoïdale stable à 440 Hz peut être assez ennuyeuse, nous allons donc créer un synthétiseur de musique électronique. J'ai créé un synthétiseur très rudimentaire composé de 12 classes et de deux interfaces dans le projet de bibliothèque Petzold.MusicSynthesis de la solution SynthesizerDemos. Certaines de ces classes sont similaires au code que j'ai écrit pour Silverlight 3 et qui a été publié sur mon blog en juillet 2007. Il est disponible sur charlespetzold.com.

Au cœur de ce synthétiseur se trouve un dérivé de MediaStreamSource nommé DynamicPcmStreamSource avec une propriété nommée SampleProvider. Il est défini de la façon suivante :

public IStereoSampleProvider SampleProvider { get; set; }
The IStereoSampleProvider interface is simple:
public interface IStereoSampleProvider
{
  AudioSample GetNextSample();
}

AudioSample comporte deux champs publics de type short nommés Left et Right. Dans la méthode GetSampleAsync, DynamicPcmStreamSource appelle la méthode GetNextSample pour obtenir une paire d'échantillons 16 bits :

AudioSample audioSample = SampleProvider.GetNextSample();

Une classe qui implémente IStereoSampleProvider est nommée Mixer. Mixer a une propriété Inputs qui est une collection d'objets de type MixerInput. Chaque MixerInput a une propriété Input de type IMonoSampleProvider, définie de la façon suivante :

public interface IMonoSampleProvider
{
  short GetNextSample();
}

Une classe qui implémente IMonoSampleProvider est nommée SteadyNoteDurationPlayer. C'est une classe abstraite capable de lire une série de notes de la même durée à un tempo particulier. Elle a une propriété nommée Oscillator qui implémente également IMonoSampleProvider pour générer les formes d'onde réelles. Deux classes sont dérivées de SteadyNoteDurationPlayer : Sequencer, qui lit une série de notes de façon répétitive, et Rambler, qui lit un flux aléatoire de notes. J'utilise ces deux classes dans deux applications différentes de la solution SynthesizerDemos, la première étant uniquement exécutée au premier plan et la seconde lisant la musique en arrière-plan

L'application destinée uniquement au premier plan se nomme WaveformManipulator et elle propose un contrôle vous permettant de définir interactivement une forme d'onde pour lire la musique, comme illustré à la figure 2.

The WaveformManipulator Program
Figure 2 Le programme WaveformManipulator

Les points qui définissent la forme d'onde sont transférés dans un dérivé d'Oscillator nommé VariableWaveformOscillator. À mesure que vous déplacez les points tactiles vers le haut et vers le bas, vous remarquerez qu'il y a environ une seconde de délai avant que vous puissiez entendre réellement un changement de timbre de la musique. Cela est dû à taille de la mémoire tampon par défaut, 1 000 ms, définie par MediaStreamSource.

Le programme WaveformManipulator utilise deux objets Sequencer chargés avec la même série de notes, à savoir des arpèges en E mineur, A mineur, D mineur et G majeur. Toutefois, les tempos pour les deux objets Sequencer sont légèrement différents et ils se désynchronisent, tout d'abord avec un type d'effet de reverb ou d'écho, puis plus comme un contrepoint. C'est une forme de « musique de traitement » inspirée par le travail précurseur du compositeur américain Steve Reich. Le code d'initialisation provenant de MainPage.xaml.cs qui « relie » les composants du synthétiseur est illustré à la figure 3.

Figure 3 Code d'initialisation du synthétiseur dans WaveformManipulator

// Initialize Waveformer control
for (int i = 0; i < waveformer.Points.Count; i++)
{
  double angle = (i + 1) * 2 * Math.PI / (waveformer.Points.Count + 1);
  waveformer.Points[i] = new Point(angle, Math.Sin(angle));
}
// Create two Sequencers with slightly different tempi
Sequencer sequencer1 = new Sequencer(SAMPLE_RATE)
{
  Oscillator = new VariableWaveformOscillator(SAMPLE_RATE)
  {
    Points = waveformer.Points
  },
  Tempo = 480
};
Sequencer sequencer2 = new Sequencer(SAMPLE_RATE)
{
  Oscillator = new VariableWaveformOscillator(SAMPLE_RATE)
  {
    Points = waveformer.Points
  },
  Tempo = 470
};
// Set the same Pitch objects in the Sequencer objects
Pitch[] pitches =
{
  ...
};
foreach (Pitch pitch in pitches)
{
  sequencer1.Pitches.Add(pitch);
  sequencer2.Pitches.Add(pitch);
}
// Create Mixer and MixerInput objects
mixer = new Mixer();
mixer.Inputs.Add(new MixerInput(sequencer1) { Space = -0.5 });
mixer.Inputs.Add(new MixerInput(sequencer2) { Space = 0.5 });

Les objets Mixer, DynamicPcmStreamSource et MediaElement sont connectés dans le remplacement d'OnNavigatedTo :

DynamicPcmStreamSource dynamicPcmStreamSource =
  new DynamicPcmStreamSource(SAMPLE_RATE);
dynamicPcmStreamSource.SampleProvider = mixer;
mediaElement.SetSource(dynamicPcmStreamSource);

Étant donné que WaveformManipulator utilise MediaElement pour lire la musique, il ne lit la musique que lorsque le programme est exécuté au premier plan. 

Limites de l'arrière-plan

J'ai beaucoup réfléchi à la création d'une version de WaveformManipulator capable de lire la musique en arrière-plan à l'aide de BackgroundAudioPlayer. Il est évident que vous pourriez uniquement manipuler la forme d'onde lorsque le programme est en arrière-plan, mais je n'ai pas pu dépasser un obstacle dont j'ai parlé dans mon article du mois dernier : les DLL de l'agent d'arrière-plan fournies par votre programme pour gérer le traitement en arrière-plan sont exécutées dans une tâche différente du programme lui-même et la seule façon que je puisse imaginer pour que ces deux tâches parviennent à échanger des données arbitraires est d'avoir recours à un stockage isolé.

J'ai décidé de ne pas poursuivre ce travail, partiellement parce que j'avais une meilleure idée pour un programme lisant de l'audio en continu en arrière-plan. Ce programme était destiné à lire un accord aléatoire, mais avec des notes légèrement modifiées lorsque l'accéléromètre enregistrait une modification de l'orientation du téléphone. Le fait de bouger le téléphone pouvait créer une chanson totalement nouvelle.

Avec ce projet, je suis allé jusqu'à tenter d'ajouter une référence à l'assembly Microsoft.Devices.Sensors dans le projet AudioStreamAgent. J'ai alors obtenu un message avec une croix rouge m'avertissant qu'une tentative d'ajout d'une référence non prise en charge par un agent d'arrière-plan avait eu lieu. Il semblerait que les agents d'arrière-plan ne puissent pas utiliser l'accéléromètre. Tant pis pour cette idée de programme !

À la place, j'ai décidé d'écrire un programme nommé PentatonicRambler qui utilise le flux continu en arrière-plan pour lire une mélodie sans fin dans une échelle pentatonique composée de cinq notes noires de piano seulement. Les notes sont choisies de façon aléatoire par un composant de synthétiseur nommé Rambler, qui limite chaque note suivante à une étape avant ou après la note précédente. Grâce à l'absence de grands sauts, le flux de notes obtenu ressemble plus à une mélodie composée (ou improvisée) qu'à une mélodie purement aléatoire.

La figure 4 illustre le remplacement d'OnBeginStreaming dans le dérivé d'AudioStreamingAgent.

Figure 4 Configuration du synthétiseur pour PentatonicRambler

protected override void OnBeginStreaming(AudioTrack track, AudioStreamer streamer)
{
  // Create a Rambler
  Rambler rambler = new Rambler(SAMPLE_RATE,
  new Pitch(Note.Csharp, 4), // Start
  new Pitch(Note.Csharp, 2), // Minimum
  new Pitch(Note.Csharp, 6)) // Maximum
  {
    Oscillator = new AlmostSquareWave(SAMPLE_RATE),
    Tempo = 480
  };
  // Set allowable note values
  rambler.Notes.Add(Note.Csharp);
  rambler.Notes.Add(Note.Dsharp);
  rambler.Notes.Add(Note.Fsharp);
  rambler.Notes.Add(Note.Gsharp);
  rambler.Notes.Add(Note.Asharp);
  // Create Mixer and MixerInput objects
  Mixer mixer = new Mixer();
  mixer.Inputs.Add(new MixerInput(rambler));
  DynamicPcmStreamSource audioStreamSource =
    new DynamicPcmStreamSource(SAMPLE_RATE);
  audioStreamSource.SampleProvider = mixer;
  streamer.SetSource(audioStreamSource);
}

J'aurais préféré définir l'assemblage des composants du synthétiseur dans le programme lui-même, puis transférer cette configuration dans l'agent d'arrière-plan, mais étant donné l'isolation du processus entre la tâche de programme et les tâches de l'agent d'arrière-plan, cela demanderait un peu de travail. La configuration du synthétiseur devrait être définie entièrement dans une chaîne de texte (peut-être une chaîne XML), puis passée du programme vers le dérivé d'AudioStreamingAgent via la propriété Tag d'AudioTrack.

En attendant, Je souhaiterais que les améliorations futures de Windows Phone comprennent une fonctionnalité permettant aux programmes de communiquer avec les agents d'arrière-plan qu'ils appellent.

Charles Petzold contribue depuis longtemps à l'élaboration de MSDN Magazine. Son site Web est charlespetzold.com.

Merci aux experts techniques suivants d'avoir relu cet article : Eric BieMark Hopkins et Chris Pearson