J'ai un problème avec un BufferedWaveProvider de la bibliothèque NAudio. J'enregistre 2 appareils audio (un microphone et un haut-parleur), je les fusionne en 1 flux et je l'envoie à un encodeur (pour une vidéo).

Pour ce faire, je fais ce qui suit:

  1. Créez un fil de discussion dans lequel j'enregistrerai le microphone à l'aide de WasapiCapture.
  2. Créez un fil de discussion dans lequel j'enregistrerai l'audio des haut-parleurs à l'aide de WasapiLookbackCapture. (J'utilise aussi un SilenceProvider donc je n'ai pas de lacunes dans ce que j'enregistre).
  3. Je veux mélanger ces 2 audio donc je dois m'assurer qu'ils ont le même format, donc je détecte quel est le meilleur WaveFormat dans tous ces appareils audio. Dans mon scénario, c'est l'orateur. Je décide donc que l'audio du Microphone passera par un MediaFoundationResampler pour adapter son format afin qu'il ait le même que celui du haut-parleur.
  4. Chaque morceau audio de la capture Wasapi (Lookback) est envoyé à un BufferedWaveProvider.
  5. Ensuite, j'ai aussi fait un MixingSampleProvider où je passe le ISampleProvider de chaque fil d'enregistrement. Je passe donc le MediaFoundationResampler pour le microphone et le BufferedWaveProvider pour les haut-parleurs.
  6. En boucle dans un troisième thread, je lis les données du MixingSampleProvider, qui est censé vider de façon asynchrone le (s) BufferedWaveProvider pendant qu'il se remplit.
  7. Parce que chaque tampon peut ne pas être rempli exactement en même temps, je regarde quelle est la durée commune minimale entre ces 2 tampons, et je lis ce montant dans le fournisseur d'échantillons de mixage.
  8. Ensuite, je mets ce que je lis en file d'attente pour que mon encodeur, dans un 4ème thread, le traite également en parallèle.

Veuillez consulter l'organigramme ci-dessous qui illustre ma description ci-dessus.

enter image description here

Mon problème est le suivant:

  • Cela fonctionne très bien lors de l'enregistrement du microphone et du haut-parleur pendant plus d'une heure tout en jouant à un jeu vidéo qui utilise également le microphone (pour le multijoueur en ligne). Pas de crash. Les tampons restent tout le temps vides. C'est génial.
  • Mais pour une raison quelconque, chaque fois que j'essaye mon application during une conversation audio Discord, Skype ou Teams, je plante immédiatement (dans les 5 secondes) sur BufferedWaveProvider.AppSamples car la mémoire tampon est pleine.

En le regardant en mode débogage, je peux voir que:

  • Le buffer correspondant à l'enceinte est presque vide. Il a en moyenne 100ms max d'audio.
  • Le buffer correspondant au micro (celui que je rééchantillonne) est plein (5 secondes).

D'après ce que j'ai lu sur le blog de l'auteur de NAudio, la documentation et sur StackOverflow, je pense que je fais la meilleure pratique (mais je peux me tromper), qui consiste à écrire dans le tampon à partir d'un thread et à le lire en parallèle à partir d'un autre. . Il y a bien sûr un risque qu'il se remplisse plus vite que je ne le lis, et c'est essentiellement ce qui se passe actuellement. Mais je ne comprends pas pourquoi.

Besoin d'aide

J'aimerais avoir de l'aide pour comprendre ce qui me manque ici, s'il vous plaît. Les points suivants me déroutent:

  1. Pourquoi ce problème se produit-il uniquement avec les réunions Discord / Skype / Teams? Les jeux vidéo que j'utilise utilisent également le microphone, donc je ne peux pas imaginer que ce soit quelque chose comme another app is preventing the microphone/speakers to works correctly.

  2. Je synchronise le démarrage des deux enregistreurs audio. Pour ce faire, j'utilise un signal pour demander aux enregistreurs de démarrer, et quand ils ont tous commencé à générer des données (via l'événement DataAvailable), j'envoie un signal pour leur dire de remplir les tampons avec ce qu'ils recevra dans le prochain événement. Ce n'est probablement pas parfait car les deux appareils audio envoient leur DataAvailable à des moments différents, mais nous parlons de 60ms de différence maximum (sur ma machine), pas 5 secondes. Donc je ne comprends pas pourquoi il se remplit.

  3. Pour rebondir sur ce que j'ai dit au n ° 2, ma télémétrie montre que le tampon se remplit de cette façon (les valeurs sont factices):

