Gemischte Aufnahme - wie Echo unterdrücken?



  • Ich habe ein mp3 via text-to-speech erzeugt, das programmierbare Pausen zum Nachsprechen lässt. Wenn dieses mp3 nun abgespielt wird, kann der Nutzer das Gesagte nachsprechen oder darauf antworten. Zur Kontrolle kann das Gesamte, also mp3 und eigene Mikrofonaufnahme als gemischte Aufnahme (auch mp3) aufgezeichnet werden. Das klappt auch alles prächtig. Nur hört man beim Sprechen die eigene Sprache als Echo. Ich habe bisher keinen funktionierenden Weg gefunden dieses Echo (Monitoring) zu unterdrücken. In Windows ist es beim Mikrophon ausgeschaltet. Hier sind die beiden Funktionen zum Starten und Stoppen:

    using NAudio.CoreAudioApi;
    using NAudio.Lame;
    using NAudio.Mixer;
    using NAudio.Wave;
    using NAudio.Wave.SampleProviders;
    
    private bool mp3Generated = false;
    private CheckBox chkRecord;
    private Label lblRecording;
    private WaveInEvent? micIn;
    private BufferedWaveProvider? micBuffer;
    private IWavePlayer? mixPlayer;                     // spielt das Gemisch ab
    private WaveFileWriter? repetitionWriter;
    private WasapiOut? ttsOutput;                       // Keine direkte Ausgabe mehr an Default Device
    private AudioFileReader? audioReader;
    private WaveOutEvent? player;
    
    private void StartRecording(string playbackFile, string repetitionFile)
    {
        try
        {
            StopRecording(); // Vorher aufräumen
    
            if (!File.Exists(playbackFile))
            {
                MessageBox.Show($"Playback file not found: {playbackFile}");
                return;
            }
    
            // --- TTS vorbereiten
            var reader = new AudioFileReader(playbackFile); // liest Output.mp3
            var ttsProvider = EnsureMono44100(reader);
    
            // --- Mikrofon vorbereiten
            micIn = new WaveInEvent
            {
                WaveFormat = new WaveFormat(44100, 16, 1),
                BufferMilliseconds = 50
            };
            micBuffer = new BufferedWaveProvider(micIn.WaveFormat)
            {
                DiscardOnBufferOverflow = true
            };
            micIn.DataAvailable += (s, a) => micBuffer.AddSamples(a.Buffer, 0, a.BytesRecorded);
            micIn.StartRecording();
    
            var micProvider = micBuffer.ToSampleProvider(); // Mono float 44.1k
    
            // --- Mixer aus TTS + Mikro
            var mixer = new MixingSampleProvider(new[] { ttsProvider, micProvider })
            {
                ReadFully = true
            };
    
            // --- WAV-Datei zum Schreiben
            repetitionWriter = new WaveFileWriter(repetitionFile, WaveFormat.CreateIeeeFloatWaveFormat(44100, 1));
    
            // --- Tap für gleichzeitiges Schreiben
            var tap = new TapSampleProvider(mixer, repetitionWriter);
    
            // --- TTS AUSGABE ohne Monitoring
            ttsOutput = new WasapiOut(AudioClientShareMode.Shared, false, 100); // false = kein Loopback
            ttsOutput.Init(tap);
            ttsOutput.Play();
    
            lblRecording.Visible = true;
            btnPause.Enabled = false;
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error during recording: " + ex.Message);
            StopRecording();
        }
    }
    
    private void StopRecording()
    {
        try
        {
            micIn?.StopRecording();
            micIn?.Dispose();
            micIn = null;
    
            mixPlayer?.Stop();
            mixPlayer?.Dispose();
            mixPlayer = null;
    
            repetitionWriter?.Flush();
            repetitionWriter?.Dispose();
            repetitionWriter = null;
    
            micBuffer = null;
    
            lblRecording.Visible = false;
            btnPause.Enabled = true;
    
            // WAV → MP3
            string wavPath = "Output_Repetition.wav";
            string mp3Path = "Output_Repetition.mp3";
            if (File.Exists(wavPath))
            {
                using (var reader = new AudioFileReader(wavPath))
                using (var writer = new LameMP3FileWriter(mp3Path, reader.WaveFormat, LAMEPreset.VBR_90))
                {
                    reader.CopyTo(writer);
                }
    
                File.Delete(wavPath); // WAV löschen
            }
    
            lblFertig.Text = "Recording finished: Output_Repetition.mp3";
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error stopping recording:\n" + ex);
        }
    }
    

    Ich habe es sogar mit ChatGPT-4o und -5 versucht. Dabei wird jedoch die gemischte Aufnahme kaputt gemacht. Offenbar nicht ganz trivial.



  • Hört man dieses Echo nur bei der gemischten Aufnahme oder auch, wenn nur vom Mikrofon aufgenommen wird?

    Edit: Ansonsten mal nach "acoustic echo cancellation (AEC)" suchen - ich selber habe bisher keine direkten C#-Komponenten im Netz gefunden.



  • @Th69 sagte in Gemischte Aufnahme - wie Echo unterdrücken?:

    Ohne vorhandenes Output.mp3 kann man nicht aufnehmen.

    Es gibt noch einen Micro-Test:

    private void BtnMicTest_Click(object? sender, EventArgs e)
    {
        var micTest = new WaveInEvent
        {
            WaveFormat = new WaveFormat(44100, 1),
            BufferMilliseconds = 100
        };
    
        var writer = new WaveFileWriter("MicOnly.wav", micTest.WaveFormat);
    
        micTest.DataAvailable += (s, args) =>
        {
            writer.Write(args.Buffer, 0, args.BytesRecorded);
        };
    
        micTest.RecordingStopped += (s, args) =>
        {
            writer.Dispose();
            micTest.Dispose();
            MessageBox.Show("Mic test finished. File saved: MicOnly.wav");
        };
    
        micTest.StartRecording();
    
        // nach 5 Sekunden automatisch stoppen
        Task.Delay(5000).ContinueWith(_ =>
        {
            micTest.StopRecording();
        });
    }
    

    Dabei gibt es kein Echo. Vielleicht eine Möglichkeit, wenn man es sauber synchronisiert.
    Das Problem ist wohl gleichzeitiges Abspielen und Aufnehmen.



  • Danke für diese Frage! Das ist genau die Lösung. Getrennt abspielen und aufnehmen, und erst anschließend passend bezüglich Zeit mit gleichem Format mischen.

    using NAudio.Lame;
    using NAudio.Wave;
    using NAudio.Wave.SampleProviders;
    
    private bool mp3Generated = false;
    private CheckBox chkRecord;
    private Label lblRecording;
    private WaveInEvent? micIn;
    private WaveFileWriter? repetitionWriter;
    private readonly object _recLock = new();
    private CancellationTokenSource? _autoStopCts;
    private bool _isRecording;
    private enum StopReason { Manual, AutoTimeout }
    private StopReason _lastStopReason = StopReason.Manual;
    private bool hideTextMode = false;
    private Button btnToggleText;            // dein Toggle-Button als Feld
    private bool _toggleBackOnPlaybackEnd;   // soll am Ende automatisch zurückgeschaltet werden?
    private bool _manualStopRequested;       // wurde Stop manuell ausgelöst?
    
    // Startet Playback + Mikroaufnahme
    private void StartRecording(string playbackFile, string repetitionFile)
    {
        try
        {
            // Schutz: nur aufnehmen, wenn passend generiert
            if (!mp3Generated)
            {
                MessageBox.Show("No MP3 for this lesson. Generate MP3 first.");
                return;
            }
    
            // falls noch was lief
            StopRecording();
    
            if (!File.Exists(playbackFile))
            {
                MessageBox.Show($"Playback file not found: {playbackFile}");
                return;
            }
    
            // Mic starten
            micIn = new WaveInEvent
            {
                WaveFormat = new WaveFormat(44100, 1),
                BufferMilliseconds = 100
            };
            repetitionWriter = new WaveFileWriter("MicOnly.wav", micIn.WaveFormat);
            micIn.DataAvailable += (s, e) =>
            {
                repetitionWriter.Write(e.Buffer, 0, e.BytesRecorded);
            };
            micIn.StartRecording();
    
            // Playback starten (nur fürs Ohr)
            audioReader = new AudioFileReader(playbackFile);
            player = new WaveOutEvent();
            player.Init(audioReader);
    
            // -> End-Toggle nur, wenn zum Start HideText aktiv war
            _toggleBackOnPlaybackEnd = hideTextMode;
            _manualStopRequested = false;
            player.PlaybackStopped -= Player_PlaybackStopped;
            player.PlaybackStopped += Player_PlaybackStopped;
    
            player.Play();
    
            lblRecording.Visible = true;
            btnPause.Enabled = false;
            _isRecording = true;
    
            // --- Auto-Stop nach Output.mp3 + 10s ---
            // Vorherigen Auto-Stop abbrechen (falls vorhanden)
            _autoStopCts?.Cancel();
            _autoStopCts = new CancellationTokenSource();
    
            var autoDuration = audioReader.TotalTime + TimeSpan.FromSeconds(10);
            var token = _autoStopCts.Token;
    
            // Timer asynchron starten
            Task.Run(async () =>
            {
                try
                {
                    await Task.Delay(autoDuration, token);
                    if (!token.IsCancellationRequested && _isRecording)
                    {
                        // Auto-Stop auslösen (UI-Thread)
                        _lastStopReason = StopReason.AutoTimeout;
                        if (!IsDisposed && IsHandleCreated)
                            BeginInvoke(new Action(StopRecording));
                    }
                }
                catch (TaskCanceledException) { /* ignoriere */ }
            });
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error during recording: " + ex);
            _lastStopReason = StopReason.Manual;
            StopRecording();
        }
    }
    
    
    
    // Stoppt Aufnahme + erzeugt Mix/MP3
    private void StopRecording()
    {
        try
        {
            if (!_isRecording)
                return; // doppelte Aufrufe vermeiden
    
            _isRecording = false;
    
            // Auto-Stop-Timer abbrechen
            _autoStopCts?.Cancel();
            _autoStopCts = null;
    
            // Aufnahme stoppen
            if (micIn != null)
            {
                try { micIn.StopRecording(); } catch { }
                micIn.Dispose();
                micIn = null;
            }
    
            if (repetitionWriter != null)
            {
                try { repetitionWriter.Dispose(); } catch { }
                repetitionWriter = null;
            }
    
            // Playback stoppen
            if (player != null)
            {
                _manualStopRequested = true;   // 👉 HIER neu
                try { player.Stop(); } catch { }
                player.PlaybackStopped -= Player_PlaybackStopped; // Abo abmelden
                player.Dispose();
                player = null;
            }
    
            if (audioReader != null)
            {
                audioReader.Dispose();
                audioReader = null;
            }
    
            // Ohne Mikro-Datei kein Mix
            if (!File.Exists("MicOnly.wav"))
            {
                lblFertig.Text = "No MicOnly.wav recorded.";
                return;
            }
    
            string repetitionFile = "Output_Repetition.wav";
    
            using (var origReader = new AudioFileReader("Output.mp3"))
            using (var micReader = new AudioFileReader("MicOnly.wav"))
            {
                ISampleProvider origMono = EnsureMono44100(origReader);
                ISampleProvider micMono = EnsureMono44100(micReader);
    
                // Längen
                var recordDuration = micReader.TotalTime;
                var outputDuration = origReader.TotalTime;
    
                TimeSpan finalDuration;
                if (_lastStopReason == StopReason.Manual)
                {
                    // Manuelles Stop -> exakt bis Länge der Aufnahme
                    finalDuration = recordDuration;
                }
                else // AutoTimeout
                {
                    // Auto-Stop -> bis Output.mp3 + 10s, aber nicht länger als tatsächlich aufgenommen
                    var upperBound = outputDuration + TimeSpan.FromSeconds(10);
                    finalDuration = recordDuration < upperBound ? recordDuration : upperBound;
                }
    
                // Beide Quellen auf finalDuration begrenzen
                var origLimited = new OffsetSampleProvider(origMono) { Take = finalDuration };
                var micLimited = new OffsetSampleProvider(micMono) { Take = finalDuration };
    
                var mix = new MixingSampleProvider(new[] { origLimited, micLimited })
                {
                    ReadFully = false
                };
    
                WaveFileWriter.CreateWaveFile(repetitionFile, mix.ToWaveProvider());
            }
    
            // MP3 erzeugen
            string mp3Path = "Output_Repetition.mp3";
    
            // 👉 Falls bereits vorhanden, zuerst nach _old sichern
            if (File.Exists(mp3Path))
            {
                string oldName = Path.GetFileNameWithoutExtension(mp3Path) + "_old.mp3";
                try
                {
                    if (File.Exists(oldName)) File.Delete(oldName);
                    File.Move(mp3Path, oldName);
                }
                catch { /* Ignorieren oder loggen */ }
            }
    
            using (var reader = new AudioFileReader(repetitionFile))
            using (var writer = new LameMP3FileWriter(mp3Path, reader.WaveFormat, LAMEPreset.VBR_90))
            {
                reader.CopyTo(writer);
            }
    
            // Aufräumen
            try { File.Delete("MicOnly.wav"); } catch { }
            try { File.Delete(repetitionFile); } catch { }
    
            lblFertig.Text = "Recording finished: " + mp3Path;
    
            // 👉 Nur wenn Auto-Stop, eine Info-Box anzeigen
            if (_lastStopReason == StopReason.AutoTimeout)
            {
                MessageBox.Show("Recording finished automatically.\n\nFile saved: " + mp3Path,
                    "Auto-Stop", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error stopping recording:\n" + ex);
        }
        finally
        {
            lblRecording.Visible = false;
            btnPause.Enabled = true;
        }
    }
    
    private void Player_PlaybackStopped(object? sender, StoppedEventArgs e)
    {
        try
        {
            // Nur bei natürlichem Ende UND wenn Hide beim Start aktiv war
            if (!_manualStopRequested && _toggleBackOnPlaybackEnd && hideTextMode)
            {
                if (IsHandleCreated)
                {
                    BeginInvoke(new Action(() =>
                    {
                        hideTextMode = false;
                        btnToggleText.Text = "Hide Text";
                        btnToggleText.BackColor = Color.LightGreen;
                        lstStatus.Invalidate();
                    }));
                }
            }
        }
        catch { }
        finally
        {
            _toggleBackOnPlaybackEnd = false;
            _manualStopRequested = false;
    
            if (player != null)
                player.PlaybackStopped -= Player_PlaybackStopped;
        }
    }
    
    private static ISampleProvider EnsureMono44100(IWaveProvider src)
    {
        var provider = src.ToSampleProvider();
    
        // Resample falls nötig
        if (provider.WaveFormat.SampleRate != 44100)
            provider = new WdlResamplingSampleProvider(provider, 44100);
    
        // Kanäle vereinheitlichen
        if (provider.WaveFormat.Channels == 2)
        {
            // Stereo -> Mono
            provider = new StereoToMonoSampleProvider(provider)
            {
                LeftVolume = 0.5f,
                RightVolume = 0.5f
            };
        }
        else if (provider.WaveFormat.Channels > 2)
        {
            // Mehrkanal -> nur die ersten beiden Kanäle nehmen und zu Mono mischen
            var multi = new MultiplexingSampleProvider(new[] { provider }, 2);
            multi.ConnectInputToOutput(0, 0); // Front Left
            multi.ConnectInputToOutput(1, 1); // Front Right
            provider = new StereoToMonoSampleProvider(multi)
            {
                LeftVolume = 0.5f,
                RightVolume = 0.5f
            };
        }
        // Falls Mono (1 Kanal), bleibt es wie es ist
    
        return provider;
    }
    
    
    
    // Tap-Provider: schreibt die durchlaufenden Samples parallel in die WAV
    private sealed class TapSampleProvider : ISampleProvider
    {
        private readonly ISampleProvider source;
        private readonly WaveFileWriter writer;
        public TapSampleProvider(ISampleProvider source, WaveFileWriter writer)
        {
            this.source = source;
            this.writer = writer;
            WaveFormat = source.WaveFormat;
        }
        public WaveFormat WaveFormat { get; }
        public int Read(float[] buffer, int offset, int count)
        {
            int read = source.Read(buffer, offset, count);
            if (read > 0) writer.WriteSamples(buffer, offset, read);
            return read;
        }
    }
    

Anmelden zum Antworten