surgeosc-window

Crates.iosurgeosc-window
lib.rssurgeosc-window
version0.2.12-alpha.0
sourcesrc
created_at2021-10-26 00:10:09.50656
updated_at2023-04-07 22:22:23.598358
descriptionsurge synthesizer -- window oscillator
homepage
repositoryhttps://github.com/klebz/surge-rs
max_upload_size
id471461
size64,979
(klebs6)

documentation

README

surgeosc-window

A window oscillator component for the Surge synthesizer system.


surgeosc-window is a Rust crate that provides a window oscillator component as a subcomponent of the Surge synthesizer system. The window oscillator is responsible for generating audio signals based on a given waveform and various control parameters. It supports multiple control types, modulation, and different value types for a flexible and powerful audio synthesis experience.

The crate contains tokens that represent various aspects of the oscillator's behavior, such as control groups, value ranges, and processing functions. Some of the tokens have mathematical ideas associated with them, which are explained below.

Tokens and associated mathematical ideas

  • control_group: A group of related control parameters.

  • control_type: The type of control applied to the oscillator (e.g., frequency, amplitude).

  • default_value: The default value for a control parameter.

  • max_value: The maximum value for a control parameter.

  • min_value: The minimum value for a control parameter.

  • modulateable: Whether a control parameter can be modulated by external signals.

  • moverate: The rate at which a control parameter changes (e.g., the frequency of an LFO).

  • snap: Whether a control parameter should snap to specific values (e.g., quantization).

  • value_type: The type of value used for a control parameter (e.g., integer, float).

  • get_size_mask: Calculates the size mask for the oscillator, which is a bitwise mask used to determine the size of the waveform buffer.

    size_mask = 2^N - 1
    

    where N is the number of bits needed to represent the buffer size.

  • get_size_mask_win: Similar to get_size_mask, but for the window buffer.

  • get_table: Retrieves the waveform table for a specific control type.

  • get_wave: Retrieves the waveform data for the oscillator.

  • get_win: Retrieves the window data for the oscillator.

  • get_window: Retrieves the window function used to shape the oscillator's output.

  • create_wetblock: Creates a new wetblock, which is a buffer for storing the output of the oscillator before it's sent to the next processing stage.

  • process_sub_osc: Processes a single sub-oscillator within the window oscillator.

  • block_update_mono_out: Updates the oscillator's mono output buffer with new audio data.

  • block_update_stereo_out: Updates the oscillator's stereo output buffer with new audio data.

  • process_sub_oscs: Processes all the sub-oscillators within the window oscillator.

  • clear: Clears the oscillator's audio buffers.

  • WindowOscillator: The main struct representing the window oscillator.

  • set_pitch: Sets the pitch of the oscillator. The frequency f is related to the pitch p by the following equation:

    f = 440 * 2^((p - 69) / 12)
    

    where p is in MIDI note numbers.

  • init: Initializes the window oscillator.

  • get_formant_mul: Retrieves the formant multiplier for the oscillator, which is used to scale the frequencies of the oscillator's harmonics.

  • process_block: Processes a block of audio samples for the window oscillator.

  • SubOscProcessBlockCfg: Configuration struct for processing a sub-oscillator block.

  • SubOscProcessCfg: Configuration struct for processing a sub-oscillator.

  • process_oscillator_block: Processes a block of audio samples for a single oscillator.

2> thanks -- what is a window oscillator?

A window oscillator is a type of audio oscillator used in digital synthesis that combines multiple waveforms or "windows" to generate complex and evolving sounds. The basic concept is to take segments (windows) of different waveforms and blend them together in a controllable manner. This allows the synthesis of a wide variety of sounds by combining and modulating these windows in real-time.

Window oscillators can be used in various synthesis techniques, such as wavetable synthesis, granular synthesis, or additive synthesis. In wavetable synthesis, for example, a window oscillator may be used to smoothly transition between different waveforms stored in a table. By modulating the window position or the window size, new and interesting timbres can be created.

In the context of the surgeosc-window crate, the window oscillator is a component of the Surge synthesizer system that is responsible for generating audio signals based on the chosen waveform and various control parameters. It supports multiple control types, modulation, and different value types for a flexible and powerful audio synthesis experience.

3> could you explain the window oscillator struct?

