| name | blibliki-engine |
| description | 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 |
Blibliki Engine
Overview
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.
Monorepo Build Workflow
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:
- Apps import from
packages/engine/dist/ (built artifacts), not src/
pnpm test validates source but doesn't build
- Without rebuild, apps use stale code
Commands:
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 checking
When to rebuild:
- Using
pnpm dev: No manual rebuild needed - changes auto-rebuild on save. Check terminal for build errors.
- Not using
pnpm dev: Must run pnpm build:packages manually after engine changes before testing in apps.
Module Implementation Patterns
Constructor Signature
CRITICAL: Module constructor signature is fixed.
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();
}
}
constructor(engine: Engine, id: string, props?: any)
Registering IOs
Use register* helper methods, NOT direct AudioIO construction.
this.registerAudioInput({
name: "gain",
getAudioNode: () => this.audioNode.gain,
});
this.registerMidiInput({
name: "midi in",
onMidiEvent: this.onMidiEvent,
});
this.inputs = {
gain: new AudioIO(this, "gain", "input", this.audioNode.gain),
};
Quick reference:
registerAudioInput(props) - Audio/AudioParam inputs
registerAudioOutput(props) - Audio outputs
registerMidiInput(props) - MIDI inputs with event handler
registerMidiOutput(props) - MIDI outputs
registerDefaultIOs() - Auto-registers standard "in"/"out" + "midi in"
Module Props Schema
CRITICAL: Schema is a TypeScript type, not a function. Use kind not type.
export type IGainProps = { gain: number };
export const gainPropSchema: ModulePropSchema<IGainProps> = {
gain: {
kind: "number",
min: 0,
max: 2,
step: 0.01,
label: "Gain",
},
};
modulePropSchema({ gain: { type: 'number', ... }})
Applying Props with Setter Hooks
Props don't automatically update Web Audio nodes. Use setter hooks.
class MonoGain
extends Module<ModuleType.Gain>
implements Pick<SetterHooks<IGainProps>, "onAfterSetGain">
{
onAfterSetGain: SetterHooks<IGainProps>["onAfterSetGain"] = (value) => {
this.audioNode.gain.value = value;
};
}
PolyModule Pattern
CRITICAL: PolyModule takes monoModuleConstructor, not pre-allocated voices.
export default class Gain extends PolyModule<ModuleType.Gain> {
constructor(
engineId: string,
params: IPolyModuleConstructor<ModuleType.Gain>,
) {
const props = { ...DEFAULT_PROPS, ...params.props };
const monoModuleConstructor = (
engineId: string,
params: IModuleConstructor<ModuleType.Gain>,
) => Module.create(MonoGain, engineId, params);
super(engineId, { ...params, props, monoModuleConstructor });
this.registerDefaultIOs();
}
}
this.voices = Array.from({ length: 8 }, () => new Voice());
Module Registration Checklist
When creating a new module, update FIVE locations:
- Create module class:
packages/engine/src/modules/YourModule.ts
- Export schema:
packages/engine/src/modules/index.ts
- Add to ModuleType enum:
packages/engine/src/types.ts
- Create UI component:
apps/grid/src/components/AudioModule/YourModule.tsx
- Register in UI mapping:
apps/grid/src/components/AudioModule/index.tsx
After registration: Run pnpm build:packages before testing in grid app.
Common Workflows
Debugging "Changes Don't Appear"
If engine changes don't appear in grid app:
- Did you run
pnpm build:packages? ← Most common cause
- Check
packages/engine/dist/ has updated files
- Check browser console for import errors
- Restart dev server if needed
- If using
pnpm dev, it auto-rebuilds (check for build errors in terminal)
Debugging MIDI Routing
MIDI flow: MidiDevice → MidiSelector → VirtualMidi → Module.onMidiEvent
Files to check:
packages/engine/src/modules/MidiSelector.ts - Routes MIDI from devices
packages/engine/src/modules/VirtualMidi.ts - Programmatic MIDI generation
apps/grid/src/lib/MidiDevice*.ts - Device management
- Your module's
onMidiEvent handler and registered MidiIO inputs
Common issues:
- MidiSelector not routing to correct module
- PolyModule voices not created before IOs registered (constructor timing)
- MIDI input not registered via
registerMidiInput()
Converting to Polyphonic
- Read
packages/engine/src/core/module/PolyModule.ts first
- Create mono version of module (handles one voice)
- Change base class from
Module<T> to PolyModule<T>
- Pass
monoModuleConstructor: () => new MonoYourModule(...) to super
- Voices created automatically by PolyModule base class
- Build and test with multiple simultaneous notes
Before Finishing
When completing engine work, verify ALL of these:
Most commonly missed: Verifying packages are rebuilt before testing in app.
Common Mistakes
| 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) |
File Locations
When implementing features, reference these files:
- Example modules:
packages/engine/src/modules/Gain.ts, Oscillator.ts
- Module base:
packages/engine/src/core/module/Module.ts
- PolyModule base:
packages/engine/src/core/module/PolyModule.ts
- IO system:
packages/engine/src/core/IO/AudioIO.ts, MidiIO.ts
- Schema types:
packages/engine/src/core/schema.ts