diff --git a/AudioEditor.csproj b/AudioEditor.csproj
index 53cc1f1..37f6f33 100644
--- a/AudioEditor.csproj
+++ b/AudioEditor.csproj
@@ -5,4 +5,7 @@
net8.0
true
+
+
+
\ No newline at end of file
diff --git a/Controls/AudioClip.tscn b/Controls/AudioClip.tscn
index 3279a03..41bc0c4 100644
--- a/Controls/AudioClip.tscn
+++ b/Controls/AudioClip.tscn
@@ -1,6 +1,7 @@
-[gd_scene load_steps=3 format=3 uid="uid://dmmgalpx4fcc7"]
+[gd_scene load_steps=4 format=3 uid="uid://dmmgalpx4fcc7"]
[ext_resource type="Script" path="res://Scripts/AudioClip.gd" id="1_iy5jd"]
+[ext_resource type="Script" path="res://Source/Waveform.cs" id="2_43oho"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8tb17"]
border_width_left = 2
@@ -65,4 +66,16 @@ offset_right = 40.0
offset_bottom = 23.0
text = "Clip properties!"
+[node name="Waveform" type="Control" parent="."]
+unique_name_in_owner = true
+modulate = Color(0.779291, 0.779291, 0.779291, 1)
+layout_mode = 1
+anchor_top = 0.312
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_top = 0.0319996
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2_43oho")
+
[connection signal="on_double_click" from="." to="Window" method="popup_centered"]
diff --git a/Scripts/AudioClip.gd b/Scripts/AudioClip.gd
index 6216937..c97f42d 100644
--- a/Scripts/AudioClip.gd
+++ b/Scripts/AudioClip.gd
@@ -10,6 +10,8 @@ signal on_selected
signal on_deselected
signal on_double_click
+var clip_path: String
+
var timeline: Timeline
var dragging: bool
diff --git a/Scripts/Timeline.gd b/Scripts/Timeline.gd
index cd2bc39..6fac0ae 100644
--- a/Scripts/Timeline.gd
+++ b/Scripts/Timeline.gd
@@ -20,6 +20,8 @@ extends Container
@export var audio_clip_scene: PackedScene
+signal clip_added(clip: Control)
+
func format_time_ms_hours(ms: float) -> String:
var total_seconds = ms / 1000
var hours = int(total_seconds / 3600)
@@ -141,21 +143,24 @@ func local_x_to_timeline(x: float) -> float:
return (x + time_offset) / get_pixels_per_unit() * time_interval
-func clip_dropped(at_position: Vector2, clip_name: String, clip_start_time: float, clip_end_time: float):
+func clip_dropped(at_position: Vector2, path: String, clip_name: String, clip_start_time: float, clip_end_time: float):
var local_position = at_position - global_position
var timeline_position = local_x_to_timeline(local_position.x)
var track_idx = get_track_idx_by_y(at_position.y)
- add_audio_clip(clip_name, track_idx, timeline_position + clip_start_time, timeline_position + clip_end_time)
+ add_audio_clip(path, clip_name, track_idx, timeline_position + clip_start_time, timeline_position + clip_end_time)
pass
-func add_audio_clip(clip_name: String, track_idx: int, clip_start_time: float, clip_end_time: float):
+func add_audio_clip(path: String, clip_name: String, track_idx: int, clip_start_time: float, clip_end_time: float):
var audio_clip = audio_clip_scene.instantiate() as AudioClip
+ audio_clip.clip_path = path
audio_clip.clip_name = clip_name
audio_clip.track_idx = track_idx
audio_clip.start_time = clip_start_time
audio_clip.end_time = clip_end_time
add_child(audio_clip)
+
+ clip_added.emit(audio_clip)
queue_redraw()
pass
\ No newline at end of file
diff --git a/Source/AudioFileAnalyzer.cs b/Source/AudioFileAnalyzer.cs
new file mode 100644
index 0000000..987826f
--- /dev/null
+++ b/Source/AudioFileAnalyzer.cs
@@ -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 _waveformInfoCache = new();
+}
+
+public class WaveformInfo
+{
+ public int SampleRate { get; set; }
+ public TimeSpan Length { get; set; }
+ public float[] Samples { get; set; }
+}
\ No newline at end of file
diff --git a/Source/ProjectController.cs b/Source/ProjectController.cs
index 4a65fc4..1e2368f 100644
--- a/Source/ProjectController.cs
+++ b/Source/ProjectController.cs
@@ -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.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);
}
}
\ No newline at end of file
diff --git a/Source/Waveform.cs b/Source/Waveform.cs
new file mode 100644
index 0000000..75486b7
--- /dev/null
+++ b/Source/Waveform.cs
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/Views/MainView.tscn b/Views/MainView.tscn
index eef06f8..54c61fa 100644
--- a/Views/MainView.tscn
+++ b/Views/MainView.tscn
@@ -183,4 +183,5 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_3f3qp")
[node name="ProjectController" type="Node" parent="."]
script = ExtResource("11_gc3ui")
+[connection signal="clip_added" from="VBoxContainer/VSplitContainer/HSplitContainer/Timeline" to="ProjectController" method="CreateAudioClipPreview"]
[connection signal="AudioClipDropped" from="ProjectController" to="VBoxContainer/VSplitContainer/HSplitContainer/Timeline" method="clip_dropped"]
diff --git a/project.godot b/project.godot
index 97f81be..e59f50a 100644
--- a/project.godot
+++ b/project.godot
@@ -21,6 +21,10 @@ config/icon="res://icon.svg"
driver/driver="Dummy"
+[autoload]
+
+AudioFileAnalyzer="*res://Source/AudioFileAnalyzer.cs"
+
[display]
window/size/viewport_width=1920