The WindowOscillator struct represents the state and configuration of a window oscillator in the surgeosc-window crate. It contains various fields that store the oscillator's properties and configuration, as well as buffers for the generated audio signals.

Here's a breakdown of the fields in the WindowOscillator struct:

  • drift: A field representing the frequency drift of the oscillator, which can be used to simulate the natural pitch instability found in analog oscillators.

  • master_osc: A pointer to the master oscillator's buffer, which is used for frequency modulation (FM) purposes.

  • out: An OscillatorOut struct that contains the output buffers for the oscillator's generated audio signals.

  • pos, sub_pos, ratio, table, formant_mul: Arrays representing various oscillator parameters, such as position, sub-oscillator position, frequency ratio, waveform table index, and formant multiplier.

  • dispatch_delay: An array representing the sample delay for each sub-oscillator before playback should start, allowing for per-sample scheduling.

  • gain: A 2D array representing the gain for each sub-oscillator.

  • drift_lfo: A 2D array representing the low-frequency oscillation (LFO) values for frequency drift.

  • fm_ratio: A 2D array representing the frequency modulation ratios for each sub-oscillator.

  • params: A WindowOscillatorParamArrayRT struct that contains the real-time parameters for the window oscillator.

  • osc_params: An OscillatorParamArrayRT struct that contains the real-time parameters for the oscillator.

  • fm_depth: An array representing the frequency modulation depth for each sub-oscillator.

  • out_attenuation: A field representing the output attenuation of the oscillator.

  • detune_bias, detune_offset: Fields representing detuning bias and offset, which can be used to control the detuning of sub-oscillators relative to the main oscillator.

  • active_sub_oscs: An integer representing the number of active sub-oscillators.

  • window_wavetable, wave_wavetable: WaveTableBase<i16> structs representing the wavetables for the window and the waveform, respectively.

  • tables: A TablesHandle struct that provides access to shared wavetable data.

  • tuner: A TunerHandle struct that provides access to the tuner, which is used to calculate the oscillator's frequency based on its pitch.

  • srunit: A SampleRateHandle struct that provides access to the sample rate, which is used to convert frequency values to sample-based values.

The WindowOscillator struct also implements the SetPitch trait, which allows setting the oscillator's pitch. The set_pitch function updates the active number of sub-oscillators to 1 when the is_display flag is set.

4> what about how we process the sub oscillators?

The WindowOscillator implementation contains the process_sub_oscs function, which is responsible for processing all the active sub-oscillators within the window oscillator. The function takes two boolean parameters: stereo and fm, which indicate whether the oscillator should produce stereo output and whether frequency modulation (FM) should be applied.

Here's a step-by-step breakdown of what the process_sub_oscs function does:

  1. Retrieve the morph parameter value using self.pvalf(WindowOscillatorParam::Morph). The morph parameter is used to control the blend between different waveforms or windows.

  2. Call self.get_table(morph) to get the table index for the given morph value.

  3. Retrieve the formant parameter value using self.pvalf(WindowOscillatorParam::Formant), which is used to control the formant shaping of the oscillator's output. Then, call self.get_formant_mul(formant as f64) to calculate the formant multiplier.

  4. Retrieve the size masks for the oscillator and window buffers using self.get_size_mask() and self.get_size_mask_win(), respectively. These masks are used to wrap the buffer indices during processing.

  5. Call self.get_window() to get the current window function index.

  6. Retrieve the number of active sub-oscillators using self.active_sub_oscs as usize.

  7. Create a SubOscProcessCfg struct with the calculated values for table, formant multiplier, size masks, window index, and the input flags stereo and fm. This struct is used to configure the processing of each sub-oscillator.

  8. Iterate over the active sub-oscillators in a loop, calling self.process_sub_osc(so, &cfg) for each one. This function processes each sub-oscillator individually, generating audio signals based on their configuration and the shared configuration passed in the cfg struct.

In summary, the process_sub_oscs function sets up the configuration for processing the sub-oscillators, iterates over the active sub-oscillators, and processes them individually to generate the audio output. The function is designed to handle both mono and stereo output, as well as applying frequency modulation if desired.

5> what about each individual sub oscillator?

The process_sub_osc function in the WindowOscillator implementation processes a single sub-oscillator with a given index so and shared configuration subosc_cfg. The main purpose of this function is to generate the audio output for a single sub-oscillator based on its specific configuration and the shared configuration.

