WIP: waveform visualization
This commit is contained in:
88
Source/AudioFileAnalyzer.cs
Normal file
88
Source/AudioFileAnalyzer.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Godot;
|
||||
using NAudio.Wave;
|
||||
|
||||
namespace AudioEditor;
|
||||
|
||||
public partial class AudioFileAnalyzer : Node
|
||||
{
|
||||
public static AudioFileAnalyzer Instance { get; private set; }
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public void AnalyzeFile(string path, out WaveformInfo waveformInfo)
|
||||
{
|
||||
waveformInfo = new();
|
||||
|
||||
if (_waveformInfoCache.ContainsKey(path))
|
||||
{
|
||||
waveformInfo = _waveformInfoCache[path];
|
||||
return;
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(path);
|
||||
|
||||
switch (ext)
|
||||
{
|
||||
case ".wav":
|
||||
GD.Print("Analyzing a WAV file...");
|
||||
AnalyzeWav(path, out waveformInfo);
|
||||
break;
|
||||
default:
|
||||
GD.Print($"Format {ext} is not supported!");
|
||||
break;
|
||||
}
|
||||
|
||||
_waveformInfoCache.Add(path, waveformInfo);
|
||||
}
|
||||
|
||||
public bool TryGetFromCache(string path, out WaveformInfo waveformInfo)
|
||||
{
|
||||
return _waveformInfoCache.TryGetValue(path, out waveformInfo);
|
||||
}
|
||||
|
||||
private void AnalyzeWav(string path, out WaveformInfo waveformInfo)
|
||||
{
|
||||
waveformInfo = new WaveformInfo();
|
||||
using (var reader = new WaveFileReader(path))
|
||||
{
|
||||
waveformInfo.Length = reader.TotalTime;
|
||||
waveformInfo.SampleRate = reader.WaveFormat.SampleRate;
|
||||
|
||||
long sampleCount = reader.Length / reader.WaveFormat.BlockAlign;
|
||||
waveformInfo.Samples = new float[sampleCount];
|
||||
|
||||
// Render the waveform
|
||||
byte[] buffer = new byte[reader.WaveFormat.BlockAlign];
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
int sampleIndex = i * reader.WaveFormat.BlockAlign;
|
||||
while (reader.Position < reader.Length)
|
||||
{
|
||||
// Read the raw bytes for the current sample
|
||||
reader.Read(buffer, 0, buffer.Length);
|
||||
|
||||
// Convert the byte array to a 16-bit sample (assuming 16-bit PCM)
|
||||
short sample = BitConverter.ToInt16(buffer, 0); // Assuming 16-bit signed samples
|
||||
|
||||
waveformInfo.Samples[sampleIndex++] = (float)sample / short.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, WaveformInfo> _waveformInfoCache = new();
|
||||
}
|
||||
|
||||
public class WaveformInfo
|
||||
{
|
||||
public int SampleRate { get; set; }
|
||||
public TimeSpan Length { get; set; }
|
||||
public float[] Samples { get; set; }
|
||||
}
|
||||
@@ -6,21 +6,46 @@ namespace AudioEditor;
|
||||
[GlobalClass]
|
||||
public partial class ProjectController : Node
|
||||
{
|
||||
[Signal] public delegate void AudioClipDroppedEventHandler(Vector2 atPosition, string clipName, double startTime, double endTime);
|
||||
[Signal] public delegate void AudioClipDroppedEventHandler(Vector2 atPosition, string path, string clipName, double startTime, double endTime);
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
GetWindow().FilesDropped += FilesDropped;
|
||||
}
|
||||
|
||||
public void CreateAudioClipPreview(Control audioClip)
|
||||
{
|
||||
// GD.Print(audioClip.Clas());
|
||||
var path = audioClip.Get("clip_path").AsString();
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AudioFileAnalyzer.Instance.TryGetFromCache(path, out WaveformInfo waveformInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioClip.HasNode("%Waveform"))
|
||||
{
|
||||
var waveform = audioClip.GetNode<Waveform>("%Waveform");
|
||||
waveform.SetWaveformInfo(waveformInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void FilesDropped(string[] files)
|
||||
{
|
||||
var mousePosition = GetViewport().GetMousePosition();
|
||||
|
||||
var fileName = Path.GetFileName(files[0]);
|
||||
var path = files[0];
|
||||
var fileName = Path.GetFileName(path);
|
||||
|
||||
GD.Print(fileName);
|
||||
AudioFileAnalyzer.Instance.AnalyzeFile(path, out WaveformInfo waveformInfo);
|
||||
|
||||
EmitSignal(SignalName.AudioClipDropped, mousePosition, fileName, 0, 10 * 1000);
|
||||
GD.Print(path);
|
||||
|
||||
EmitSignal(SignalName.AudioClipDropped, mousePosition, path, fileName, 0, waveformInfo.Length.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
101
Source/Waveform.cs
Normal file
101
Source/Waveform.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Godot;
|
||||
using System;
|
||||
|
||||
namespace AudioEditor;
|
||||
|
||||
public partial class Waveform : Control
|
||||
{
|
||||
private WaveformInfo _waveform;
|
||||
|
||||
public void SetWaveformInfo(WaveformInfo waveform)
|
||||
{
|
||||
_waveform = waveform;
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
public override void _Draw()
|
||||
{
|
||||
if (_waveform == null) return;
|
||||
base._Draw();
|
||||
|
||||
int width = (int)Size.X;
|
||||
int height = (int)Size.Y;
|
||||
|
||||
// Downsample the waveform to get a reasonable number of peaks to draw
|
||||
float[] averagePeaks = DownsampleToAveragePeaks(_waveform.Samples, width, _waveform.SampleRate);
|
||||
|
||||
// Begin drawing the waveform
|
||||
for (int i = 0; i < width; i++)
|
||||
{
|
||||
float averagePeak = averagePeaks[i];
|
||||
|
||||
// Normalize the average peak to fit within the height of the control
|
||||
int y = (int)(averagePeak * height / 2);
|
||||
|
||||
// Drawing the line in the positive and negative direction from the center
|
||||
DrawLine(new Vector2(i, height / 2 - y), new Vector2(i, height / 2 + y), new Color(1, 1, 1, 0.7f)); // White with transparency
|
||||
}
|
||||
}
|
||||
|
||||
private float[] DownsampleToAveragePeaks(float[] samples, int targetWidth, int sampleRate)
|
||||
{
|
||||
int totalSamples = samples.Length;
|
||||
float durationInSeconds = totalSamples / (float)sampleRate; // Calculate the total duration in seconds
|
||||
int stepSize = totalSamples / targetWidth; // Larger chunks for downsampling
|
||||
|
||||
// If stepSize is 0 (due to rounding), default to 1 to prevent division by zero
|
||||
if (stepSize == 0) stepSize = 1;
|
||||
|
||||
float[] averagePeaks = new float[targetWidth];
|
||||
|
||||
for (int i = 0; i < targetWidth; i++)
|
||||
{
|
||||
int startIndex = i * stepSize;
|
||||
int endIndex = Math.Min(startIndex + stepSize, totalSamples);
|
||||
|
||||
// Calculate the average or peak for this window (you can use GetPeak if needed)
|
||||
averagePeaks[i] = GetAverage(samples, startIndex, endIndex);
|
||||
}
|
||||
|
||||
return averagePeaks;
|
||||
}
|
||||
|
||||
// Get the average value of samples in the current window
|
||||
private float GetAverage(float[] samples, int startIndex, int endIndex)
|
||||
{
|
||||
float sum = 0f;
|
||||
int count = 0;
|
||||
for (int i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
sum += Math.Abs(samples[i]); // Use absolute values for consistency
|
||||
count++;
|
||||
}
|
||||
return count > 0 ? sum / count : 0f;
|
||||
}
|
||||
|
||||
// Optional: You could use this method if you prefer showing the peak values
|
||||
private float GetPeak(float[] samples, int startIndex, int endIndex)
|
||||
{
|
||||
float maxPeak = 0f;
|
||||
for (int i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
maxPeak = Math.Max(maxPeak, Math.Abs(samples[i]));
|
||||
}
|
||||
return maxPeak;
|
||||
}
|
||||
|
||||
// This can be an alternative to GetAverage if you want to show average peak
|
||||
private float GetAveragePeak(float[] samples, int startIndex, int endIndex)
|
||||
{
|
||||
float sum = 0f;
|
||||
int count = 0;
|
||||
|
||||
for (int i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
sum += Math.Abs(samples[i]); // Use absolute value for a more stable peak
|
||||
count++;
|
||||
}
|
||||
|
||||
return count > 0 ? sum / count : 0f; // Return the average
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user