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; } }
-
Bitte sehr!
Bei der Problemanalyse stelle ich mir selber auch immer die Frage: Wann tritt dieses Problem nicht auf? (um das Problem näher einzugrenzen)