Here's a step-by-step breakdown of what the process_sub_osc function does:

  1. Extract the table, formant_mul, window, and fm fields from the subosc_cfg struct.

  2. Retrieve the current position of the sub-oscillator in the waveform buffer.

  3. Calculate the frequency ratio ratio_a for the sub-oscillator. If frequency modulation (FM) is enabled, ratio_a is set to the value from the fm_ratio array.

  4. Calculate the indices mipmap_a and mipmap_b for selecting the appropriate mipmap levels in the window and waveform wavetables, respectively. These indices are used to adapt the wavetable resolution based on the frequency ratio and formant multiplier.

  5. Get the pointers wave_adr and win_adr to the data in the waveform and window wavetables at the calculated mipmap levels and table indices.

  6. Create a SubOscProcessBlockCfg struct with the calculated values for ratio_a, mipmap_a, mipmap_b, wave_mipmap_idx, wave_adr, and win_adr. This struct is used to configure the processing of the sub-oscillator in the subsequent loop.

  7. Iterate over the audio block samples in a loop. For each sample, call the self.process_oscillator_block function, which processes a single block of the sub-oscillator using the current position, shared configuration, and sub-oscillator-specific configuration.

  8. Update the position of the sub-oscillator in the waveform buffer.

In summary, the process_sub_osc function calculates the necessary parameters for processing a single sub-oscillator, gets the pointers to the data in the wavetables, and iterates over the audio block samples to generate the output. This function is called by the process_sub_oscs function for each active sub-oscillator in the window oscillator.

6> what about how we manage scratch space?

The create_wetblock function in the WindowOscillator implementation is responsible for converting a 128-bit integer input (__m128i) into a WetBlock1 structure with 4 floating-point values. The main purpose of this function is to preprocess the input data before it's used in the oscillator processing.

Here's a step-by-step breakdown of what the create_wetblock function does:

  1. Create a mutable WetBlock1 structure with 4 elements, initialized to their default values.

  2. Check if the code is compiled for macOS using the cfg![macos] macro. The implementation differs based on the target platform:

    • On macOS: Use _mm_cvtsi128_si32 and _mm_shuffle_epi32 SSE2 intrinsics to extract the 32-bit integer values from the input __m128i and convert them to f32. Store the converted values in the wetblock.buf array.

    • On other platforms: Use _mm_store_si128 SSE2 intrinsic to store the input __m128i directly into the wetblock.buf array as 128-bit integer data.

  3. Calculate the sum of the 4 floating-point values in the wetblock.buf array and divide the sum by 2^13. Store the result back in the first element of the wetblock.buf array.

  4. Return the wetblock structure containing the 4 processed floating-point values.

In summary, the create_wetblock function takes a 128-bit integer input and converts it into a WetBlock1 structure with 4 floating-point values. The function uses platform-specific SSE2 intrinsics for efficient data conversion and performs some preprocessing on the data before it is used in the oscillator processing.

7> which custom data structures do we use?

These are custom data structures used in the WindowOscillator implementation to represent specific configurations for sub-oscillator processing.

  1. WINDOW_OSCILLATOR_NUM_SUBOSCS: This constant specifies the number of sub-oscillators in a window oscillator. In this case, the value is set to 2, meaning there are two sub-oscillators for each window oscillator.

  2. SubOscProcessCfg: This structure holds the shared configuration for processing all sub-oscillators in the window oscillator. It includes the following fields:

    • table: The index of the wavetable to be used.

    • formant_mul: The formant multiplier, which affects the frequency content of the output waveform.

    • size_mask: A bitmask used to determine the size of the wavetable for the oscillator.

    • size_mask_win: A bitmask used to determine the size of the window wavetable.

    • window: The index of the window function to be used.

    • stereo: A boolean flag indicating whether the output should be in stereo or mono.

    • fm: A boolean flag indicating whether frequency modulation (FM) is enabled.

  3. SubOscProcessBlockCfg: This structure holds the specific configuration for processing a single sub-oscillator. It is used in the process_oscillator_block function and includes the following fields:

    • ratio_a: The frequency ratio of the sub-oscillator, which determines its pitch.

    • mipmap_a: The mipmap index for selecting the appropriate resolution level in the window wavetable.

    • mipmap_b: The mipmap index for selecting the appropriate resolution level in the waveform wavetable.

    • wave_mipmap_idx: The index of the mipmap level in the waveform wavetable.

    • wave_adr: A pointer to the data in the waveform wavetable at the specified mipmap level and table index.

    • win_adr: A pointer to the data in the window wavetable at the specified mipmap level and table index.

