summaryrefslogtreecommitdiff
path: root/docs/design.org
blob: c111716425d39f5f2210ffb786eac48d40fe0100 (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
* 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.