Microphone buffered duration: 0ms | Speakers: 0ms
Microphone buffered duration: 60ms | Speakers: 60ms
Microphone buffered duration: 0ms | Speakers: 0ms <= That's because I read the data from the mixing sample provider
Microphone buffered duration: 60ms | Speakers: 0ms <= Events may not be in sync, that's ok.
Microphone buffered duration: 120ms | Speakers: 60ms <= Alright, next loop, I'll extract 60ms on each buffer.
Microphone buffered duration: 390ms | Speakers: 0ms <= Wait, how?
Microphone buffered duration: 390ms | Speakers: 60ms
[...]
Microphone buffered duration: 5000ms | Speakers: 0ms <= Oh no :(

Il semble donc que la mémoire tampon du microphone se remplit plus vite ... Mais pourquoi? Cela peut-il être dû au fait que le rééchantillonneur ralentit la lecture de la mémoire tampon du microphone? Si c'est le cas, cela devrait également ralentir la lecture de la mémoire tampon du haut-parleur puisque je la lis via un MixingSampleProvider, n'est-ce pas?

Voici un extrait simplifié de mon code si cela peut aider:


/* THREAD #1 AND #2 */

_audioCapturer = new WasapiCapture(_device); // Or WasapiLookbackCapture + SilenceProvider playing
_audioCapturer.DataAvailable += AudioCapturer_DataAvailable;

// This buffer can host up to 5 second of audio, after that it crashed when calling AddSamples.
// So we should make sure we don't store more than this amount.
_waveBuffer = new BufferedWaveProvider(_audioCapturer.WaveFormat)
{
    DiscardOnBufferOverflow = false,
    ReadFully = false
};

if (DoINeedToResample)
{
    // Create a resampler to adapt the audio to the desired wave format.
    // In my scenario explained above, this happens for the Microphone.
    _resampler = new MediaFoundationResampler(_waveBuffer, targettedWaveFormat);
}
else
{
    // No conversion is required.
    // In my scenario explained above, this happens for the Speakers.
    _resampler = _waveBuffer;
}

        private void AudioCapturer_DataAvailable(object? sender, WaveInEventArgs e)
        {
            NotifyRecorderIsReady();
            if (!AllRecorderAreReady)
            {
                // Don't record the frame unless every other recorders have started to record too.
                return;
            }

            // Add the captured sample to the wave buffer.
            _waveBuffer.AddSamples(e.Buffer, 0, e.BytesRecorded);

            // Notify the "mixer" that a chunk has been recorded.

        }

/* The Mixer, in another class */


_waveProvider = new MixingSampleProvider(_allAudioRecoders.Select(r => r._resampler));
_allAudioRecoders.ForEach(r => r._audioCapturer.StartRecording());

Task _mixingTask = Task.CompletedTask;

        private void OnChunkAddedToBufferedWaveProvider()
        {
            if (_mixingTask.IsCanceled
                || _mixingTask.IsCompleted
                || _mixingTask.IsFaulted
                || _mixingTask.IsCompletedSuccessfully)
            {
                // Treat the buffered audio in parallel.

                _mixingTask = Task.Run(() =>
                {
                    /* THREAD #3 */
                    lock (_lockObject)
                    {
                        TimeSpan minimalBufferedDuration;
                        do
                        {
                            // Gets the common duration of sound that all audio recorder captured.
                            minimalBufferedDuration = _allAudioRecoders.OrderBy(t => t._waveBuffer.Ticks).First().BufferedDuration;

                            if (minimalBufferedDuration.Ticks > 0)
                            {
                                // Read a sample from the mixer.
                                var bufferLength = minimalBufferedDuration.TotalSeconds * _waveProvider!.WaveFormat.AverageBytesPerSecond;
                                var data = new byte[(int)bufferLength];
                                var readData = _waveProvider.Read(data, 0, data.Length);

                                // Send the data to a queue that will be treated in parallel by the encoder.
                            }
                        } while (minimalBufferedDuration.Ticks > 0);
                    }
                });
            }
        }

Quelqu'un a-t-il une idée de ce que je fais mal et / ou pourquoi cela ne se reproduit que lorsque vous discutez avec la voix sur Discord / Skype / Teams et non via des jeux multijoueurs en ligne?

Merci d'avance!

[MISE À JOUR] 09/02/2021

J'ai peut-être trouvé le problème, mais je ne sais pas à 100% comment le gérer. Il semble que j'arrête de recevoir des données du microphone et que la mémoire tampon du haut-parleur est donc pleine. (on dirait qu'hier, c'était le contraire).

[MISE À JOUR] 12/02/2021

Cela ressemble à ça pour une raison quelconque, peut-être (et je dis peut-être parce que le problème pourrait être autre chose) le BufferedWaveProvider ne se résout pas après avoir lu dans certains scénarios.

Ce qui me fait penser à cela, c'est ce qui suit:

  1. Avant de lire le MixingSampleProvider, je consigne la durée de la mémoire tampon dans chaque tampon que nous avons.
  2. Et je l'enregistre après avoir lu aussi.
  3. La plupart du temps, c'est génial, j'obtiens des données constantes montrant le modèle suivant pendant des dizaines de minutes, voire une heure:
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 10ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 0ms

// I don't explain why both buffer are empty considering my algorithm was supposed to read only 10ms, but the output MP4 seems fine and in sync, so it's fine? ...
  1. Et puis tout à coup l'un des tampons se remplira en 5 secondes.
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 10ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 0ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 20ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 20ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 30ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 30ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 50ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 50ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 70ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 70ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 80ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 80ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 100ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 100ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 110ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 110ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 130ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 130ms
[...]
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 4970ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 4970ms
BEFORE READING MICROPHONE: 10ms
BEFORE READING SPEAKER: 4980ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 4980ms
BEFORE READING MICROPHONE: 20ms
BEFORE READING SPEAKER: 5000ms
AFTER READING MICROPHONE: 0ms
AFTER READING SPEAKER: 5000ms
<!-- Crash -->

Je pourrais faire une correction sale en effaçant le tampon quand il commence à ne plus être synchronisé, mais je voudrais vraiment comprendre pourquoi cela se produit et s'il existe une meilleure approche pour le contourner.

Je vous remercie

[MISE À JOUR] # 2

OK, je pense que j'ai isolé le problème. Cela peut être un bogue dans la bibliothèque NAudio. Voici ce que j'ai fait:

  1. Jouez mon programme comme d'habitude.
  2. Lorsque l'un des tampons atteint 5 s (c'est-à-dire qu'il est plein), arrêtez de remplir ce tampon spécifique.
  3. En faisant cela, je me retrouve dans une situation où le tampon de 1 périphérique se remplit, le tampon de l'autre périphérique ne l'est pas, mais je continue à lire ces tampons quand je le peux.
  4. Et voici ce que j'ai découvert: il semble que la taille du tampon qui est plein ne diminue jamais après la lecture, ce qui explique pourquoi tout à coup il se remplit. Il est malheureusement incohérent et ne peut pas expliquer pourquoi.
6
Etienne Baudoux 9 févr. 2021 à 10:34

1 réponse

Meilleure réponse

Après plus d'enquêtes et un post sur GitHub: https://github.com/naudio/NAudio/issues / 742

J'ai découvert que je devais écouter l'événement MixingSampleProvider.MixerInputEnded et ajouter à nouveau l'échantillon fourni au MixingSampleProvider quand cela se produit.

La raison pour laquelle cela se produit est que je traite l'audio en le capturant, et il y a des moments où je peux le traiter plus vite que je ne l'enregistre, donc le MixingSampleProvider considère qu'il n'a plus rien à lire et s'arrête. Je devrais donc lui dire que non, ce n'est pas fini et il devrait s'attendre à plus.

0
Etienne Baudoux 8 mars 2021 à 05:45