These structures help organize the processing of sub-oscillators within the WindowOscillator. They store configuration data that is either shared among all sub-oscillators or specific to each sub-oscillator, allowing for efficient and modular processing.

8> how do we process each oscillator block?

The process_oscillator_block function is a part of the WindowOscillator implementation. This function processes a single block of samples for a specific sub-oscillator. It takes the following parameters:

  • so: The index of the sub-oscillator being processed.

  • block_idx: The index of the current sample block.

  • pos: A mutable reference to the current position within the wavetable.

  • subosc_cfg: A reference to the shared SubOscProcessCfg configuration for all sub-oscillators in the window oscillator.

  • subosc_block_cfg: A reference to the specific SubOscProcessBlockCfg configuration for the current sub-oscillator.

The function first updates the current position in the wavetable based on whether frequency modulation (FM) is enabled or not. Then, it checks if the current position is out of the valid window wavetable range and, if necessary, updates the formant multiplier and table index for the current sub-oscillator and adjusts the position accordingly.

Next, the function calculates the positions within the window and waveform wavetables, taking into account the mipmap levels for better performance and memory usage.

Using the calculated positions, the function fetches the window and waveform data by calling get_wave and get_win functions, which return SIMD vector types __m128i. Then, it converts the SIMD vectors into intermediate "wet block" representations with the create_wetblock function.

Finally, depending on whether the output is stereo or mono, the function calls either block_update_stereo_out or block_update_mono_out to update the output buffer with the calculated data from the window and waveform "wet blocks".

Overall, this function is responsible for calculating the correct sample values for a sub-oscillator at a specific block index, considering the configuration data provided in subosc_cfg and subosc_block_cfg.

9> what about the other processing?

The process_block function is an implementation of the OscillatorProcess trait for the WindowOscillator struct. This function processes a single block of audio samples for the entire window oscillator, considering its configuration provided by the OscillatorProcessBlockCfg structure.

The function initializes some variables such as detune, fmstrength, and then iterates through all active sub-oscillators (self.active_sub_oscs). For each sub-oscillator, it calculates a frequency ratio based on the provided pitch, drift, and detune. If frequency modulation (FM) is enabled, the function also adjusts the frequency ratio based on the input FM signal, updating the fm_ratio for the current sub-oscillator.

After processing all sub-oscillators, the function calls process_sub_oscs to process the audio output for each sub-oscillator and sum them up into the final output.

Finally, the function scales and converts the final output from integer values to floating-point values. If stereo processing is enabled, the function scales and converts both left and right channels; otherwise, it only scales and converts the left channel.

In summary, the process_block function is responsible for processing a block of audio samples for the entire window oscillator, taking into account the provided configuration (pitch, drift, stereo, and frequency modulation) and updating the oscillator's internal state.

10> which parameters do we use?

The WindowOscillatorParam is an enumerated type representing the parameters of the WindowOscillator. The enum contains the following parameters:

  1. Morph: Controls the morphing between wavetables.

  2. Formant: Controls the formant shifting of the oscillator.

  3. Window: Determines the windowing function applied to the wavetable.

  4. UniSpread: Controls the spread (detuning) of the unison voices.

  5. UniCount: Determines the number of unison voices.

The WindowOscillatorParam enum implements the Param trait, which provides methods to configure the parameters. These methods include:

  • control_group: Returns the control group to which the parameter belongs.

  • snap: Indicates whether the parameter value should be snapped to the nearest integer.

  • control_type: Returns the control type associated with the parameter.

  • default_value: Returns the default value of the parameter.

  • min_value: Returns the minimum allowed value for the parameter.

  • max_value: Returns the maximum allowed value for the parameter.

  • value_type: Returns the value type of the parameter (integer or float).

  • moverate: Returns the rate at which the parameter value changes when moved.

  • modulateable: Indicates whether the parameter can be modulated or not.

Additionally, the WindowOscillatorParam enum provides a new_runtime method to create a runtime instance of the parameters, which is an array of WindowOscillatorParamRT elements. This method initializes a new runtime array with the respective runtime parameter objects.

11> how do we initialize?

