| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- using System;
- using System.Diagnostics;
- using System.Globalization;
- using System.IO;
- using System.Runtime.InteropServices;
- using System.Threading.Tasks;
- using Avalonia.Controls;
- using Avalonia.Interactivity;
- using Avalonia.Threading;
- using SlamCross.Controls;
- namespace SlamCross.Views;
- public partial class TrimWindow : Window
- {
- // Trim byte positions are offsets into s16le @ 22050 Hz mono (matches AudioService)
- private const int TrimSampleRate = 22050;
- private const int TrimBytesPerSample = 2;
- private double _totalDuration;
- private Process? _previewFf;
- private Process? _previewPa;
- private bool _isPreviewing;
- private bool _suppressChange;
- private string _filePath = "";
- public bool Confirmed { get; private set; }
- public int StartPos { get; private set; }
- public int EndPos { get; private set; }
- public TrimWindow()
- {
- InitializeComponent();
- }
- public TrimWindow(string filePath, int startPos, int endPos)
- {
- InitializeComponent();
- _filePath = filePath;
- StartPos = startPos;
- EndPos = endPos;
- _ = LoadAsync(filePath, startPos, endPos);
- }
- private static double BytesToSec(int b) => b / (TrimSampleRate * (double)TrimBytesPerSample);
- private static int SecToBytes(double sec) => (int)(sec * TrimSampleRate * TrimBytesPerSample);
- private async Task LoadAsync(string filePath, int startPos, int endPos)
- {
- _totalDuration = await GetDurationAsync(filePath);
- double startSec = BytesToSec(startPos);
- double endSec = endPos > 0 ? BytesToSec(endPos) : _totalDuration;
- if (this.FindControl<TextBlock>("DurationLabel") is TextBlock lbl)
- lbl.Text = $"Duration: {_totalDuration:F2}s | Drag to select trim region";
- _suppressChange = true;
- if (this.FindControl<NumericUpDown>("StartSec") is NumericUpDown sb)
- {
- sb.Maximum = (decimal)_totalDuration;
- sb.Value = (decimal)startSec;
- }
- if (this.FindControl<NumericUpDown>("EndSec") is NumericUpDown eb)
- {
- eb.Maximum = (decimal)_totalDuration;
- eb.Value = (decimal)endSec;
- }
- _suppressChange = false;
- var peaks = await Task.Run(() => ComputePeaks(filePath, 4096));
- var wf = this.FindControl<WaveformControl>("Waveform");
- if (wf == null) return;
- wf.SetPeaks(peaks);
- wf.SetSelection(
- _totalDuration > 0 ? startSec / _totalDuration : 0,
- _totalDuration > 0 ? endSec / _totalDuration : 1);
- wf.SelectionChanged += (startNorm, endNorm) =>
- {
- double s = startNorm * _totalDuration;
- double e = endNorm * _totalDuration;
- _suppressChange = true;
- if (this.FindControl<NumericUpDown>("StartSec") is NumericUpDown nsb) nsb.Value = (decimal)s;
- if (this.FindControl<NumericUpDown>("EndSec") is NumericUpDown neb) neb.Value = (decimal)e;
- _suppressChange = false;
- };
- }
- private static async Task<double> GetDurationAsync(string filePath)
- {
- try
- {
- var psi = new ProcessStartInfo("ffprobe") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
- psi.ArgumentList.Add("-v"); psi.ArgumentList.Add("quiet");
- psi.ArgumentList.Add("-show_entries"); psi.ArgumentList.Add("format=duration");
- psi.ArgumentList.Add("-of"); psi.ArgumentList.Add("csv=p=0");
- psi.ArgumentList.Add(filePath);
- using var proc = Process.Start(psi)!;
- var s = await proc.StandardOutput.ReadToEndAsync();
- await proc.WaitForExitAsync();
- return double.TryParse(s.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0;
- }
- catch { return 0; }
- }
- private static (float Low, float High)[] ComputePeaks(string filePath, int binCount)
- {
- try
- {
- var psi = new ProcessStartInfo("ffmpeg") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
- psi.ArgumentList.Add("-i"); psi.ArgumentList.Add(filePath);
- psi.ArgumentList.Add("-f"); psi.ArgumentList.Add("f32le");
- psi.ArgumentList.Add("-ar"); psi.ArgumentList.Add("8000");
- psi.ArgumentList.Add("-ac"); psi.ArgumentList.Add("1");
- psi.ArgumentList.Add("-loglevel"); psi.ArgumentList.Add("quiet");
- psi.ArgumentList.Add("pipe:1");
- using var proc = Process.Start(psi)!;
- using var ms = new MemoryStream();
- proc.StandardOutput.BaseStream.CopyTo(ms);
- proc.WaitForExit();
- var bytes = ms.ToArray();
- int totalSamples = bytes.Length / 4;
- if (totalSamples == 0) return Array.Empty<(float, float)>();
- var samples = MemoryMarshal.Cast<byte, float>(bytes.AsSpan());
- int actualBins = Math.Min(binCount, totalSamples);
- var peaks = new (float Low, float High)[actualBins];
- for (int i = 0; i < actualBins; i++)
- {
- int binStart = (int)((long)i * totalSamples / actualBins);
- int binEnd = (int)((long)(i + 1) * totalSamples / actualBins);
- float lo = 0f, hi = 0f;
- for (int j = binStart; j < binEnd; j++)
- {
- float v = samples[j];
- if (v < lo) lo = v;
- if (v > hi) hi = v;
- }
- peaks[i] = (lo, hi);
- }
- return peaks;
- }
- catch { return Array.Empty<(float, float)>(); }
- }
- private void Time_Changed(object? sender, NumericUpDownValueChangedEventArgs e)
- {
- if (_suppressChange || _totalDuration <= 0) return;
- var wf = this.FindControl<WaveformControl>("Waveform");
- if (wf == null) return;
- double s = (double)(this.FindControl<NumericUpDown>("StartSec")?.Value ?? 0m);
- double en = (double)(this.FindControl<NumericUpDown>("EndSec")?.Value ?? (decimal)_totalDuration);
- wf.SetSelection(s / _totalDuration, en / _totalDuration);
- }
- private void StopPreview()
- {
- _isPreviewing = false;
- try { _previewFf?.Kill(); } catch { }
- try { _previewPa?.Kill(); } catch { }
- _previewFf = null;
- _previewPa = null;
- }
- private void Play_Click(object? sender, RoutedEventArgs e)
- {
- if (_isPreviewing)
- {
- StopPreview();
- if (this.FindControl<Button>("PlayButton") is Button sb) sb.Content = "▶ Preview";
- return;
- }
- double startSec = (double)(this.FindControl<NumericUpDown>("StartSec")?.Value ?? 0m);
- double endSec = (double)(this.FindControl<NumericUpDown>("EndSec")?.Value ?? (decimal)_totalDuration);
- var ffPsi = new ProcessStartInfo("ffmpeg") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
- ffPsi.ArgumentList.Add("-ss"); ffPsi.ArgumentList.Add(startSec.ToString("F5", CultureInfo.InvariantCulture));
- ffPsi.ArgumentList.Add("-i"); ffPsi.ArgumentList.Add(_filePath);
- if (endSec > startSec)
- {
- ffPsi.ArgumentList.Add("-t");
- ffPsi.ArgumentList.Add((endSec - startSec).ToString("F5", CultureInfo.InvariantCulture));
- }
- ffPsi.ArgumentList.Add("-f"); ffPsi.ArgumentList.Add("s16le");
- ffPsi.ArgumentList.Add("-ar"); ffPsi.ArgumentList.Add("48000");
- ffPsi.ArgumentList.Add("-ac"); ffPsi.ArgumentList.Add("1");
- ffPsi.ArgumentList.Add("-loglevel"); ffPsi.ArgumentList.Add("quiet");
- ffPsi.ArgumentList.Add("pipe:1");
- var paPsi = new ProcessStartInfo("pacat") { UseShellExecute = false, RedirectStandardInput = true, CreateNoWindow = true };
- paPsi.ArgumentList.Add("--playback");
- paPsi.ArgumentList.Add("--rate=48000");
- paPsi.ArgumentList.Add("--channels=1");
- paPsi.ArgumentList.Add("--format=s16le");
- Process ff, pa;
- try
- {
- ff = Process.Start(ffPsi)!;
- pa = Process.Start(paPsi)!;
- }
- catch { return; }
- _previewFf = ff;
- _previewPa = pa;
- _isPreviewing = true;
- if (this.FindControl<Button>("PlayButton") is Button btn) btn.Content = "■ Stop";
- Task.Run(async () =>
- {
- // ffmpeg pipes data into pacat and exits quickly; pacat buffers and plays it out in real time
- try { await ff.StandardOutput.BaseStream.CopyToAsync(pa.StandardInput.BaseStream); } catch { }
- try { pa.StandardInput.Close(); } catch { }
- // wait for pacat to finish playing before resetting the button
- ff.WaitForExit();
- pa.WaitForExit();
- Dispatcher.UIThread.Post(() =>
- {
- _isPreviewing = false;
- _previewFf = null;
- _previewPa = null;
- if (this.FindControl<Button>("PlayButton") is Button b) b.Content = "▶ Preview";
- });
- });
- }
- private void Reset_Click(object? sender, RoutedEventArgs e)
- {
- _suppressChange = true;
- if (this.FindControl<NumericUpDown>("StartSec") is NumericUpDown sb) sb.Value = 0m;
- if (this.FindControl<NumericUpDown>("EndSec") is NumericUpDown eb) eb.Value = (decimal)_totalDuration;
- _suppressChange = false;
- if (this.FindControl<WaveformControl>("Waveform") is WaveformControl wf)
- wf.SetSelection(0, 1);
- }
- private void OK_Click(object? sender, RoutedEventArgs e)
- {
- double startSec = (double)(this.FindControl<NumericUpDown>("StartSec")?.Value ?? 0m);
- double endSec = (double)(this.FindControl<NumericUpDown>("EndSec")?.Value ?? (decimal)_totalDuration);
- StartPos = startSec > 0.001 ? SecToBytes(startSec) : 0;
- // EndPos=0 means "play to end" - only set if user trimmed it short
- EndPos = (_totalDuration > 0 && endSec < _totalDuration - 0.1) ? SecToBytes(endSec) : 0;
- Confirmed = true;
- Close();
- }
- private void Cancel_Click(object? sender, RoutedEventArgs e) => Close();
- protected override void OnClosed(EventArgs e)
- {
- StopPreview();
- base.OnClosed(e);
- }
- }
|