@pepperhorn/sound-module 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/dist/fonts.css +15 -0
- package/dist/index.js +5946 -0
- package/dist/patches/poly-bass/C2.wav +0 -0
- package/dist/patches/poly-bass/C3-a.wav +0 -0
- package/dist/patches/poly-bass/C3-b.wav +0 -0
- package/dist/patches/poly-bass/C4-hard-a.wav +0 -0
- package/dist/patches/poly-bass/C4-hard-b.wav +0 -0
- package/dist/patches/poly-bass/C4-soft.wav +0 -0
- package/dist/patches/sine-key/C3.wav +0 -0
- package/dist/patches/sine-key/C4.wav +0 -0
- package/dist/patches/sine-key/C5.wav +0 -0
- package/dist/styles.css +2019 -0
- package/dist/types/audio/AudioEngine.d.ts +147 -0
- package/dist/types/audio/customSampler.d.ts +43 -0
- package/dist/types/audio/effects.d.ts +111 -0
- package/dist/types/audio/fetchSlots.d.ts +4 -0
- package/dist/types/audio/loadPatch.d.ts +25 -0
- package/dist/types/audio/loggedStorage.d.ts +46 -0
- package/dist/types/audio/preview.d.ts +16 -0
- package/dist/types/components/Knob.d.ts +22 -0
- package/dist/types/components/LiveTouchKeyboard.d.ts +11 -0
- package/dist/types/components/LoadErrorToast.d.ts +14 -0
- package/dist/types/components/LoadingOverlay.d.ts +3 -0
- package/dist/types/components/MidiPanel.d.ts +17 -0
- package/dist/types/components/MidiStatus.d.ts +4 -0
- package/dist/types/components/PatchCard.d.ts +20 -0
- package/dist/types/components/PatchPicker.d.ts +13 -0
- package/dist/types/components/PatchStrip.d.ts +17 -0
- package/dist/types/components/PowerOnOverlay.d.ts +8 -0
- package/dist/types/components/TouchKeyboard.d.ts +17 -0
- package/dist/types/components/overlayEscape.d.ts +8 -0
- package/dist/types/embed/SoundModule.d.ts +16 -0
- package/dist/types/embed/catalog.d.ts +6 -0
- package/dist/types/embed/index.d.ts +2 -0
- package/dist/types/input/breath.d.ts +79 -0
- package/dist/types/input/midiAccess.d.ts +14 -0
- package/dist/types/input/midiMonitor.d.ts +10 -0
- package/dist/types/input/midiProfile.d.ts +77 -0
- package/dist/types/input/useComputerKeyboard.d.ts +19 -0
- package/dist/types/input/useWebMidi.d.ts +10 -0
- package/dist/types/patches/builtin.d.ts +2 -0
- package/dist/types/patches/catalog.d.ts +3 -0
- package/dist/types/patches/types.d.ts +51 -0
- package/dist/types/state/activeNotes.d.ts +4 -0
- package/dist/types/state/useFullCatalog.d.ts +19 -0
- package/dist/types/state/useSoundEngine.d.ts +41 -0
- package/dist/types/state/useStore.d.ts +131 -0
- package/package.json +53 -4
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { PatchManifest } from '../patches/types';
|
|
2
|
+
import type { ChainEffectId } from './effects';
|
|
3
|
+
import type { EffectId } from './effects';
|
|
4
|
+
import { type LoadProgress } from './loadPatch';
|
|
5
|
+
import { type PatchSourceTag } from './loggedStorage';
|
|
6
|
+
/** What a finished load tells the UI about the patch it just produced. */
|
|
7
|
+
export interface LoadOutcome {
|
|
8
|
+
source: PatchSourceTag;
|
|
9
|
+
/** Every sample resolved — the patch is safe to mark as cached for offline. */
|
|
10
|
+
offlineReady: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type { PatchSourceTag } from './loggedStorage';
|
|
13
|
+
export type { LoadProgress } from './loadPatch';
|
|
14
|
+
export declare function subscribeEngineLog(fn: (line: string) => void): () => void;
|
|
15
|
+
export declare function resetDecodeCounters(): void;
|
|
16
|
+
export declare function snapshotDecodeCounters(): {
|
|
17
|
+
failures: string[];
|
|
18
|
+
attempted: number;
|
|
19
|
+
succeeded: number;
|
|
20
|
+
failed: number;
|
|
21
|
+
};
|
|
22
|
+
export interface EngineOptions {
|
|
23
|
+
/** Patch console.warn to surface smplr decode failures. Default true. */
|
|
24
|
+
consoleShim?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Default false. When true (embed), the engine log is silent unless the
|
|
27
|
+
* store's debug mode is on, so a host page's console isn't flooded.
|
|
28
|
+
*/
|
|
29
|
+
quiet?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare class AudioEngine {
|
|
32
|
+
readonly context: AudioContext;
|
|
33
|
+
private chain;
|
|
34
|
+
private current;
|
|
35
|
+
private currentId;
|
|
36
|
+
private loadSeq;
|
|
37
|
+
onLoaded?: (id: string, outcome: LoadOutcome) => void;
|
|
38
|
+
onLoading?: (id: string | null) => void;
|
|
39
|
+
onProgress?: (id: string, progress: LoadProgress) => void;
|
|
40
|
+
onError?: (id: string, err: unknown) => void;
|
|
41
|
+
onSinkChange?: (sinkId: string) => void;
|
|
42
|
+
private currentSource;
|
|
43
|
+
private currentSinkId;
|
|
44
|
+
private instrumentCache;
|
|
45
|
+
private previewBus;
|
|
46
|
+
private previewEntry;
|
|
47
|
+
private previewSeq;
|
|
48
|
+
private previewTimers;
|
|
49
|
+
onPreviewChange?: (id: string | null) => void;
|
|
50
|
+
private doublerState;
|
|
51
|
+
private activeDoubles;
|
|
52
|
+
constructor(options?: EngineOptions);
|
|
53
|
+
/** Silence everything and suspend the context. Chain and caches are kept so resume() plays again. */
|
|
54
|
+
suspend(): Promise<void>;
|
|
55
|
+
resume(): Promise<void>;
|
|
56
|
+
/** Play a short sine pop through the FX chain to confirm the audio path. */
|
|
57
|
+
testTone(seconds?: number, gain?: number): void;
|
|
58
|
+
/**
|
|
59
|
+
* Audio-path latency in ms. baseLatency is the Web Audio buffer; outputLatency
|
|
60
|
+
* adds the OS/device path (Bluetooth sinks typically add 150–250ms here).
|
|
61
|
+
*/
|
|
62
|
+
latencyInfo(): {
|
|
63
|
+
baseLatencyMs: number;
|
|
64
|
+
outputLatencyMs: number | null;
|
|
65
|
+
};
|
|
66
|
+
loadPatch(patch: PatchManifest): Promise<void>;
|
|
67
|
+
private evictCacheIfNeeded;
|
|
68
|
+
/** Drop the in-memory instrument cache (used by purgeAndReload). */
|
|
69
|
+
private clearInstrumentCache;
|
|
70
|
+
get loadedPatchSource(): PatchSourceTag | null;
|
|
71
|
+
private noteOnCount;
|
|
72
|
+
noteOn(midi: number, velocity?: number): void;
|
|
73
|
+
/**
|
|
74
|
+
* Pre-load a patch into the persistent browser cache without making it the
|
|
75
|
+
* active instrument. Used by the picker's "download for offline" button.
|
|
76
|
+
* Returns true if the load completed (samples are now cached).
|
|
77
|
+
*/
|
|
78
|
+
preloadPatch(patch: PatchManifest): Promise<{
|
|
79
|
+
ok: boolean;
|
|
80
|
+
fetched: number;
|
|
81
|
+
attempted: number;
|
|
82
|
+
failed: string[];
|
|
83
|
+
}>;
|
|
84
|
+
private ensurePreviewBus;
|
|
85
|
+
/** Silence and forget any audition in flight. Safe to call at any time. */
|
|
86
|
+
stopPreview(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Audition a patch from the library without making it the active instrument:
|
|
89
|
+
* loads its samples (which fills the offline caches, so a later select is
|
|
90
|
+
* fast and works offline) and plays a C–G run into a dry bus.
|
|
91
|
+
*/
|
|
92
|
+
previewPatch(patch: PatchManifest): Promise<{
|
|
93
|
+
ok: boolean;
|
|
94
|
+
offlineReady: boolean;
|
|
95
|
+
}>;
|
|
96
|
+
/** Trigger middle C through the current instrument — independent of touch input. */
|
|
97
|
+
testCurrentNote(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Nuke every cached/stale layer for the given patch and re-fetch from
|
|
100
|
+
* scratch. Use when Firefox's HTTP cache, the Cache API, OR a stale
|
|
101
|
+
* smplr instance is suspected of holding bad state.
|
|
102
|
+
*/
|
|
103
|
+
purgeAndReload(patch: PatchManifest): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Diagnostic: walk a chromatic scale through the current instrument and
|
|
106
|
+
* log which notes return a non-null voice. Used to figure out why some
|
|
107
|
+
* notes silently produce no sound.
|
|
108
|
+
*/
|
|
109
|
+
testScale(): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Diagnostic: fetch a known vendored sample, decode, and play it directly
|
|
112
|
+
* through the FX chain via a raw AudioBufferSource — bypassing smplr's
|
|
113
|
+
* Channel/Voice/scheduler entirely. If THIS produces sound but live patch
|
|
114
|
+
* notes don't, the bug is inside smplr's voice path. If neither produces
|
|
115
|
+
* sound, the FX chain is broken.
|
|
116
|
+
*/
|
|
117
|
+
testRawBuffer(): Promise<void>;
|
|
118
|
+
noteOff(midi: number): void;
|
|
119
|
+
panic(): void;
|
|
120
|
+
/** Breath/expression level 0..1 on the shared pre-FX gain stage. */
|
|
121
|
+
setBreath(level: number, smoothingMs?: number, immediate?: boolean): void;
|
|
122
|
+
/**
|
|
123
|
+
* Jump the shared breath gain to full, for notes that are not breath
|
|
124
|
+
* controlled (touch / computer keyboard). Immediate, so smoothing is moot.
|
|
125
|
+
*/
|
|
126
|
+
liftBreath(): void;
|
|
127
|
+
setEffectParam(id: EffectId, paramId: string, value: number): void;
|
|
128
|
+
/** Rewire the audio chain into this order. Note-level effects are unaffected. */
|
|
129
|
+
setEffectOrder(order: ChainEffectId[]): void;
|
|
130
|
+
setEffectEnabled(id: EffectId, on: boolean): void;
|
|
131
|
+
get loadedPatchId(): string | null;
|
|
132
|
+
/** True on browsers where AudioContext.setSinkId is implemented. */
|
|
133
|
+
get sinkIdSupported(): boolean;
|
|
134
|
+
/** Current routing target: '' = system default. */
|
|
135
|
+
get sinkId(): string;
|
|
136
|
+
/**
|
|
137
|
+
* Route all audio output to the given deviceId (as returned by
|
|
138
|
+
* navigator.mediaDevices.enumerateDevices() { kind: 'audiooutput' }). Pass
|
|
139
|
+
* '' to route to the OS default output.
|
|
140
|
+
*
|
|
141
|
+
* USB audio interfaces — including class-compliant (UAC1/UAC2) devices —
|
|
142
|
+
* show up in enumerateDevices() as regular audiooutput entries once the
|
|
143
|
+
* OS has claimed the device, so this is the correct path for them.
|
|
144
|
+
*/
|
|
145
|
+
setSinkId(deviceId: string): Promise<void>;
|
|
146
|
+
}
|
|
147
|
+
export declare function getEngine(options?: EngineOptions): AudioEngine;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SampleEntry } from '../patches/types';
|
|
2
|
+
interface SamplerSource {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
samples: Record<string, SampleEntry>;
|
|
5
|
+
}
|
|
6
|
+
interface LoadProgressLike {
|
|
7
|
+
loaded: number;
|
|
8
|
+
total: number;
|
|
9
|
+
}
|
|
10
|
+
/** The subset of the shared storage cascade this sampler needs. */
|
|
11
|
+
interface SampleStorage {
|
|
12
|
+
fetch(url: string): Promise<Response>;
|
|
13
|
+
}
|
|
14
|
+
export declare class CustomSampler {
|
|
15
|
+
readonly context: AudioContext;
|
|
16
|
+
readonly output: GainNode;
|
|
17
|
+
readonly load: Promise<this>;
|
|
18
|
+
private regions;
|
|
19
|
+
private byMidi;
|
|
20
|
+
private active;
|
|
21
|
+
private destination?;
|
|
22
|
+
private onLoadProgress?;
|
|
23
|
+
private storage?;
|
|
24
|
+
private totalSamples;
|
|
25
|
+
private loadedSamples;
|
|
26
|
+
constructor(context: AudioContext, options: {
|
|
27
|
+
source: SamplerSource;
|
|
28
|
+
destination?: AudioNode;
|
|
29
|
+
onLoadProgress?: (progress: LoadProgressLike) => void;
|
|
30
|
+
storage?: SampleStorage;
|
|
31
|
+
});
|
|
32
|
+
private loadSource;
|
|
33
|
+
private loadEntry;
|
|
34
|
+
private fetchBuffer;
|
|
35
|
+
start(event: {
|
|
36
|
+
note: number | string;
|
|
37
|
+
velocity?: number;
|
|
38
|
+
}): () => void;
|
|
39
|
+
stop(target?: number | string): void;
|
|
40
|
+
disconnect(): void;
|
|
41
|
+
private findNearestMidi;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { PatchColor } from '../patches/types';
|
|
2
|
+
export type EffectId = 'distortion' | 'doubler1' | 'doubler2' | 'chorus' | 'delay' | 'reverb';
|
|
3
|
+
export interface EffectParamDef {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
step: number;
|
|
9
|
+
default: number;
|
|
10
|
+
unit?: string;
|
|
11
|
+
labels?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface EffectDef {
|
|
14
|
+
id: EffectId;
|
|
15
|
+
name: string;
|
|
16
|
+
color: PatchColor;
|
|
17
|
+
params: EffectParamDef[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The effects that actually exist as audio nodes, in their default order.
|
|
21
|
+
*
|
|
22
|
+
* The doublers are deliberately absent: they are note-level triggers fired in
|
|
23
|
+
* AudioEngine.noteOn, not nodes in the graph, so "where they sit in the chain"
|
|
24
|
+
* has no meaning and they cannot be reordered.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CHAIN_EFFECT_IDS: readonly ["distortion", "chorus", "delay", "reverb"];
|
|
27
|
+
export type ChainEffectId = (typeof CHAIN_EFFECT_IDS)[number];
|
|
28
|
+
export declare function isChainEffectId(id: string): id is ChainEffectId;
|
|
29
|
+
/**
|
|
30
|
+
* Coerce anything — persisted state, a saved preset from an older build — into
|
|
31
|
+
* a usable chain order: every effect exactly once. Unknown ids are dropped and
|
|
32
|
+
* missing ones appended in default order, so a stored order can never leave an
|
|
33
|
+
* effect silently unwired.
|
|
34
|
+
*/
|
|
35
|
+
export declare function normalizeFxOrder(input: unknown): ChainEffectId[];
|
|
36
|
+
/** The order with `id` moved by `delta` places, clamped at the ends. */
|
|
37
|
+
export declare function moveInOrder(order: ChainEffectId[], id: ChainEffectId, delta: number): ChainEffectId[];
|
|
38
|
+
export declare const DOUBLER_SNAPS: readonly [{
|
|
39
|
+
readonly st: -12;
|
|
40
|
+
readonly label: "-OCT";
|
|
41
|
+
}, {
|
|
42
|
+
readonly st: -11;
|
|
43
|
+
readonly label: "-M7";
|
|
44
|
+
}, {
|
|
45
|
+
readonly st: -10;
|
|
46
|
+
readonly label: "-m7";
|
|
47
|
+
}, {
|
|
48
|
+
readonly st: -7;
|
|
49
|
+
readonly label: "-5";
|
|
50
|
+
}, {
|
|
51
|
+
readonly st: -5;
|
|
52
|
+
readonly label: "-4";
|
|
53
|
+
}, {
|
|
54
|
+
readonly st: -4;
|
|
55
|
+
readonly label: "-M3";
|
|
56
|
+
}, {
|
|
57
|
+
readonly st: -3;
|
|
58
|
+
readonly label: "-m3";
|
|
59
|
+
}, {
|
|
60
|
+
readonly st: 0;
|
|
61
|
+
readonly label: "UNI";
|
|
62
|
+
}, {
|
|
63
|
+
readonly st: 3;
|
|
64
|
+
readonly label: "m3";
|
|
65
|
+
}, {
|
|
66
|
+
readonly st: 4;
|
|
67
|
+
readonly label: "M3";
|
|
68
|
+
}, {
|
|
69
|
+
readonly st: 5;
|
|
70
|
+
readonly label: "4";
|
|
71
|
+
}, {
|
|
72
|
+
readonly st: 7;
|
|
73
|
+
readonly label: "5";
|
|
74
|
+
}, {
|
|
75
|
+
readonly st: 10;
|
|
76
|
+
readonly label: "m7";
|
|
77
|
+
}, {
|
|
78
|
+
readonly st: 11;
|
|
79
|
+
readonly label: "M7";
|
|
80
|
+
}, {
|
|
81
|
+
readonly st: 12;
|
|
82
|
+
readonly label: "+OCT";
|
|
83
|
+
}];
|
|
84
|
+
export declare const EFFECT_DEFS: EffectDef[];
|
|
85
|
+
export declare function defaultParams(): Record<EffectId, Record<string, number>>;
|
|
86
|
+
export interface EffectChain {
|
|
87
|
+
/**
|
|
88
|
+
* Where instruments connect. This node's identity never changes, including
|
|
89
|
+
* across a reorder — instruments are wired to it at construction and kept in
|
|
90
|
+
* the engine's LRU cache, so a chain whose input was the first effect would
|
|
91
|
+
* silence every cached instrument the moment the order changed.
|
|
92
|
+
*/
|
|
93
|
+
inputNode: AudioNode;
|
|
94
|
+
master: GainNode;
|
|
95
|
+
setEnabled: (id: EffectId, on: boolean) => void;
|
|
96
|
+
setParam: (id: EffectId, paramId: string, value: number) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Reorder the audio-graph effects in series. Non-chain ids (the note-level
|
|
99
|
+
* doublers) are ignored and missing ones appended — see normalizeFxOrder.
|
|
100
|
+
*/
|
|
101
|
+
setOrder: (order: readonly EffectId[]) => void;
|
|
102
|
+
getOrder: () => ChainEffectId[];
|
|
103
|
+
/**
|
|
104
|
+
* Pre-FX breath/expression gain (0..1), applied on inputNode itself: ahead
|
|
105
|
+
* of the FX so reverb/delay tails ring out when breath stops, and untouched
|
|
106
|
+
* by setOrder (which only rewires inputNode's outputs).
|
|
107
|
+
*/
|
|
108
|
+
setBreath: (level: number, smoothingMs: number, immediate?: boolean) => void;
|
|
109
|
+
dispose: () => void;
|
|
110
|
+
}
|
|
111
|
+
export declare function buildEffectChain(context: AudioContext): EffectChain;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PatchManifest } from '../patches/types';
|
|
2
|
+
export interface LoadProgress {
|
|
3
|
+
loaded: number;
|
|
4
|
+
total: number;
|
|
5
|
+
}
|
|
6
|
+
export interface LoadedInstrument {
|
|
7
|
+
start(event: {
|
|
8
|
+
note: number | string;
|
|
9
|
+
velocity?: number;
|
|
10
|
+
}): () => void;
|
|
11
|
+
stop(target?: number | string): void;
|
|
12
|
+
disconnect(): void;
|
|
13
|
+
load: Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
interface InstantiateOptions {
|
|
16
|
+
context: AudioContext;
|
|
17
|
+
destination: AudioNode;
|
|
18
|
+
patch: PatchManifest;
|
|
19
|
+
onLoadProgress?: (progress: LoadProgress) => void;
|
|
20
|
+
storage?: {
|
|
21
|
+
fetch: (url: string) => Promise<Response>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function instantiatePatch(opts: InstantiateOptions): LoadedInstrument;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type SourceTier = 'local' | 'localAlt' | 'cache' | 'cdn';
|
|
2
|
+
export interface LoggedStorageStats {
|
|
3
|
+
attempted: number;
|
|
4
|
+
succeeded: number;
|
|
5
|
+
failed: string[];
|
|
6
|
+
bySource: Record<SourceTier, number>;
|
|
7
|
+
}
|
|
8
|
+
export interface PatchSourceTag {
|
|
9
|
+
tier: 'local' | 'cache' | 'cdn' | 'mixed' | 'unknown';
|
|
10
|
+
local: number;
|
|
11
|
+
cache: number;
|
|
12
|
+
cdn: number;
|
|
13
|
+
total: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function computeSourceTag(stats: LoggedStorageStats): PatchSourceTag;
|
|
16
|
+
/**
|
|
17
|
+
* How much of a patch actually arrived.
|
|
18
|
+
*
|
|
19
|
+
* This matters because smplr's loader resolves whatever happens: a sample that
|
|
20
|
+
* 404s or fails to decode is dropped with a `console.warn` and the load
|
|
21
|
+
* promise still fulfils (see `loadAudioBuffer` in smplr, and its own comment
|
|
22
|
+
* "Failed samples are silently omitted"). So "the load resolved" says nothing
|
|
23
|
+
* about whether the instrument can make a sound — only these counts do.
|
|
24
|
+
*/
|
|
25
|
+
export type LoadCompleteness = 'complete' | 'degraded' | 'empty';
|
|
26
|
+
export declare function classifyLoad(stats: LoggedStorageStats): LoadCompleteness;
|
|
27
|
+
/**
|
|
28
|
+
* True when a completed load left the patch genuinely playable offline: every
|
|
29
|
+
* sample request was satisfied and at least one actually resolved. A load with
|
|
30
|
+
* missing samples still makes noise, but marking it "downloaded" would lie to
|
|
31
|
+
* the picker's offline greyout, so it does not count.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isOfflineReady(stats: LoggedStorageStats): boolean;
|
|
34
|
+
export type SampleSourceMode = 'app' | 'cdn';
|
|
35
|
+
/** Choose between the app's local-first cascade and direct CDN fetching. */
|
|
36
|
+
export declare function configureSampleSource(config: {
|
|
37
|
+
mode: SampleSourceMode;
|
|
38
|
+
}): void;
|
|
39
|
+
export declare function getSampleSourceMode(): SampleSourceMode;
|
|
40
|
+
export interface LoggedStorage {
|
|
41
|
+
fetch: (url: string) => Promise<Response>;
|
|
42
|
+
reset(): void;
|
|
43
|
+
snapshot(): LoggedStorageStats;
|
|
44
|
+
}
|
|
45
|
+
export declare function clearCdnCache(): Promise<void>;
|
|
46
|
+
export declare function createLoggedStorage(onEvent?: (line: string) => void): LoggedStorage;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PatchManifest } from '../patches/types';
|
|
2
|
+
export interface PreviewEvent {
|
|
3
|
+
notes: number[];
|
|
4
|
+
/** Milliseconds after the audition starts. */
|
|
5
|
+
at: number;
|
|
6
|
+
/** Milliseconds to hold before releasing. */
|
|
7
|
+
hold: number;
|
|
8
|
+
}
|
|
9
|
+
/** A C–G run followed by a C major triad. */
|
|
10
|
+
export declare function previewSequence(rootMidi: number): PreviewEvent[];
|
|
11
|
+
export declare function previewDurationMs(events: PreviewEvent[]): number;
|
|
12
|
+
/**
|
|
13
|
+
* C in the patch's own preferred octave, clamped to a range every sample set
|
|
14
|
+
* can reach — a bass patch auditioned at C4 would be out of its sampled range.
|
|
15
|
+
*/
|
|
16
|
+
export declare function previewRootMidi(patch: Pick<PatchManifest, 'defaultOctave'>): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface KnobProps {
|
|
2
|
+
label: string;
|
|
3
|
+
value: number;
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
step: number;
|
|
7
|
+
unit?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
size?: number;
|
|
10
|
+
/** Discrete position names (e.g. doubler intervals) shown instead of a number. */
|
|
11
|
+
labels?: string[];
|
|
12
|
+
defaultValue?: number;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
onChange: (value: number) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A skeuomorphic pot: coloured value arc, skirt collar and domed cap. Drag
|
|
18
|
+
* up/down or scroll to turn (shift = fine), arrow keys step it, double-click
|
|
19
|
+
* returns it to its default.
|
|
20
|
+
*/
|
|
21
|
+
export declare function Knob({ label, value, min, max, step, unit, color, size, labels, defaultValue, disabled, onChange, }: KnobProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The on-screen keyboard wired to the live active-note set. Kept as its own
|
|
3
|
+
* component so only the keyboard re-renders per note.
|
|
4
|
+
*/
|
|
5
|
+
export declare function LiveTouchKeyboard(props: {
|
|
6
|
+
noteOn: (midi: number, vel: number) => void;
|
|
7
|
+
noteOff: (midi: number) => void;
|
|
8
|
+
fill?: boolean;
|
|
9
|
+
keyLabels?: boolean;
|
|
10
|
+
octaves?: number;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surfaced when a patch load fails. Before this existed a failed load left the
|
|
3
|
+
* LCD showing a patch that was never actually loaded, with no clue why the keys
|
|
4
|
+
* were silent — the single most confusing failure mode of the loader.
|
|
5
|
+
*/
|
|
6
|
+
export declare function LoadErrorToast({ onRetry, inline, }: {
|
|
7
|
+
onRetry: (id: string) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Sit in the layout instead of floating. Keyboard mode fills the viewport and
|
|
10
|
+
* ends in the touch controls, so a floating toast would cover panic and the
|
|
11
|
+
* octave buttons no matter which edge it was pinned to.
|
|
12
|
+
*/
|
|
13
|
+
inline?: boolean;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface LatencyInfo {
|
|
2
|
+
baseLatencyMs: number;
|
|
3
|
+
outputLatencyMs: number | null;
|
|
4
|
+
}
|
|
5
|
+
interface MidiPanelProps {
|
|
6
|
+
/** Reads the AudioContext latency (engine.latencyInfo). Optional for tests. */
|
|
7
|
+
getLatency?: () => LatencyInfo;
|
|
8
|
+
/** Render absolutely inside the nearest positioned ancestor (embed). */
|
|
9
|
+
contained?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* MIDI & breath controls sheet, opened from the MIDI status pill. Shows the
|
|
13
|
+
* permission state first; once granted, devices, presets, the live breath
|
|
14
|
+
* meter and the breath shaping controls.
|
|
15
|
+
*/
|
|
16
|
+
export declare function MidiPanel({ getLatency, contained }: MidiPanelProps): import("react/jsx-runtime").JSX.Element | null;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PatchManifest } from '../patches/types';
|
|
2
|
+
interface PatchCardProps {
|
|
3
|
+
patch: PatchManifest;
|
|
4
|
+
active: boolean;
|
|
5
|
+
favourited: boolean;
|
|
6
|
+
downloaded: boolean;
|
|
7
|
+
downloading: boolean;
|
|
8
|
+
/** True when we're offline AND this patch isn't cached locally. */
|
|
9
|
+
unavailableOffline?: boolean;
|
|
10
|
+
/** This card's audition is loading or playing. */
|
|
11
|
+
previewing?: boolean;
|
|
12
|
+
onSelect: (id: string) => void;
|
|
13
|
+
onPreview: (id: string) => void;
|
|
14
|
+
onToggleFavourite: (id: string) => void;
|
|
15
|
+
onDownload?: (id: string) => void;
|
|
16
|
+
/** Render the offline download button. Off in the embed. */
|
|
17
|
+
showDownload?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function PatchCard({ patch, active, favourited, downloaded, downloading, unavailableOffline, previewing, onSelect, onPreview, onToggleFavourite, onDownload, showDownload, }: PatchCardProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface PatchPickerProps {
|
|
2
|
+
onSelect: (id: string) => void;
|
|
3
|
+
/** Audition a patch without switching to it. */
|
|
4
|
+
onPreview: (id: string) => void;
|
|
5
|
+
/** Fetch a patch's samples into the offline caches. */
|
|
6
|
+
onDownload?: (id: string) => void;
|
|
7
|
+
/** Render absolutely inside the nearest positioned ancestor (embed). */
|
|
8
|
+
contained?: boolean;
|
|
9
|
+
/** Show download buttons and the offline pill. Off in the embed. */
|
|
10
|
+
offlineFeatures?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function PatchPicker({ onSelect, onPreview, onDownload, contained, offlineFeatures, }: PatchPickerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PatchManifest } from '../patches/types';
|
|
2
|
+
import type { PatchSourceTag } from '../audio/AudioEngine';
|
|
3
|
+
interface PatchStripProps {
|
|
4
|
+
patch: PatchManifest | undefined;
|
|
5
|
+
index: number;
|
|
6
|
+
total: number;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
source: PatchSourceTag | null;
|
|
9
|
+
onStep: (delta: number) => void;
|
|
10
|
+
onBrowse: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The library readout for keyboard mode: everything you need to know about the
|
|
14
|
+
* loaded patch in one thin bar, so the rest of the screen belongs to the keys.
|
|
15
|
+
*/
|
|
16
|
+
export declare function PatchStrip({ patch, index, total, loading, source, onStep, onBrowse, }: PatchStripProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "tap to power on" scrim browsers need before audio can start. `contained`
|
|
3
|
+
* pins it to the nearest positioned ancestor instead of the viewport (embed).
|
|
4
|
+
*/
|
|
5
|
+
export declare function PowerOnOverlay({ onPowerOn, contained, }: {
|
|
6
|
+
onPowerOn: () => void;
|
|
7
|
+
contained?: boolean;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface TouchKeyboardProps {
|
|
2
|
+
noteOn: (midi: number, velocity: number) => void;
|
|
3
|
+
noteOff: (midi: number) => void;
|
|
4
|
+
/** Notes currently sounding from ANY source (computer keyboard, MIDI, touch) —
|
|
5
|
+
* used to light the on-screen keys. Touch also feeds this via App.noteOn. */
|
|
6
|
+
activeNotes?: ReadonlySet<number>;
|
|
7
|
+
octaves?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Stretch to fill the parent instead of using the fixed rack height. Used by
|
|
10
|
+
* keyboard mode, where the keys take the lion's share of the viewport.
|
|
11
|
+
*/
|
|
12
|
+
fill?: boolean;
|
|
13
|
+
/** Show the computer-keyboard letter that plays each key (follows the octave). */
|
|
14
|
+
keyLabels?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function TouchKeyboard({ noteOn, noteOff, activeNotes, octaves, fill, keyLabels, }: TouchKeyboardProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an overlay's Escape handler (bound on window) should act on `e`.
|
|
3
|
+
* Full-screen app overlays own the whole page, so any Escape counts. A
|
|
4
|
+
* contained (embed) overlay only answers Escape pressed inside the element it
|
|
5
|
+
* is drawn in — the module root — so Escape elsewhere on the host page is left
|
|
6
|
+
* to the host.
|
|
7
|
+
*/
|
|
8
|
+
export declare function escapeIsForOverlay(e: KeyboardEvent, overlay: HTMLElement | null, contained: boolean): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SoundModuleProps {
|
|
2
|
+
/** Base URL that serves /patches/**. becreative passes "". Omitted → CST patches hidden. */
|
|
3
|
+
assetBaseUrl?: string;
|
|
4
|
+
/** Applied to the component root only. Default 'dark'. */
|
|
5
|
+
theme?: 'dark' | 'light';
|
|
6
|
+
/** On-screen piano octaves. Default 2 (25 keys). */
|
|
7
|
+
octaves?: number;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The embeddable sound module: patch strip, status row and a labelled
|
|
12
|
+
* on-screen piano, with every overlay drawn inside its own root. The computer
|
|
13
|
+
* keyboard plays only while focus is inside the module; MIDI and breath play
|
|
14
|
+
* regardless.
|
|
15
|
+
*/
|
|
16
|
+
export declare function SoundModule({ assetBaseUrl, theme, octaves, className }: SoundModuleProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PatchManifest } from '../patches/types';
|
|
2
|
+
export declare function joinAssetUrl(base: string, path: string): string;
|
|
3
|
+
export declare function embedCatalog(catalog: PatchManifest[], assetBaseUrl: string | undefined): PatchManifest[];
|
|
4
|
+
export declare function configureEmbed(opts: {
|
|
5
|
+
assetBaseUrl?: string;
|
|
6
|
+
}): void;
|