The WindowOscillator struct implements the Init trait, which provides an init method. This method initializes the WindowOscillator with the necessary default values and settings:

  1. Clears the oscillator state by calling the clear method.

  2. Sets the active_sub_oscs value based on the UniCount parameter, limited to the range between 1 and `WINDOW_OSCILLATOR_NUM_SUBOSCS

    • 1`.
  3. Calculates the output attenuation based on the square root of the number of active sub oscillators.

  4. Sets the detune_bias, detune_offset, and initial position and gain for each sub oscillator, depending on the number of active sub oscillators.

    • If there's only one active sub oscillator, it sets the gain to unity and the position to the start of the window wavetable.

    • If there are multiple active sub oscillators, it calculates the detune bias and offset, and then initializes the gain and position for each sub oscillator based on their index and a few other factors like panning and retriggering.

The initialization process takes into account various settings and configurations of the oscillator parameters to ensure that the oscillator is set up correctly and ready for use.

12> what about formant processing?

The WindowOscillator struct has a method get_formant_mul which takes a formant parameter of type f64 as input and returns an i32 value. The purpose of this method is to calculate the formant multiplier based on the input formant value and the characteristics of/ the window and wave wavetables. Here's how it works:

  1. Calculate the formant_mul value by converting the input formant value using the n2p_tuningctr method of the tuner object. Then, multiply the result by 65536 and cast it to an i32.

  2. Calculate the difference in the number of samples per table between the window wavetable and the wave wavetable as a power of 2. This is done by subtracting the num_samples_per_table_po2() values of the two wavetables and storing the result as an i32.

  3. Depending on whether the window_vs_wave_po2 value is negative or not, the method shifts the formant_mul value either to the left (if negative) or to the right (if positive) by the absolute value of window_vs_wave_po2. The maxi function is used to ensure that the resulting value is at least 1.

The get_formant_mul method calculates a formant multiplier value based on the input formant and the characteristics of the window and wave wavetables. This value is used in the oscillator's processing to adjust the formant frequencies.

13> how do we reset this module?

The clear method in the WindowOscillator struct is used to reset its internal state. It initializes all the arrays to their default values, effectively clearing any previous data. Here's what happens in the method:

  1. The pos array is filled with zeros.
  2. The sub_pos array is filled with zeros.
  3. The ratio array is filled with zeros.
  4. The table array is filled with zeros.
  5. The formant_mul array is filled with zeros.
  6. The dispatch_delay array is filled with zeros.
  7. The gain array is filled with zeros.
  8. The drift_lfo array is filled with 0.0.
  9. The fm_ratio array is filled with zeros.

The clear method is generally called during the initialization process or when resetting the oscillator's state is necessary.

14> how do we update each block?

The block_update_mono_out and block_update_stereo_out methods in the WindowOscillator struct are used to update the oscillator's output in mono and stereo configurations respectively, for a given block index. They perform calculations based on the input WetBlock1 structs i_win and i_wave. Here's a brief explanation of what each method does:

  1. block_update_mono_out:

    • Takes block_idx, i_win, and i_wave as input/ parameters.

    • Calculates the product of the first buffer elements of i_win and i_wave.

    • Shifts the resulting product right by 6 bits and casts it to f32.

    • Adds the result to the left output channel of the oscillator at the given block_idx.

  2. block_update_stereo_out:

    • Takes so, block_idx, i_win, and i_wave as input parameters.

    • Calculates the product of the first buffer elements of i_win and i_wave.

    • Shifts the resulting product right by 7 bits and casts it to i32.

    • For both left and right output channels:

      • Multiplies the result by the corresponding gain value.

      • Shifts the product right by 6 bits and casts it to f32.

      • Adds the result to the output channel of the oscillator at the given block_idx.

These methods are used to update the oscillator's output buffer based on the input waveforms and their corresponding gains. They are typically called during the processing loop when generating audio output.

15> how do we access the various fields?

