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

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:

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:

  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:

  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:

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:

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:

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 `WINDOWOSCILLATORNUM_SUBOSCS

  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.

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:

  2. block_update_stereo_out:

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:

  2. get_size_mask:

  3. get_size_mask_win:

  4. get_window:

  5. get_wave:

  6. get_win:

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.