Skip to content

Commit

Permalink
Optimize wavetable sampling
Browse files Browse the repository at this point in the history
 * Created dedicated oversampled versions of dimension + waveform sampling functions to help avoid overhead of calling them over in a loop
 * Update mixer levels viz UI after adding/removing inputs
  • Loading branch information
Ameobea committed Feb 3, 2025
1 parent 87efa95 commit 575b886
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 11 deletions.
18 changes: 8 additions & 10 deletions engine/wavetable/src/fm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,19 +498,17 @@ impl Oscillator for WaveTableHandle {
];

// 4x oversampling to avoid aliasing
let oversample_factor = 4usize;
let mut sample = 0.;
const OVERSAMPLE_FACTOR: usize = 4usize;
let mut phase = self.phase;
for _ in 0..oversample_factor {
phase = Self::compute_new_phase_oversampled(phase, oversample_factor as f32, frequency);
sample += wavetable.get_sample(
phase * (wavetable.settings.waveform_length - 1) as f32,
&mixes,
);
}
let sample_indices: [f32; OVERSAMPLE_FACTOR] = std::array::from_fn(|_| {
phase = Self::compute_new_phase_oversampled(phase, OVERSAMPLE_FACTOR as f32, frequency);
phase * (wavetable.settings.waveform_length - 1) as f32
});

let sample = wavetable.get_sample_oversampled::<OVERSAMPLE_FACTOR>(sample_indices, &mixes);

self.phase = phase;
sample / oversample_factor as f32
sample
}
}

Expand Down
91 changes: 91 additions & 0 deletions engine/wavetable/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,42 @@ impl WaveTable {
// )
}

fn sample_waveform_oversampled<const OVERSAMPLE_FACTOR: usize>(
&self,
dimension_ix: usize,
waveform_ix: usize,
sample_indices: [f32; OVERSAMPLE_FACTOR],
) -> f32 {
let waveform_offset_samples = (dimension_ix * self.settings.get_samples_per_dimension())
+ (waveform_ix * self.settings.waveform_length);

let mut sample_acc = 0.;
for sample_ix in sample_indices {
let sample_mix = sample_ix.fract();
let (sample_low_ix, sample_hi_ix) = (
sample_ix.floor() as usize,
(sample_ix.ceil() as usize).min(self.samples.len() - 1),
);

let (low_sample, high_sample) = (
unsafe {
*self
.samples
.get_unchecked(waveform_offset_samples + sample_low_ix)
},
unsafe {
*self
.samples
.get_unchecked(waveform_offset_samples + sample_hi_ix)
},
);

sample_acc += mix(sample_mix, low_sample, high_sample);
}

sample_acc / OVERSAMPLE_FACTOR as f32
}

