From b9559897c05b8c2f67e34372685952d586af5c3d Mon Sep 17 00:00:00 2001 From: Grant Shangreaux Date: Sat, 30 Oct 2021 23:28:56 -0500 Subject: Add: restructured Osc class preparing for more wave forms --- klangfarb/Osc.gdns | 8 +++++++ klangfarb/SineWave.gdns | 8 ------- klangfarb/main.gd | 4 ++-- klangfarbrs/src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 klangfarb/Osc.gdns delete mode 100644 klangfarb/SineWave.gdns diff --git a/klangfarb/Osc.gdns b/klangfarb/Osc.gdns new file mode 100644 index 0000000..65d44ef --- /dev/null +++ b/klangfarb/Osc.gdns @@ -0,0 +1,8 @@ +[gd_resource type="NativeScript" load_steps=2 format=2] + +[ext_resource path="res://klangfarbrs.gdnlib" type="GDNativeLibrary" id=1] + +[resource] +resource_name = "Osc" +class_name = "Osc" +library = ExtResource( 1 ) diff --git a/klangfarb/SineWave.gdns b/klangfarb/SineWave.gdns deleted file mode 100644 index e220a2e..0000000 --- a/klangfarb/SineWave.gdns +++ /dev/null @@ -1,8 +0,0 @@ -[gd_resource type="NativeScript" load_steps=2 format=2] - -[ext_resource path="res://klangfarbrs.gdnlib" type="GDNativeLibrary" id=1] - -[resource] -resource_name = "SineWave" -class_name = "SineWave" -library = ExtResource( 1 ) diff --git a/klangfarb/main.gd b/klangfarb/main.gd index fb29c8c..f91dbb7 100644 --- a/klangfarb/main.gd +++ b/klangfarb/main.gd @@ -4,9 +4,9 @@ extends AudioStreamPlayer export(float, 20, 8000, 10) var freq = 440.0 # load the GDNative script connected to the rust lib -var SineWave = preload("res://SineWave.gdns") +var Osc = preload("res://Osc.gdns") # make an instance of our one "class" in rust lib -var wave = SineWave.new() +var wave = Osc.new() # initialize the Godot stream we fill up with samples var playback: AudioStreamPlayback = null diff --git a/klangfarbrs/src/lib.rs b/klangfarbrs/src/lib.rs index ba201f0..a418995 100644 --- a/klangfarbrs/src/lib.rs +++ b/klangfarbrs/src/lib.rs @@ -11,25 +11,66 @@ use gdnative::prelude::*; use gdnative::core_types::TypedArray; use std::f32::consts::TAU; +/// This struct is used as a class in Godot. It is a "numerically controlled oscillator" +/// which is driven by a phasor. The sample rate and waveform should be set after you +/// create a new instance in GDScript. #[derive(NativeClass)] #[inherit(Node)] -pub struct SineWave { +pub struct Osc { + pub waveform: Waveform, pub sample_rate: f32, - pub phase: f32 + phase: f32, +} + +/// The various waveforms the `Osc` can generate. +enum Waveform { + Sine, + // Square, + // Triangle, + // Saw, + // Noise, +} + +/// Generates the next sample for an oscillator based on its waveform. +fn generate_sample(osc: &Osc) -> f32 { + match osc.waveform { + Waveform::Sine => { + (TAU * osc.phase).sin() + } + } +} + +/// Phase stays between 0.0 and 1.0 and represents position on the axis of time +/// for a given wave form. Since audio signals are periodic, we can just calculate +/// the first cycle of a wave repeatedly. This also prevents pitch drift caused by +/// floating point errors over time. +fn calculate_phase(osc: &Osc, frequency: f32) -> f32 { + (osc.phase + (frequency / osc.sample_rate)) % 1.0 } /// # Examples /// +/// It is more work than benefit to figure out how to instantiate a Godot object (Node) +/// that does not behave as typical Rust. However, I wanted to try out the feature of +/// examples in the documentation that run as automated tests. :galaxy-brain: +/// /// ``` -/// use klangfarbrs::SineWave; -/// let mut wave = SineWave { sample_rate: 24000.0, phase: 0.0 }; +/// use klangfarbrs::Osc; +/// let mut wave = Osc { sample_rate: 24000.0, phase: 0.0 }; /// assert_eq!(wave.sample_rate, 24000.0); /// ``` - #[methods] -impl SineWave { +impl Osc { + /// # Examples + /// + /// ```gdscript + /// var Osc = preload("res://Osc.gdns") + /// var wave = Osc.new() + /// wave.set_sample_rate(24000.0) + /// wave.square() # changes to a square wave + /// ``` pub fn new(_owner: &Node) -> Self { - Self { sample_rate: 48000.0, phase: 0.0 } + Self { waveform: Waveform::Sine, sample_rate: 48000.0, phase: 0.0 } } #[export] @@ -47,9 +88,9 @@ impl SineWave { let mut frames = TypedArray::new(); for _i in 0..duration { - let sample = (TAU * self.phase).sin().clamp(-1.0, 1.0); + let sample = generate_sample(&self); frames.push(Vector2::new(sample, sample)); - self.phase = (self.phase + (frequency / self.sample_rate)) % 1.0; + self.phase = calculate_phase(&self, frequency); } return frames @@ -58,8 +99,8 @@ impl SineWave { // Function that registers all exposed classes to Godot fn init(handle: InitHandle) { - // Register the `Sine` type we declared. - handle.add_class::(); + // Register the `Osc` type we declared. + handle.add_class::(); } // Macro that creates the entry-points of the dynamic library. -- cgit v1.2.3