E
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;
}
}