* Mono Synth MVP A Rust library that drives a GDNative script and exposes an interface for a monophonic Synth object. This object will produce audio frames which will fill Godot's ~AudioStreamPlayback~ buffer, leveraging the cross-platform audio I/O that Godot provides. The Rust dynamic library will need to be built for each platform, but it will leverage Rust's perfomance optimizations to enable realtime audio interactions. ** Godot Synth Interface From Godot, we're primarily interested in manipulating various aspects of the Mono Synth. Frequency, modulation, amplitude, and any other expressive aspect of a single tone we can hook up. Overall volume will be controlled by Godot. *** Input - *Sample Rate* - Godot must send its I/O sample rate to the synth when initialized This is the number of audio samples played back per second. So we need to generate samples at the same rate. - *Frequency* - range from 20Hz to ½ the sample rate. increment may need to change over the range. Human ear detects small changes at lower frequencies, but not as much in the higher frequencies. - *Continuous* - whether or not to produce a continuous tone or use ADSR envelope - *Waveform* - a selection of basic waveforms will be implemented in Rust. We can control which one is being generated via a switch in Godot. - *Envelope* - this controls the volume of sound over time. Typical envelopes have parameters for: - *Attack* - amount of time before the waveform is at its peak - *Decay* - amount of time it takes to go from peak to sustained level - *Sustain* - the level (amplitude) to sustain while a note is held - *Release* - the amount of time to go to 0 from sustain when note is released - *Frequency Modulator* - an oscillator that modulates the main tone generator which has the effect of producing more complex sounds with sidebands of the main signal. It has two controllable parameters: - *Frequency multiple* integers create harmonic sidebands while non-integers make inharmonic tones similar to a bell or metallic sound. - *FM Depth* the amount of modulation to apply. Increasing by a lot can make some interesting effects Not implemented (yet): - *Filter* - a low pass filter with a two parameters: - *Cutoff* the frequency where the filter begins - *Resonance* ? i think its like an amplitude boost at the cutoff? *** Output Currently, the output is an array of (sample, sample) vectors, where each sample is a float between -1.0 and 1.0. You can request a certain number of frames, and get back the calculated samples to fill them. We can think of this as essentially the stereo audio jack from a synthesizer. * Signal Generator Model In Puredata, one of the key abstractions is the Signal object, which all of the audio specific related bits implement. Signals abstract away time, and you can build an audio pipleine by connecting them together. A phasor drives an oscillator which sends its signal through a line amplitude signal to shape its volume. [[file:diagrams/pd-phasor-oscillator.png]] This is roughly what we've built with Rust as iterators. Each ~.next()~ is like a tick of the =phasor~= object. =cos~= calculates the cosine value like our the ~impl Iterator for Osc~ does in its ~.next()~ function. =line~=, when triggered by an input, produces a ramp from 0 to 1 over 1000 ms. It also "ticks" at the same rate as the other signals (the underlying sample rate). Each sample value from the cosine is multiplied by the values from the line signal. At the end of the 1000ms, the =*~= object is now constantly at the max amplitude. ** Iterator as Signal ? Assuming we're calling ~next()~ on each part of our signal chain in the same cycle, at the same rate, we can utilize Rust's =Iterator= trait as our version of the Signal. This is how its working now. It is possible this will only get us so far, but I think we can roll with it. The currently implemented [[../klangfarbrs/src/envelope.rs][Envelope]] module is actually wrapping three of the =line~= type objects. ~attack~ ~decay~ and ~release~ could all be described by a ~Line~ struct that has a target amplitude and a duration. * Instrument (basic) An Instrument is N Sine(?) waves with an Envelope applied to it. (OscBank, Envelope) it implements Iterator so that ~next()~ sums the oscillators, scales them down by 1/N, and multiplies by the Envelope value. ** Partial The four arguments to each invocation of the partial abstraction specify: amplitude. The amplitude of the partial at its peak, at the end of the attack and the beginning of the decay of the note. relative duration. This is multiplied by the overall note duration (controlled in the main patch) to determine the duration of the decay portion of the sinusoid. Individual partials may thus have different decay times, so that some partials die out faster than others, under the main patch's overall control. relative frequency. As with the relative duration, this controls each partial's frequency as a multiple of the overall frequency controlled in the main patch. detune. A frequency in Hertz to be added to the product of the global frequency and the relative frequency. Inside the partial abstraction, the amplitude is simply taken directly from the ``$1" argument (multiplying by 0.1 to adjust for the high individual amplitudes); the duration is calculated from the r duration object, multiplying it by the ``$2" argument. The frequency is computed as $fp+d$ where $f$ is the global frequency (from the r frequency object), $p$ is the relative frequency of the partial, and $d$ is the detune frequency.