P1a: material import, sixteen faces, sample engine and quarry

This commit is contained in:
Emil
2026-11-01 00:56:51 +03:00
parent db4368efae
commit ebdeca3b92
28 changed files with 2799 additions and 114 deletions
+54
View File
@@ -1,9 +1,11 @@
import type {
Command,
CommandResult,
CoreSummary,
Event,
HelloMessage,
MeterFrame,
Seal,
ServerMessage,
State,
Status
@@ -17,6 +19,7 @@ export interface BridgeHandlers {
onMeter: (meter: MeterFrame) => void;
onEvent: (event: Event) => void;
onError: (message: string) => void;
onImported?: (core: CoreSummary) => void;
}
interface Pending {
@@ -25,11 +28,19 @@ interface Pending {
timer: number;
}
interface PendingImport {
resolve: (core: CoreSummary) => void;
reject: (error: Error) => void;
timer: number;
}
const COMMAND_TIMEOUT_MS = 5000;
const IMPORT_TIMEOUT_MS = 60_000;
export class Bridge {
private ws: WebSocket | null = null;
private pending = new Map<number, Pending>();
private imports = new Map<number, PendingImport>();
private nextId = 0;
private closed = false;
private reconnectTimer: number | null = null;
@@ -81,6 +92,7 @@ export class Bridge {
ws.onclose = () => {
if (this.ws === ws) this.ws = null;
this.rejectAll(new Error('the wire closed'));
this.rejectImports(new Error('the wire closed'));
this.setStatus('offline');
if (!this.closed) this.scheduleReconnect();
};
@@ -102,6 +114,23 @@ export class Bridge {
});
}
importCore(name: string, seal: Seal, data: ArrayBuffer): Promise<CoreSummary> {
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
reject(new Error('the engine is not on the wire'));
return;
}
const id = ++this.nextId;
const timer = window.setTimeout(() => {
this.imports.delete(id);
reject(new Error('the quarry did not answer in time'));
}, IMPORT_TIMEOUT_MS);
this.imports.set(id, { resolve, reject, timer });
this.ws.send(JSON.stringify({ type: 'import_core', id, name, seal }));
this.ws.send(data);
});
}
destroy(): void {
this.closed = true;
if (this.reconnectTimer !== null) {
@@ -111,6 +140,7 @@ export class Bridge {
this.ws?.close();
this.ws = null;
this.rejectAll(new Error('the wire was closed by the surface'));
this.rejectImports(new Error('the wire was closed by the surface'));
this.setStatus('offline');
}
@@ -160,6 +190,15 @@ export class Bridge {
case 'event':
this.handlers.onEvent(message.event);
break;
case 'imported': {
const pending = this.imports.get(message.id);
if (!pending) break;
this.imports.delete(message.id);
window.clearTimeout(pending.timer);
this.handlers.onImported?.(message.core);
pending.resolve(message.core);
break;
}
case 'error': {
if (message.id !== null) {
const pending = this.pending.get(message.id);
@@ -169,6 +208,13 @@ export class Bridge {
pending.reject(new Error(message.message));
break;
}
const importing = this.imports.get(message.id);
if (importing) {
this.imports.delete(message.id);
window.clearTimeout(importing.timer);
importing.reject(new Error(message.message));
break;
}
}
this.handlers.onError(message.message);
break;
@@ -196,6 +242,14 @@ export class Bridge {
this.pending.clear();
}
private rejectImports(error: Error): void {
for (const [, pending] of this.imports) {
window.clearTimeout(pending.timer);
pending.reject(error);
}
this.imports.clear();
}
private setStatus(status: Status): void {
if (this.status === status) return;
this.status = status;
+3
View File
@@ -30,6 +30,9 @@ export function commandLabel(command: Command): string {
case 'strike': return `strike ${command.vein} ${command.velocity.toFixed(2)}`;
case 'set_tempo': return `tempo ${fmtTempo(command.bpm)}`;
case 'set_vein_gain': return `${command.vein} gain ${command.gain.toFixed(2)}`;
case 'strike_face': return `strike ${command.core} f${command.face + 1}`;
case 'set_pattern_step': return `step ${command.row + 1}:${command.step + 1} at ${command.velocity}`;
case 'set_pattern_face': return `${command.core} f${command.face + 1} on row ${command.row + 1}`;
case 'get_snapshot': return 'snapshot';
}
}
+41 -3
View File
@@ -1,6 +1,30 @@
// Wire types for protocol v1. Kept in step with crates/zhila-protocol.
export type VeinId = 'bass' | 'crack' | 'voice' | 'noise' | 'silt';
export type Status = 'connecting' | 'live' | 'offline';
export type Seal = 'found' | 'taken' | 'gift' | 'loan' | 'unknown';
export interface FaceSummary {
index: number;
rank: number;
start_sec: number;
end_sec: number;
rms: number;
}
export interface CoreSummary {
id: string;
name: string;
seal: Seal;
sample_rate: number;
duration_sec: number;
faces: FaceSummary[];
peaks: number[];
}
export interface FaceRef {
core: string;
face: number;
}
export interface AudioInfo {
running: boolean;
@@ -25,6 +49,7 @@ export interface VeinState {
export interface PatternRow {
vein: VeinId;
face: FaceRef | null;
velocities: number[];
}
@@ -39,6 +64,7 @@ export interface State {
transport: TransportState;
veins: VeinState[];
pattern: Pattern;
cores: CoreSummary[];
audio: AudioInfo;
journal_len: number;
}
@@ -47,7 +73,10 @@ export type Event =
| { kind: 'transport'; playing: boolean }
| { kind: 'tempo'; bpm: number }
| { kind: 'vein_gain'; vein: VeinId; gain: number }
| { kind: 'struck'; vein: VeinId; velocity: number };
| { kind: 'struck'; vein: VeinId; velocity: number }
| { kind: 'pattern_step'; row: number; step: number; velocity: number }
| { kind: 'pattern_face'; row: number; core: string; face: number }
| { kind: 'face_struck'; core: string; face: number; velocity: number };
export type Command =
| { type: 'get_snapshot' }
@@ -55,7 +84,10 @@ export type Command =
| { type: 'stop' }
| { type: 'strike'; vein: VeinId; velocity: number }
| { type: 'set_tempo'; bpm: number }
| { type: 'set_vein_gain'; vein: VeinId; gain: number };
| { type: 'set_vein_gain'; vein: VeinId; gain: number }
| { type: 'strike_face'; core: string; face: number; velocity: number }
| { type: 'set_pattern_step'; row: number; step: number; velocity: number }
| { type: 'set_pattern_face'; row: number; core: string; face: number };
export type CommandResult =
| { kind: 'snapshot'; state: State }
@@ -93,7 +125,13 @@ export interface ErrorMessage {
message: string;
}
export type ServerMessage = HelloMessage | SnapshotMessage | EventMessage | ResultMessage | ErrorMessage;
export interface ImportedMessage {
type: 'imported';
id: number;
core: CoreSummary;
}
export type ServerMessage = HelloMessage | SnapshotMessage | EventMessage | ResultMessage | ErrorMessage | ImportedMessage;
export interface MeterFrame {
bar: number;