OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / WpfDisplay / Views / Animation / Spectrogram.xaml.cs

103 lines · csharp ref:
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

using WpfDisplay.Helper;
using WpfDisplay.Models;

namespace WpfDisplay.Views.Animation;

/// <summary>
/// Interaction logic for Spectrogram.xaml
/// </summary>
public partial class Spectrogram : UserControl
{
    private CancellationTokenSource _drawingCts = new();

    public IAudio Audio
    {
        get => (IAudio)GetValue(AudioProperty);
        set => SetValue(AudioProperty, value);
    }
    public static readonly DependencyProperty AudioProperty =
        DependencyProperty.Register("Audio", typeof(IAudio), typeof(Spectrogram), new FrameworkPropertyMetadata(null, OnSpectrogramDependencyChanged));

    public int SelectedAudioChannelId
    {
        get => (int)GetValue(SelectedAudioChannelIdProperty);
        set => SetValue(SelectedAudioChannelIdProperty, value);
    }
    public static readonly DependencyProperty SelectedAudioChannelIdProperty =
        DependencyProperty.Register("SelectedAudioChannelId", typeof(int), typeof(Spectrogram), new PropertyMetadata(0));

    public float ViewScale
    {
        get => (float)GetValue(ViewScaleProperty);
        set => SetValue(ViewScaleProperty, value);
    }
    public static readonly DependencyProperty ViewScaleProperty =
        DependencyProperty.Register("ViewScale", typeof(float), typeof(Spectrogram), new FrameworkPropertyMetadata(120.0f, OnSpectrogramDependencyChanged));

    private static void OnSpectrogramDependencyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => d.Dispatcher.InvokeAsync(((Spectrogram)d).RedrawSpectrogramVisuals);

    public Spectrogram()
    {
        InitializeComponent();
    }

    private async Task RedrawSpectrogramVisuals()
    {
        if (Audio is not null)
        {
            await _drawingCts.CancelAsync();
            _drawingCts = new CancellationTokenSource();
            var audio = Audio;
            var viewScale = ViewScale;
            var channelId = SelectedAudioChannelId;
            Width = (int)(viewScale * audio.Length);
            spectrogramImage.Source = await Task.Run(() => DrawSpectrogramImage(audio, viewScale, channelId, _drawingCts.Token)) ?? spectrogramImage.Source;
        }
    }

    private static BitmapSource? DrawSpectrogramImage(IAudio audio, float viewScale, int channelId, CancellationToken cancellationToken = default)
    {
        //TODO: propdp?
        const int displayStartFreq = 4;
        const int displayEndFreqMax = 20000;

        var displayEndFreq = Math.Min(displayEndFreqMax, audio.SampleRate * 0.95) / 2.0;

        int wres = (int)(viewScale * audio.Length);
        int hres = CavernHelper.defaultSamplingResolution / 2;
        byte[] pxs = new byte[wres * hres];
        for (int t = 0; t < wres; t++)
        {
            if (cancellationToken.IsCancellationRequested)
                return null;

            double t01 = t / (double)wres * audio.Length;
            var spectrum = audio.GetLogSpectrum(channelId, t01, displayStartFreq, displayEndFreq);
            for (int f = 0; f < spectrum.Length; f++)
            {
                if (cancellationToken.IsCancellationRequested)
                    return null;

                var s = Math.Pow(spectrum[f], 0.1);
                var c = (byte)(255 * Math.Clamp(s, 0, 1));
                pxs[t + (spectrum.Length - 1 - f) * wres] = c;
            }
        }
        var bmp = BitmapSource.Create(wres, hres, 0, 0, PixelFormats.Indexed8, SpectrogramPalettes.Viridis, pxs, wres);
        bmp.Freeze();
        return bmp;
    }

    //TODO: on click: log scale -> linear scale, SweepGenerator.ExponentialFreqs(4, endFreq, samples.Length / 2);

}