Skip to content

Commit

Permalink
Foundation + initial plumbing for sampler
Browse files Browse the repository at this point in the history
 * Add VC, AWP, wasm with build scripting, and framework of the UI
  • Loading branch information
Ameobea committed Dec 30, 2023
1 parent e73497c commit 10497c8
Show file tree
Hide file tree
Showing 32 changed files with 514 additions and 28 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/explicit-module-boundary-types": 0,
"react/jsx-no-target-blank": 0,
"@typescript-eslint/no-empty-interface": "warn"
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/consistent-type-imports": ["warn", { "disallowTypeAnnotations": false }]
},
"env": {
"es6": true,
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ public/multiband_diode_ladder_distortion.wasm
public/midi_renderer.wasm
public/oscilloscope.wasm
public/spectrum_viz_full.wasm
public/sampler.wasm
10 changes: 10 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ build-all:
cp ./engine/target/wasm32-unknown-unknown/release/midi_renderer.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/oscilloscope.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/spectrum_viz_full.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/sampler.wasm ./public
cp ./engine/build/* ./src

just build-sinsy
Expand Down Expand Up @@ -148,6 +149,7 @@ run:
cp ./engine/target/wasm32-unknown-unknown/release/midi_renderer.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/oscilloscope.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/spectrum_viz_full.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/sampler.wasm ./public

just debug-sinsy

Expand Down Expand Up @@ -328,3 +330,11 @@ build-waveform-renderer:
cd ./engine/waveform_renderer && cargo build --release --target wasm32-unknown-unknown && \
cd - && wasm-bindgen ./engine/target/wasm32-unknown-unknown/release/waveform_renderer.wasm --browser --remove-producers-section --out-dir ./engine/build
cp ./engine/build/waveform_renderer* ./src/

build-sampler:
cd ./engine/sampler && cargo build --release --target wasm32-unknown-unknown && \
cp ../target/wasm32-unknown-unknown/release/sampler.wasm ../../public

debug-sampler:
cd ./engine/sampler && cargo build --target wasm32-unknown-unknown && \
cp ../target/wasm32-unknown-unknown/debug/sampler.wasm ../../public
7 changes: 7 additions & 0 deletions engine/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ members = [
"midi_renderer",
"oscilloscope",
"canvas_utils",
"sampler"
]

[profile.release]
Expand Down
9 changes: 9 additions & 0 deletions engine/engine/src/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,12 @@ extern "C" {
pub fn cleanup_signal_analyzer(state_key: &str);
pub fn get_signal_analyzer_audio_connectables(state_key: &str) -> JsValue;
}

#[wasm_bindgen(raw_module = "./sampler/sampler")]
extern "C" {
pub fn init_sampler(state_key: &str);
pub fn hide_sampler(state_key: &str);
pub fn unhide_sampler(state_key: &str);
pub fn cleanup_sampler(state_key: &str);
pub fn get_sampler_audio_connectables(state_key: &str) -> JsValue;
}
3 changes: 2 additions & 1 deletion engine/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@ pub fn init() {

/// Creates a new view context from the provided name and sets it as the main view context.
#[wasm_bindgen]
pub fn create_view_context(vc_name: String) {
pub fn create_view_context(vc_name: String, display_name: String) {
let uuid = uuid_v4();
debug!("Creating VC with name {} with vcId {}", vc_name, uuid);
let mut view_context = build_view(&vc_name, uuid);
view_context.init();
view_context.hide();
let vcm = get_vcm();
vcm.add_view_context(uuid, vc_name, view_context);
set_vc_title(uuid.to_string(), display_name)
}

#[wasm_bindgen]
Expand Down
7 changes: 4 additions & 3 deletions engine/engine/src/view_context/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use crate::{
composition_sharing::mk_composition_sharing, control_panel::mk_control_panel,
faust_editor::mk_faust_editor, filter_designer::mk_filter_designer, granulator::mk_granulator,
graph_editor::mk_graph_editor, looper::mk_looper, midi_editor::mk_midi_editor,
midi_keyboard::mk_midi_keyboard, sample_library::mk_sample_library, sequencer::mk_sequencer,
signal_analyzer::mk_signal_analyzer, sinsy::mk_sinsy, synth_designer::mk_synth_designer,
welcome_page::mk_welcome_page,
midi_keyboard::mk_midi_keyboard, sample_library::mk_sample_library, sampler::mk_sampler,
sequencer::mk_sequencer, signal_analyzer::mk_signal_analyzer, sinsy::mk_sinsy,
synth_designer::mk_synth_designer, welcome_page::mk_welcome_page,
},
ViewContext,
};
Expand Down Expand Up @@ -425,6 +425,7 @@ pub fn build_view(name: &str, uuid: Uuid) -> Box<dyn ViewContext> {
"looper" => mk_looper(uuid),
"welcome_page" => mk_welcome_page(uuid),
"signal_analyzer" => mk_signal_analyzer(uuid),
"sampler" => mk_sampler(uuid),
_ => panic!("No handler for view context with name {}", name),
}
}
1 change: 1 addition & 0 deletions engine/engine/src/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod looper;
pub mod midi_editor;
pub mod midi_keyboard;
pub mod sample_library;
pub mod sampler;
pub mod sequencer;
pub mod signal_analyzer;
pub mod sinsy;
Expand Down
35 changes: 35 additions & 0 deletions engine/engine/src/views/sampler/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use uuid::Uuid;
use wasm_bindgen::JsValue;

use crate::{js, view_context::ViewContext};

/// This is just a shim to the JS-based sampler component.
pub struct Sampler {
pub uuid: Uuid,
}

impl Sampler {
pub fn new(uuid: Uuid) -> Self { Sampler { uuid } }

pub fn get_state_key(&self) -> String { format!("sampler_{}", self.uuid) }
}

impl ViewContext for Sampler {
fn init(&mut self) { js::init_sampler(&self.get_state_key()); }

fn cleanup(&mut self) { js::cleanup_sampler(&self.get_state_key()) }

fn get_id(&self) -> String { self.uuid.to_string() }

fn hide(&mut self) { js::hide_sampler(&self.get_state_key()); }

fn unhide(&mut self) { js::unhide_sampler(&self.get_state_key()); }

fn dispose(&mut self) { js::delete_localstorage_key(&self.get_state_key()); }

fn get_audio_connectables(&self) -> JsValue {
js::get_sampler_audio_connectables(&self.get_state_key())
}
}

pub fn mk_sampler(uuid: Uuid) -> Box<dyn ViewContext> { Box::new(Sampler { uuid }) }
11 changes: 11 additions & 0 deletions engine/sampler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "sampler"
version = "0.1.0"
authors = ["Casey Primozic <[email protected]>"]
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
dsp = { path = "../dsp" }
7 changes: 7 additions & 0 deletions engine/sampler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub struct SamplerCtx {}

#[no_mangle]
pub extern "C" fn init_sampler_ctx() -> *mut SamplerCtx {
let ctx = SamplerCtx {};
Box::into_raw(Box::new(ctx))
}
68 changes: 68 additions & 0 deletions public/SamplerAWP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const PARAM_DESCRIPTORS = [];

class SamplerAWP extends AudioWorkletProcessor {
static get parameterDescriptors() {
return PARAM_DESCRIPTORS;
}

constructor() {
super();

this.isShutdown = false;
this.ctxPtr = 0;
this.wasmInstance = null;
this.wasmMemoryBuffer = null;
this.pendingMessages = [];

this.port.onmessage = evt => this.handleMessage(evt.data);
}

async initWasmInstance(wasmBytes) {
const compiledModule = await WebAssembly.compile(wasmBytes);
this.wasmInstance = await WebAssembly.instantiate(compiledModule, { env: {} });

this.ctxPtr = this.wasmInstance.exports.init_sampler_ctx();
this.wasmMemoryBuffer = new Float32Array(this.wasmInstance.exports.memory.buffer);

this.pendingMessages.forEach(data => this.handleMessage(data));
this.pendingMessages = [];
}

handleMessage(data) {
// Store all events other than the initialization event until after Wasm is loaded and they can be handled.
//
// Pending events will be processed once that initialization is finished.
if (!this.ctxPtr && data.type !== 'setWasmBytes') {
this.pendingMessages.push(data);
return;
}

switch (data.type) {
case 'setWasmBytes': {
this.initWasmInstance(data.wasmBytes);
break;
}
case 'shutdown': {
this.isShutdown = true;
break;
}
default: {
console.error('Unhandled message type in sampler player AWP: ', data.type);
}
}
}

process(_inputs, _outputs, _params) {
if (this.isShutdown) {
return false;
} else if (!this.ctxPtr) {
return true;
}

// TODO

return true;
}
}

registerProcessor('sampler-awp', SamplerAWP);
3 changes: 2 additions & 1 deletion src/ViewContextManager/AddModulePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const ViewContextDescriptors: ViewContextDescriptor[] = [
description:
'Contains visualizations for analyzing sound including an oscilloscope and spectrogram',
},
{ name: 'sampler', displayName: 'Sampler' },
];

const AddModulePicker: React.FC<{ onClose: () => void }> = ({ onClose }) => {
Expand Down Expand Up @@ -78,7 +79,7 @@ const AddModulePicker: React.FC<{ onClose: () => void }> = ({ onClose }) => {
return;
}

engine.create_view_context(vc.name);
engine.create_view_context(vc.name, selectedModule);
onClose();
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/controls/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { createRoot } from 'react-dom/client';

import './Modal.scss';
import { SvelteComponent } from 'svelte';
import type { SvelteComponent } from 'svelte';

export interface ModalCompProps<T> {
onSubmit: (val: T) => void;
Expand Down
2 changes: 1 addition & 1 deletion src/fmSynth/ConfigureOperator.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UnreachableException } from 'ameo-utils';
import React, { Suspense, useCallback, useMemo } from 'react';
import ControlPanel from 'react-control-panel';
import { SvelteComponent } from 'svelte';
import type { SvelteComponent } from 'svelte';
import type { Writable } from 'svelte/store';

import {
Expand Down
16 changes: 8 additions & 8 deletions src/granulator/GranulatorUI/WaveformRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ export class WaveformRenderer {
private worker: Comlink.Remote<WaveformRendererWorker>;
private sampleCount = 0;
private bounds: WaveformBounds = { startMs: 0, endMs: 0 };
private selection: WaveformSelection = {
startMarkPosMs: null,
endMarkPosMs: null,
};
private widthPx = 1400;
private heightPx = 240;
private isRendering = false;
private needsRender = false;
private sampleRate = 44100;
private canvasCtx: CanvasRenderingContext2D | null = null;
private selection: WaveformSelection = {
startMarkPosMs: null,
endMarkPosMs: null,
};

public getWidthPx() {
return this.widthPx;
Expand Down Expand Up @@ -63,6 +63,9 @@ export class WaveformRenderer {
}

public reinitializeCtx(sample?: AudioBuffer | null) {
this.bounds = { startMs: 0, endMs: this.getSampleLengthMs() };
this.selection = { startMarkPosMs: null, endMarkPosMs: null };

this.worker.reinitializeCtx(
this.widthPx,
this.heightPx,
Expand Down Expand Up @@ -138,13 +141,10 @@ export class WaveformRenderer {
this.selectionChangedCbs.forEach(cb => cb({ ...this.selection }));
}

public async setSample(sample: AudioBuffer) {
public setSample(sample: AudioBuffer) {
this.sampleCount = sample.length;
this.sampleRate = sample?.sampleRate ?? 44100;
this.reinitializeCtx(sample);
if (this.bounds.endMs === 0) {
this.setBounds(0, await this.getSampleLengthMs());
}
this.updateBoundsCbs();
this.render();
}
Expand Down
4 changes: 2 additions & 2 deletions src/granulator/GranulatorUI/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import './Granulator.scss';
import { type GranulatorInstance, GranulatorInstancesById } from 'src/granulator/granulator';
import SampleEditor from 'src/granulator/GranulatorUI/SampleEditor';
import SampleRecorder from 'src/granulator/GranulatorUI/SampleRecorder';
import { WaveformRenderer } from 'src/granulator/GranulatorUI/WaveformRenderer';
import { getSample, SampleDescriptor } from 'src/sampleLibrary';
import type { WaveformRenderer } from 'src/granulator/GranulatorUI/WaveformRenderer';
import { getSample, type SampleDescriptor } from 'src/sampleLibrary';
import { selectSample } from 'src/sampleLibrary/SampleLibraryUI/SelectSample';
import { useMappedWritableValue } from 'src/reactUtils';

Expand Down
4 changes: 2 additions & 2 deletions src/granulator/granulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { get, writable } from 'svelte/store';

const ctx = new AudioContext();

const GranulatorRegistered = new AsyncOnce(
const GranulatorAWPRegistered = new AsyncOnce(
() =>
ctx.audioWorklet.addModule(
process.env.ASSET_PATH +
Expand Down Expand Up @@ -199,7 +199,7 @@ export const init_granulator = async (stateKey: string) => {

const granularWasmPromise = GranularWasm.get();
const waveformRenderer = new WaveformRenderer();
GranulatorRegistered.get().then(async () => {
GranulatorAWPRegistered.get().then(async () => {
const node = new AudioWorkletNode(ctx, 'granulator-audio-worklet-processor', {
channelCount: 1,
numberOfInputs: 1,
Expand Down
5 changes: 4 additions & 1 deletion src/graphEditor/GraphEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,10 @@ const GraphControls: React.FC<GraphControlsProps> = ({ lGraphInstance }) => {
return;
}

engine.create_view_context(selectedNodeType.current);
const displayName = ViewContextDescriptors.find(
d => d.name === selectedNodeType.current
)!.displayName;
engine.create_view_context(selectedNodeType.current, displayName);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/sampleLibrary/SampleLibraryUI/SampleLibraryUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export const SampleListing: React.FC<SampleListingProps> = ({
}

return (
<div style={{ width: 500 }}>
<div>
<SampleSearch value={sampleSearch} onChange={setSampleSearch} />
<div className='sample-row' style={{ width: '100%', borderBottom: '1px solid #888' }}>
<div />
Expand Down
Loading

0 comments on commit 10497c8

Please sign in to comment.