fn sample_dimension(&self, dimension_ix: usize, waveform_ix: f32, sample_ix: f32) -> f32 {
let waveform_mix = waveform_ix.fract();
if waveform_mix == 0. {
Expand All @@ -124,6 +160,28 @@ impl WaveTable {
mix(waveform_mix, low_sample, high_sample)
}

fn sample_dimension_oversampled<const OVERSAMPLE_FACTOR: usize>(
&self,
dimension_ix: usize,
waveform_ix: f32,
sample_indices: [f32; OVERSAMPLE_FACTOR],
) -> f32 {
let waveform_mix = waveform_ix.fract();
if waveform_mix == 0. {
return self.sample_waveform_oversampled(dimension_ix, waveform_ix as usize, sample_indices);
}

let (waveform_low_ix, waveform_hi_ix) =
(waveform_ix.floor() as usize, waveform_ix.ceil() as usize);

let low_sample =
self.sample_waveform_oversampled(dimension_ix, waveform_low_ix, sample_indices);
let high_sample =
self.sample_waveform_oversampled(dimension_ix, waveform_hi_ix, sample_indices);

mix(waveform_mix, low_sample, high_sample)
}

pub fn get_sample(&self, sample_ix: f32, mixes: &[f32]) -> f32 {
if cfg!(debug_assertions) {
if sample_ix < 0.0 || sample_ix >= (self.settings.waveform_length - 1) as f32 {
Expand All @@ -135,12 +193,21 @@ impl WaveTable {
}

let base_sample = if self.settings.waveforms_per_dimension == 1 {
return self.sample_waveform(0, 0, sample_ix);
} else if self.settings.waveforms_per_dimension == 1 {
self.sample_waveform(0, 0, sample_ix)
} else {
let waveform_ix = mixes[0] * ((self.settings.waveforms_per_dimension - 1) as f32);
self.sample_dimension(0, waveform_ix, sample_ix)
};

// for legacy reasons, there are always two dimensions that have their data duplicated across
// both dims. The mix is set to 0, so there's no reason to waste compute sampling +
// interoplating.
if self.settings.dimension_count == 2 && mixes[1 * 2 + 1] == 0. {
return base_sample;
}

// For each higher dimension, mix the base sample from the lowest dimension with the output
// of the next dimension until a final sample is produced
let mut sample = base_sample;
Expand All @@ -157,6 +224,30 @@ impl WaveTable {

sample
}

pub fn get_sample_oversampled<const OVERSAMPLE_FACTOR: usize>(
&self,
sample_indices: [f32; OVERSAMPLE_FACTOR],
mixes: &[f32],
) -> f32 {
let base_sample = if self.settings.waveforms_per_dimension == 1 {
return self.sample_waveform_oversampled::<OVERSAMPLE_FACTOR>(0, 0, sample_indices);
} else if self.settings.waveforms_per_dimension == 1 {
self.sample_waveform_oversampled::<OVERSAMPLE_FACTOR>(0, 0, sample_indices)
} else {
let waveform_ix = mixes[0] * ((self.settings.waveforms_per_dimension - 1) as f32);
self.sample_dimension_oversampled::<OVERSAMPLE_FACTOR>(0, waveform_ix, sample_indices)
};

// for legacy reasons, there are always two dimensions that have their data duplicated across
// both dims. The mix is set to 0, so there's no reason to waste compute sampling +
// interoplating.
if self.settings.dimension_count == 2 && mixes[1 * 2 + 1] == 0. {
return base_sample;
}

unimplemented!()
}
}

/// Represents a single voice playing out of an attached `WaveTable`
Expand Down
36 changes: 35 additions & 1 deletion src/graphEditor/nodes/CustomAudio/mixer/MixerLevelsViz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export class MixerLevelsViz {
backgroundColor: BACKGROUND_COLOR,
});
} catch (err) {
logError('Failed to initialize PixiJS applicationl; WebGL not supported?');
logError('Failed to initialize PixiJS application; WebGL not supported?');
throw err;
}

Expand Down Expand Up @@ -213,6 +213,40 @@ export class MixerLevelsViz {
this.app.stage.addChild(this.levelMetersContainer);
};

private updateVizLayout = () => {
const inputCount = this.levelMeters.length;
const newHeight =
BASE_MARGIN_TOP_PX +
(inputCount > 2 ? MORE_THAN_TWO_INPUTS_ADDITIONAL_MARGIN_TOP_PX : 0) +
(MIXER_LEVELS_VIZ_HEIGHT_PER_INPUT_PX + LEVEL_METER_VERTICAL_SPACING_PX) * inputCount;
this.app.renderer.resize(MIXER_LEVELS_VIZ_WIDTH_PX, newHeight);

for (let i = 0; i < inputCount; i++) {
this.levelMeters[i].displayObject.y =
BASE_MARGIN_TOP_PX +
(inputCount > 2 ? MORE_THAN_TWO_INPUTS_ADDITIONAL_MARGIN_TOP_PX : 0) +
i * (LEVEL_METER_VERTICAL_SPACING_PX + MIXER_LEVELS_VIZ_HEIGHT_PER_INPUT_PX);
}
};

public addInput = () => {
const newMeter = new LevelMeter(this.app.renderer as PIXI.Renderer);
this.levelMeters.push(newMeter);
this.levelMetersContainer.addChild(newMeter.displayObject);
this.updateVizLayout();
};

public removeInput = () => {
if (this.levelMeters.length === 0) {
return;
}

const removedMeter = this.levelMeters.pop()!;
this.levelMetersContainer.removeChild(removedMeter.displayObject);
removedMeter.displayObject.destroy({ children: true });
this.updateVizLayout();
};

public setAudioThreadBuffer = (audioThreadBuffer: Float32Array) => {
this.audioThreadBuffer = audioThreadBuffer;
};
Expand Down
2 changes: 2 additions & 0 deletions src/graphEditor/nodes/CustomAudio/mixer/MixerSmallView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
if (awpHandle) {
connectTrackToAWP(awpHandle, inputCount - 1, false);
}
vizInst?.addInput();
};
const removeInput = () => {
if (awpHandle) {
Expand All @@ -168,6 +169,7 @@
mixer.removeInput();
inputCount = inputCount - 1;
vizInst?.removeInput();
};
$: settings = buildSettings(mixer, inputCount, addInput, removeInput);
Expand Down

0 comments on commit 575b886

Please sign in to comment.