Use when working with Blibliki's packages/engine - creating modules, modifying engine code, debugging MIDI/audio issues, or when changes in engine package don't appear in grid app
The Blibliki engine (packages/engine) is a TypeScript audio synthesis framework built on Web Audio API. Critical: Apps consume BUILT packages from dist/, not source files. Changes require rebuilding before they appear in apps.
The #1 mistake: Modifying engine source and expecting apps to pick it up without rebuilding.
digraph build_workflow {
"Modifying engine code?" [shape=diamond];
"Edit packages/engine/src/" [shape=box];
"Run: pnpm build:packages" [shape=box, style=filled, fillcolor=yellow];
"Edit apps/grid/src/" [shape=box];
"Run: pnpm dev to test" [shape=box];
"Just reading/understanding?" [shape=diamond];
"Read code directly" [shape=box];
"Modifying engine code?" -> "Edit packages/engine/src/" [label="yes"];
"Edit packages/engine/src/" -> "Run: pnpm build:packages";
"Run: pnpm build:packages" -> "Edit apps/grid/src/" [label="if updating UI"];
"Edit apps/grid/src/" -> "Run: pnpm dev to test";
"Modifying engine code?" -> "Just reading/understanding?" [label="no"];
"Just reading/understanding?" -> "Read code directly" [label="yes"];
}
Why this matters:
packages/engine/dist/ (built artifacts), not src/pnpm test validates source but doesn't buildCommands:
pnpm dev - Start dev servers with watch mode (RECOMMENDED - auto-rebuilds on save)pnpm build:packages - Rebuild all packages (use when working without pnpm dev)pnpm test - Run tests (validates source, doesn't build)pnpm tsc - Type checkingWhen to rebuild:
pnpm dev: No manual rebuild needed - changes auto-rebuild on save. Check terminal for build errors.pnpm dev: Must run pnpm build:packages manually after engine changes before testing in apps.CRITICAL: Module constructor signature is fixed.
// ✅ CORRECT - Mono module
class MonoGain extends Module<ModuleType.Gain> {
constructor(engineId: string, params: ICreateModule<ModuleType.Gain>) {
const props = { ...DEFAULT_PROPS, ...params.props };
const audioNodeConstructor = (context: Context) =>
new GainNode(context.audioContext);
super(engineId, { ...params, audioNodeConstructor, props });
this.registerDefaultIOs();
}
}
// ❌ WRONG - Different signature
constructor(engine: Engine, id: string, props?: any)
Use register* helper methods, NOT direct AudioIO construction.
// ✅ CORRECT - AudioParam modulation input
this.registerAudioInput({
name: "gain",
getAudioNode: () => this.audioNode.gain, // Returns AudioParam
});
// ✅ CORRECT - MIDI input
this.registerMidiInput({
name: "midi in",
onMidiEvent: this.onMidiEvent,
});
// ❌ WRONG - Don't construct AudioIO directly
this.inputs = {
gain: new AudioIO(this, "gain", "input", this.audioNode.gain),
};
Quick reference:
registerAudioInput(props) - Audio/AudioParam inputsregisterAudioOutput(props) - Audio outputsregisterMidiInput(props) - MIDI inputs with event handlerregisterMidiOutput(props) - MIDI outputsregisterDefaultIOs() - Auto-registers standard "in"/"out" + "midi in"CRITICAL: Schema is a TypeScript type, not a function. Use kind not type.
// ✅ CORRECT
export type IGainProps = { gain: number };
export const gainPropSchema: ModulePropSchema<IGainProps> = {
gain: {
kind: "number", // kind, not type
min: 0,
max: 2,
step: 0.01,
label: "Gain",
},
};
// ❌ WRONG
modulePropSchema({ gain: { type: 'number', ... }})
Props don't automatically update Web Audio nodes. Use setter hooks.
class MonoGain
extends Module<ModuleType.Gain>
implements Pick<SetterHooks<IGainProps>, "onAfterSetGain">
{
// ✅ CORRECT - Hook called when props.gain changes
onAfterSetGain: SetterHooks<IGainProps>["onAfterSetGain"] = (value) => {
this.audioNode.gain.value = value;
};
}
// Available hooks per prop:
// - onSetPropName(value) - Transform before setting (return new value)
// - onAfterSetPropName(value) - Side effect after setting (void)
CRITICAL: PolyModule takes monoModuleConstructor, not pre-allocated voices.
// ✅ CORRECT - Poly module
export default class Gain extends PolyModule<ModuleType.Gain> {
constructor(
engineId: string,
params: IPolyModuleConstructor<ModuleType.Gain>,
) {
const props = { ...DEFAULT_PROPS, ...params.props };
// Pass constructor function for mono voices
const monoModuleConstructor = (
engineId: string,
params: IModuleConstructor<ModuleType.Gain>,
) => Module.create(MonoGain, engineId, params);
super(engineId, { ...params, props, monoModuleConstructor });
this.registerDefaultIOs();
}
}
// ❌ WRONG - Don't pre-allocate voice instances
this.voices = Array.from({ length: 8 }, () => new Voice());
When creating a new module, update FIVE locations:
packages/engine/src/modules/YourModule.tspackages/engine/src/modules/index.tspackages/engine/src/types.tsapps/grid/src/components/AudioModule/YourModule.tsxapps/grid/src/components/AudioModule/index.tsxAfter registration: Run pnpm build:packages before testing in grid app.
If engine changes don't appear in grid app:
pnpm build:packages? ← Most common causepackages/engine/dist/ has updated filespnpm dev, it auto-rebuilds (check for build errors in terminal)MIDI flow: MidiDevice → MidiSelector → VirtualMidi → Module.onMidiEvent
Files to check:
packages/engine/src/modules/MidiSelector.ts - Routes MIDI from devicespackages/engine/src/modules/VirtualMidi.ts - Programmatic MIDI generationapps/grid/src/lib/MidiDevice*.ts - Device managementonMidiEvent handler and registered MidiIO inputsCommon issues:
registerMidiInput()packages/engine/src/core/module/PolyModule.ts firstModule<T> to PolyModule<T>monoModuleConstructor: () => new MonoYourModule(...) to superWhen completing engine work, verify ALL of these:
pnpm dev, or run pnpm build:packages)pnpm test (all tests pass)pnpm tsc (type checking passes)pnpm lint (no linting errors)pnpm dev running)Most commonly missed: Verifying packages are rebuilt before testing in app.
| Mistake | Fix |
|---|---|
| "Changes don't appear in app" | Run pnpm build:packages to rebuild engine |
| "Tests pass but app breaks" | Tests validate source, apps use dist/. Rebuild packages. |
| Wrong constructor signature | Use (engineId: string, params: ICreateModule<T>) |
new AudioIO() directly |
Use registerAudioInput/Output() helpers |
Schema uses type property |
Use kind property instead |
modulePropSchema() function call |
Plain object with type ModulePropSchema<T> |
| Props not updating Web Audio | Implement setter hooks: onAfterSetPropName |
| Pre-allocating voices in PolyModule | Pass monoModuleConstructor function |
| "Import errors in app" | Check module exported in index.ts and schema registered |
| "PolyModule voices not created" | Ensure voices created before IOs registered (constructor timing) |
When implementing features, reference these files:
packages/engine/src/modules/Gain.ts, Oscillator.tspackages/engine/src/core/module/Module.tspackages/engine/src/core/module/PolyModule.tspackages/engine/src/core/IO/AudioIO.ts, MidiIO.tspackages/engine/src/core/schema.ts