summaryrefslogtreecommitdiff
path: root/klangfarbrs/src/lib.rs
blob: 7954eb2fdff3db0a91fb8c0949dad63610ed3fbd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! # Rust audio oscillator for the Godot game engine
//!
//! This crate contains logic for generating samples for audio wave forms which are then
//! used to fill Godot's `AudioStreamPlayback` buffers. Scripts using this code as a dynamic
//! library will be able to request a certain number of frames (represented as a `Vector2`)
//! at a specific frequency. Because of how the Godot bindings work, the wave structs will
//! have a default sample rate at 48kHz. You'll want to set it in your script's `_ready`
//! function to match the sample rate in Godot.

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 Osc {
    pub waveform: Waveform,
    pub sample_rate: f32,
    phase: f32,
}

/// The various waveforms the `Osc` can generate.
pub 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()
        },
        Waveform::Square => {
            if osc.phase < 0.5 {
                -1.0
            } else {
                1.0
            }
        }
    }
}

/// 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::Osc;
/// let mut wave = Osc { sample_rate: 24000.0, phase: 0.0 };
/// assert_eq!(wave.sample_rate, 24000.0);
/// ```
#[methods]
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 { waveform: Waveform::Sine, sample_rate: 48000.0, phase: 0.0 }
    }

    #[export]
    fn _ready(&self, _owner: &Node) {
        godot_print!("DAS IST KLANGFARBRS.")
    }

    #[export]
    fn sine(&mut self, _owner: &Node) {
        self.waveform = Waveform::Sine
    }

    #[export]
    fn square(&mut self, _owner: &Node) {
        self.waveform = Waveform::Square
    }

    #[export]
    pub fn set_sample_rate(&mut self, _owner: &Node, sample_rate: f32) {
        self.sample_rate = sample_rate;
    }

    #[export]
    pub fn frames(&mut self, _owner: &Node, frequency: f32, duration: i32) -> TypedArray<Vector2> {
        let mut frames = TypedArray::new();

        for _i in 0..duration {
            let sample = generate_sample(&self);
            frames.push(Vector2::new(sample, sample));
            self.phase = calculate_phase(&self, frequency);
        }

        return frames
    }
}

// Function that registers all exposed classes to Godot
fn init(handle: InitHandle) {
    // Register the `Osc` type we declared.
    handle.add_class::<Osc>();
}

// Macro that creates the entry-points of the dynamic library.
godot_init!(init);