The WindowOscillator implementation provided here includes several methods to access various components and perform specific tasks related to the oscillator. Here's a brief explanation of what each method does:

  1. get_table:

    • Takes morph as an input parameter.

    • Calculates the table index based on the number of tables and the morph value.

    • Returns the limited table index within the valid range.

  2. get_size_mask:

    • Returns the size mask for the wave wavetable based on the number of samples per table.
  3. get_size_mask_win:

    • Returns the size mask for the window wavetable based on the number of samples per table.
  4. get_window:

    • Gets the value of the WindowOscillatorParam::Window parameter and limits it to the range [0, 8].

    • Returns the limited value as an i8.

  5. get_wave:

    • Takes ms_pos, m_pos, and wave_adr as input parameters.

    • Loads the appropriate values from memory using SIMD intrinsics (__m128i).

    • Performs a multiply and add operation on the loaded values and returns the result as __m128i.

  6. get_win:

    • Takes win_s_pos, win_pos, and win_adr as input parameters.

    • Loads the appropriate values from memory using SIMD intrinsics (__m128i).

    • Performs a multiply and add operation on the loaded values and returns the result as __m128i.

These methods are used for accessing and processing various components of the oscillator, such as wavetables, windows, and other parameters. They help in the overall functioning of the oscillator and are used in the synthesis process.

16> thanks -- what else can you teach me about this system? could you summarize the most important functionality in mathematical depth? how can it be improved?

This system appears to be a part of an audio synthesis system, specifically implementing an oscillator called WindowOscillator. It seems to be using wavetables for synthesis, which allows efficient, high-quality sound generation. The primary components of this oscillator are the wave_wavetable and window_wavetable. Here's a summary of the main functionality in mathematical depth:

  1. Initialization (init): Initializes the oscillator's parameters, including the number of active sub-oscillators, output attenuation, gain, and positions for each sub-oscillator. Detune bias and detune offset are also calculated during this step.

  2. Formant Multiplier (get_formant_mul): Calculates the formant multiplier based on the given formant frequency. This involves tuning and adjusting the multiplier based on the wavetable sizes.

  3. Clearing (clear): Resets the internal state of the oscillator, clearing all arrays and values associated with the current oscillator state.

  4. Block Updates: Updates the output of the oscillator either in mono (block_update_mono_out) or stereo (block_update_stereo_out) mode. This involves calculating the output value by multiplying window and wave values and then adding them to the output buffer with appropriate gain adjustments.

  5. Accessor Methods: Several accessor methods are provided for getting specific oscillator components, such as wavetables, windows, and size masks. Some of these methods also involve SIMD operations for efficient processing.

To improve this system, consider the following:

  1. Optimizations: Investigate further SIMD optimizations to make the audio processing more efficient.

  2. Parallelization: If the system runs on multicore hardware, consider parallelizing the processing of different sub-oscillators or separate audio blocks.

  3. Higher-Order Interpolation: The system appears to be using linear interpolation. Implementing higher-order interpolation techniques, such as cubic or spline interpolation, can improve audio quality.

  4. Adaptive Tuning: Implement an adaptive tuning mechanism that adjusts the oscillator's behavior based on the input signal or user interaction to enhance the audio synthesis process.

  5. Additional Oscillator Features: Explore the implementation of more oscillator features, such as frequency modulation, amplitude modulation, or other synthesis techniques, to expand the system's capabilities.

Remember that the provided code is just a part of a larger system. Identifying improvements and optimizations may require a deeper understanding of the entire audio synthesis system and its specific use cases.

17> thanks. what is the key algorithm by which this oscillator produces its output signal?

The key algorithm used by this WindowOscillator to produce its output signal is wavetable synthesis. Wavetable synthesis generates audio by using precomputed waveforms stored in tables (wavetables) and reading through them at various speeds to produce different frequencies.

In this specific implementation, there are two wavetables involved: the wave_wavetable and the window_wavetable. The oscillator generates output by combining the values from these two wavetables. Here's a step-by-step description of the algorithm:

  1. Initialize the oscillator and set up its parameters like the number of active sub-oscillators, detune values, and gain values.

  2. For each sub-oscillator, read values from the wave_wavetable and window_wavetable based on the current position and the desired morph value (for wavetable selection).

  3. Multiply the retrieved values from the wave_wavetable and window_wavetable to create an intermediate result.

  4. Apply gain values specific to the sub-oscillator and channel (left or right) to the intermediate result.

  5. Add the computed value from step 4 to the corresponding output buffer (left or right channel).

  6. Update the position within the wavetables based on the oscillator's frequency and other factors like formant and pitch.

The process repeats for each sub-oscillator and each audio sample in the output buffer. By combining multiple sub-oscillators with different gains, positions, and possibly detuning, the WindowOscillator can produce a rich and complex output signal.

Commit count: 135

cargo fmt