@pepperhorn/sound-module 0.0.0-stage → 0.1.0
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 +44 -4
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type BreathCurve = 'linear' | 'exponential' | 'logarithmic';
|
|
2
|
+
export interface BreathConfig {
|
|
3
|
+
/** CC number carrying breath. Odisei + EWI default to CC2. */
|
|
4
|
+
cc: number;
|
|
5
|
+
/** Below this (0..127) the gate is shut. */
|
|
6
|
+
gateThreshold: number;
|
|
7
|
+
/** Gain at the gate threshold. */
|
|
8
|
+
floor: number;
|
|
9
|
+
/** Gain at full breath. */
|
|
10
|
+
ceiling: number;
|
|
11
|
+
curve: BreathCurve;
|
|
12
|
+
/** setTargetAtTime time constant for continuous breath changes. */
|
|
13
|
+
smoothingMs: number;
|
|
14
|
+
/** 'fixed' lets breath own loudness; the Odisei velocity is breath-derived. */
|
|
15
|
+
velocityMode: 'fixed' | 'passthrough';
|
|
16
|
+
fixedVelocity: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const WIND_BREATH_DEFAULTS: BreathConfig;
|
|
19
|
+
/** Map a raw breath CC (0..127) to a 0..1 gain. Returns 0 when gated. */
|
|
20
|
+
export declare function breathToGain(cc: number, config: BreathConfig): number;
|
|
21
|
+
export interface BreathOutputs {
|
|
22
|
+
noteOn: (midi: number, velocity: number) => void;
|
|
23
|
+
noteOff: (midi: number) => void;
|
|
24
|
+
/** immediate = jump straight to level (gate opening, reset). */
|
|
25
|
+
setBreath: (level: number, smoothingMs: number, immediate: boolean) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Note gate + breath router for one MIDI input stream.
|
|
29
|
+
*
|
|
30
|
+
* Inactive until the first breath CC arrives, so a plain keyboard never gets
|
|
31
|
+
* gated. Once active, note-ons only sound while breath is above the gate
|
|
32
|
+
* threshold; held keys resume when breath comes back.
|
|
33
|
+
*/
|
|
34
|
+
export declare class BreathController {
|
|
35
|
+
private active;
|
|
36
|
+
private open;
|
|
37
|
+
private level;
|
|
38
|
+
/** Keys the device currently holds down. */
|
|
39
|
+
private held;
|
|
40
|
+
/** Notes we've actually started on the engine. */
|
|
41
|
+
private sounding;
|
|
42
|
+
private lastWriteAt;
|
|
43
|
+
private pendingWrite;
|
|
44
|
+
/** Last raw breath value (0..127), so config changes can re-evaluate it. */
|
|
45
|
+
private lastRaw;
|
|
46
|
+
/** A breath CC has been seen at least once (even while disabled). */
|
|
47
|
+
private hasRaw;
|
|
48
|
+
private enabled;
|
|
49
|
+
private config;
|
|
50
|
+
private readonly outputs;
|
|
51
|
+
private readonly now;
|
|
52
|
+
constructor(outputs: BreathOutputs, config?: BreathConfig, now?: () => number, enabled?: boolean);
|
|
53
|
+
get isActive(): boolean;
|
|
54
|
+
get isEnabled(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Apply a new breath config at runtime without dropping held notes.
|
|
57
|
+
*
|
|
58
|
+
* - Disabling while active drops held-but-gated keys without starting them
|
|
59
|
+
* (no idle-note drone), keeps notes already sounding, returns the shared
|
|
60
|
+
* gain to 1, and makes this input pure pass-through from then on.
|
|
61
|
+
* - Any other change re-evaluates the last breath value under the new config,
|
|
62
|
+
* so a gate/curve/floor tweak is heard immediately. A CC change just
|
|
63
|
+
* re-targets which controller is listened to; the gate keeps its state until
|
|
64
|
+
* the new CC speaks.
|
|
65
|
+
*/
|
|
66
|
+
setConfig(config: BreathConfig, enabled?: boolean): void;
|
|
67
|
+
noteOn(midi: number, velocity: number): void;
|
|
68
|
+
noteOff(midi: number): void;
|
|
69
|
+
/** Feed a Control Change. Returns true if it was the breath CC. */
|
|
70
|
+
controlChange(cc: number, value: number): boolean;
|
|
71
|
+
breath(value: number): void;
|
|
72
|
+
/** Panic: release and forget every note, keep the gate state. */
|
|
73
|
+
releaseAll(): void;
|
|
74
|
+
/** Breath source gone (device unplugged): release everything, un-gate. */
|
|
75
|
+
reset(): void;
|
|
76
|
+
private start;
|
|
77
|
+
private writeNow;
|
|
78
|
+
private writeThrottled;
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function isMidiSupported(): boolean;
|
|
2
|
+
export declare function getMidiAccess(): MIDIAccess | null;
|
|
3
|
+
/** Called with the MIDIAccess once granted (immediately if already held). */
|
|
4
|
+
export declare function onMidiAccess(fn: (a: MIDIAccess) => void): () => void;
|
|
5
|
+
/** Classify a requestMIDIAccess rejection. */
|
|
6
|
+
export declare function classifyMidiError(err: unknown): 'denied' | 'error';
|
|
7
|
+
/** Ask for MIDI access (shows Chrome's prompt the first time). Deduped. */
|
|
8
|
+
export declare function requestMidi(): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* On load, reconnect silently only when it cannot raise a prompt: the browser
|
|
11
|
+
* already reports `granted`, or the user enabled MIDI on a previous visit.
|
|
12
|
+
* A `denied` permission is reflected straight away so the UI can explain it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function autoRequestMidi(): Promise<void>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const breathMonitor: {
|
|
2
|
+
/** Last value on the configured breath CC, or -1 if none has arrived. */
|
|
3
|
+
raw: number;
|
|
4
|
+
/** Last CC of any number, so "breath on the wrong CC" is visible. */
|
|
5
|
+
lastCc: number;
|
|
6
|
+
lastCcValue: number;
|
|
7
|
+
/** performance.now() of the last breath-CC message. */
|
|
8
|
+
at: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function resetBreathMonitor(): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type BreathConfig } from './breath';
|
|
2
|
+
export declare const MIDI_PROFILE_KEY = "module:midi-map:v1";
|
|
3
|
+
/** Set once MIDI access has been granted, so the next visit can reconnect silently. */
|
|
4
|
+
export declare const MIDI_ENABLED_KEY = "module:midi-enabled:v1";
|
|
5
|
+
export type MidiPresetId = 'travel-sax-2' | 'travel-clarinet' | 'ewi-usb' | 'keyboard';
|
|
6
|
+
export interface MidiProfile {
|
|
7
|
+
/** The preset last applied. Tweaks keep it; the UI shows "modified". */
|
|
8
|
+
preset: MidiPresetId;
|
|
9
|
+
breath: BreathConfig;
|
|
10
|
+
/** Breath gating on/off. Off = every input is plain pass-through. */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface MidiPreset {
|
|
14
|
+
id: MidiPresetId;
|
|
15
|
+
label: string;
|
|
16
|
+
short: string;
|
|
17
|
+
breath: BreathConfig;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const MIDI_PRESETS: Record<MidiPresetId, MidiPreset>;
|
|
21
|
+
export declare const MIDI_PRESET_ORDER: MidiPresetId[];
|
|
22
|
+
/**
|
|
23
|
+
* Travel Sax 2 by default. Breath only arms once a breath CC actually arrives,
|
|
24
|
+
* so a plain keyboard still plays straight through under this preset.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_MIDI_PRESET: MidiPresetId;
|
|
27
|
+
/** The Breath Channel choices the Odisei apps offer. Learn can set any CC. */
|
|
28
|
+
export declare const BREATH_CC_CHOICES: readonly [2, 7, 11];
|
|
29
|
+
export declare const BREATH_LIMITS: {
|
|
30
|
+
readonly gateThreshold: {
|
|
31
|
+
readonly min: 0;
|
|
32
|
+
readonly max: 127;
|
|
33
|
+
};
|
|
34
|
+
readonly floor: {
|
|
35
|
+
readonly min: 0;
|
|
36
|
+
readonly max: 1;
|
|
37
|
+
};
|
|
38
|
+
readonly ceiling: {
|
|
39
|
+
readonly min: 0;
|
|
40
|
+
readonly max: 1;
|
|
41
|
+
};
|
|
42
|
+
readonly smoothingMs: {
|
|
43
|
+
readonly min: 1;
|
|
44
|
+
readonly max: 60;
|
|
45
|
+
};
|
|
46
|
+
readonly fixedVelocity: {
|
|
47
|
+
readonly min: 1;
|
|
48
|
+
readonly max: 127;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function presetProfile(id: MidiPresetId): MidiProfile;
|
|
52
|
+
/**
|
|
53
|
+
* Coerce anything (stored JSON, a hand-edited payload, a future version) into a
|
|
54
|
+
* valid profile. Numbers are clamped; an unknown curve / CC / velocity mode
|
|
55
|
+
* falls back to the profile's preset value. Never throws.
|
|
56
|
+
*/
|
|
57
|
+
export declare function normalizeMidiProfile(raw: unknown): MidiProfile;
|
|
58
|
+
/** True when the profile still equals its preset (nothing tweaked). */
|
|
59
|
+
export declare function profileMatchesPreset(p: MidiProfile): boolean;
|
|
60
|
+
export declare function loadMidiProfile(): MidiProfile;
|
|
61
|
+
export declare function saveMidiProfile(p: MidiProfile): void;
|
|
62
|
+
export declare function loadMidiEnabledFlag(): boolean;
|
|
63
|
+
export declare function saveMidiEnabledFlag(v: boolean): void;
|
|
64
|
+
export interface LearnTrack {
|
|
65
|
+
min: number;
|
|
66
|
+
max: number;
|
|
67
|
+
count: number;
|
|
68
|
+
}
|
|
69
|
+
export type LearnState = Map<string, LearnTrack>;
|
|
70
|
+
export declare const LEARN_MIN_SPAN = 40;
|
|
71
|
+
export declare const LEARN_MIN_MESSAGES = 6;
|
|
72
|
+
/**
|
|
73
|
+
* Feed one CC message into the learn tracker. Returns the CC number once some
|
|
74
|
+
* (input, cc) pair has swept at least `minSpan` values over at least
|
|
75
|
+
* `minMessages` messages, else null. Mutates `state` only.
|
|
76
|
+
*/
|
|
77
|
+
export declare function observeLearn(state: LearnState, inputId: string, cc: number, value: number, minSpan?: number, minMessages?: number): number | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
export declare const KEY_TO_SEMITONE: Readonly<Record<string, number>>;
|
|
3
|
+
/** The computer key that plays semitone `n` (0..17), upper-cased; null outside that range. */
|
|
4
|
+
export declare function keyLabelForSemitone(n: number): string | null;
|
|
5
|
+
interface KeyboardCallbacks {
|
|
6
|
+
noteOn: (midi: number, velocity: number) => void;
|
|
7
|
+
noteOff: (midi: number) => void;
|
|
8
|
+
panic: () => void;
|
|
9
|
+
}
|
|
10
|
+
export interface ComputerKeyboardOptions {
|
|
11
|
+
/** Focus-scoped mode: listen on this element only; keys play only while focus is inside it. */
|
|
12
|
+
target?: RefObject<HTMLElement | null>;
|
|
13
|
+
}
|
|
14
|
+
/** A modal dialog (MIDI sheet, save dialog...) owns the keyboard while open. */
|
|
15
|
+
export declare function isModalOpen(scope?: ParentNode | null): boolean;
|
|
16
|
+
export declare function useComputerKeyboard(callbacks: KeyboardCallbacks, options?: ComputerKeyboardOptions): {
|
|
17
|
+
armed: boolean;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BreathOutputs } from './breath';
|
|
2
|
+
type WebMidiCallbacks = BreathOutputs;
|
|
3
|
+
/**
|
|
4
|
+
* Wires granted MIDI access to the note/breath path. Access itself is requested
|
|
5
|
+
* on demand (see midiAccess.ts); on mount we only reconnect silently when the
|
|
6
|
+
* browser already granted it. Returns a stable panic function that releases
|
|
7
|
+
* every MIDI-held note.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useWebMidi(callbacks: WebMidiCallbacks): () => void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type VelocityTag = 'pp' | 'p' | 'mp' | 'mf' | 'f' | 'ff' | 'default';
|
|
2
|
+
export type SampleEntry = string | string[] | Partial<Record<VelocityTag, string | string[]>>;
|
|
3
|
+
export type SoundfontKit = 'MusyngKite' | 'FluidR3_GM';
|
|
4
|
+
export type PatchSource = {
|
|
5
|
+
kind: 'soundfont';
|
|
6
|
+
instrument: string;
|
|
7
|
+
kit?: SoundfontKit;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'piano';
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'electric-piano';
|
|
12
|
+
instrument: 'CP80' | 'PianetT' | 'WurlitzerEP200' | 'TX81Z';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'mallet';
|
|
15
|
+
instrument: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'mellotron';
|
|
18
|
+
instrument: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'smolken';
|
|
21
|
+
instrument: 'Arco' | 'Pizzicato' | 'Switched';
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'sampler';
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
samples: Record<string, SampleEntry>;
|
|
26
|
+
};
|
|
27
|
+
export type Bank = 'GM' | 'PNO' | 'EP' | 'MAL' | 'MEL' | 'BAS' | 'CST' | 'USR';
|
|
28
|
+
export interface PatchManifest {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
category: string;
|
|
32
|
+
bank: Bank;
|
|
33
|
+
color: PatchColor;
|
|
34
|
+
description?: string;
|
|
35
|
+
defaultOctave?: number;
|
|
36
|
+
source: PatchSource;
|
|
37
|
+
}
|
|
38
|
+
export type PatchColor = 'coral' | 'amber' | 'lime' | 'sky' | 'lavender' | 'peach' | 'mint' | 'rose';
|
|
39
|
+
export declare const BANK_LABELS: Record<Bank, string>;
|
|
40
|
+
export declare const BANK_ORDER: Bank[];
|
|
41
|
+
export interface UserPatch {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
basePatchId: string;
|
|
45
|
+
color: PatchColor;
|
|
46
|
+
fxEnabled: Record<string, boolean>;
|
|
47
|
+
fxParams: Record<string, Record<string, number>>;
|
|
48
|
+
/** Chain order at save time. Absent on presets saved before ordering existed. */
|
|
49
|
+
fxOrder?: string[];
|
|
50
|
+
createdAt: number;
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PatchManifest, UserPatch } from '../patches/types';
|
|
2
|
+
/**
|
|
3
|
+
* Present a user-saved preset as a normal patch manifest so the LCD, the
|
|
4
|
+
* picker, prev/next and the loading overlay can all treat it like any other
|
|
5
|
+
* entry. It reuses the base patch's audio source, which is what lets the
|
|
6
|
+
* engine's instrument cache serve both from one load.
|
|
7
|
+
*
|
|
8
|
+
* Returns null when the base patch no longer exists (e.g. a preset synced from
|
|
9
|
+
* a build that had a patch this one does not).
|
|
10
|
+
*/
|
|
11
|
+
export declare function toUserManifest(user: UserPatch): PatchManifest | null;
|
|
12
|
+
/**
|
|
13
|
+
* The built-in catalog with user presets merged in as USR-bank entries.
|
|
14
|
+
*
|
|
15
|
+
* Every consumer must use this rather than the raw `catalog`: looking a
|
|
16
|
+
* selected id up in the raw catalog silently misses user presets, which is how
|
|
17
|
+
* picking one from the library used to do nothing at all.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useFullCatalog(): PatchManifest[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type AudioEngine, type EngineOptions } from '../audio/AudioEngine';
|
|
2
|
+
import type { Bank, PatchManifest } from '../patches/types';
|
|
3
|
+
export type LatencyInfo = ReturnType<AudioEngine['latencyInfo']>;
|
|
4
|
+
export interface SoundEngineOptions {
|
|
5
|
+
/** Default true. When false, loads never mark patches as cached for offline (embed). */
|
|
6
|
+
trackOffline?: boolean;
|
|
7
|
+
/** Forwarded to getEngine(); only takes effect when it creates the engine. */
|
|
8
|
+
engineOptions?: EngineOptions;
|
|
9
|
+
/**
|
|
10
|
+
* Default false. When true, unmount silences and suspends the engine, and a
|
|
11
|
+
* re-run after power-on resumes it.
|
|
12
|
+
*/
|
|
13
|
+
suspendOnUnmount?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SoundEngineApi {
|
|
16
|
+
engine: AudioEngine;
|
|
17
|
+
fullCatalog: PatchManifest[];
|
|
18
|
+
displayPatch: PatchManifest | undefined;
|
|
19
|
+
displayIndex: number;
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
activeBank: Bank;
|
|
22
|
+
currentBankPatches: PatchManifest[];
|
|
23
|
+
needsGesture: boolean;
|
|
24
|
+
ensureAudio(): Promise<void>;
|
|
25
|
+
selectPatch(p: PatchManifest): void;
|
|
26
|
+
selectPatchId(id: string): void;
|
|
27
|
+
previewPatchById(id: string): void;
|
|
28
|
+
downloadPatchById(id: string): void;
|
|
29
|
+
handleStep(delta: number): void;
|
|
30
|
+
handleBankSelect(bank: Bank): void;
|
|
31
|
+
noteOn(midi: number, vel: number): void;
|
|
32
|
+
noteOff(midi: number): void;
|
|
33
|
+
uiNoteOn(midi: number, vel: number): void;
|
|
34
|
+
panic(): void;
|
|
35
|
+
getLatency(): LatencyInfo;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The engine, patch selection and note wiring shared by the standalone app and
|
|
39
|
+
* the embed. Owns the load callbacks, the power-on gesture and MIDI input.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useSoundEngine(options?: SoundEngineOptions): SoundEngineApi;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { EffectId } from '../audio/effects';
|
|
2
|
+
import type { ChainEffectId } from '../audio/effects';
|
|
3
|
+
import type { PatchSourceTag } from '../audio/AudioEngine';
|
|
4
|
+
import type { PatchManifest, UserPatch } from '../patches/types';
|
|
5
|
+
import { type MidiPresetId, type MidiProfile } from '../input/midiProfile';
|
|
6
|
+
import type { BreathConfig } from '../input/breath';
|
|
7
|
+
/**
|
|
8
|
+
* Web MIDI access, as three user-visible outcomes plus the pre-request states:
|
|
9
|
+
* - `unsupported`: no requestMIDIAccess (iOS, Firefox without the add-on…)
|
|
10
|
+
* - `idle`: supported, not requested yet (we ask on demand)
|
|
11
|
+
* - `pending`: prompt showing
|
|
12
|
+
* - `denied`: SecurityError — recoverable only via Chrome's site settings
|
|
13
|
+
* - `granted`: access held (devices may or may not be plugged in)
|
|
14
|
+
* - `error`: any other failure
|
|
15
|
+
*/
|
|
16
|
+
export type MidiAccessStatus = 'unsupported' | 'idle' | 'pending' | 'denied' | 'granted' | 'error';
|
|
17
|
+
export interface MidiDeviceInfo {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
manufacturer: string;
|
|
21
|
+
}
|
|
22
|
+
interface State {
|
|
23
|
+
catalog: PatchManifest[];
|
|
24
|
+
currentPatchId: string;
|
|
25
|
+
/**
|
|
26
|
+
* The patch the user last asked for. Set the instant a selection happens and
|
|
27
|
+
* cleared when the engine either commits it (currentPatchId) or fails, so the
|
|
28
|
+
* display never claims a patch that is not actually loaded.
|
|
29
|
+
*/
|
|
30
|
+
pendingPatchId: string | null;
|
|
31
|
+
loadError: {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
} | null;
|
|
35
|
+
loadingPatchId: string | null;
|
|
36
|
+
loadingProgress: {
|
|
37
|
+
loaded: number;
|
|
38
|
+
total: number;
|
|
39
|
+
} | null;
|
|
40
|
+
currentSource: PatchSourceTag | null;
|
|
41
|
+
octave: number;
|
|
42
|
+
velocity: number;
|
|
43
|
+
fxEnabled: Record<EffectId, boolean>;
|
|
44
|
+
fxParams: Record<EffectId, Record<string, number>>;
|
|
45
|
+
/** Order of the audio effects in the chain. Doublers are note-level, so absent. */
|
|
46
|
+
fxOrder: ChainEffectId[];
|
|
47
|
+
favourites: Set<string>;
|
|
48
|
+
downloaded: Set<string>;
|
|
49
|
+
downloadingPatchId: string | null;
|
|
50
|
+
warmProgress: {
|
|
51
|
+
done: number;
|
|
52
|
+
total: number;
|
|
53
|
+
} | null;
|
|
54
|
+
userPatches: UserPatch[];
|
|
55
|
+
saveDialogOpen: boolean;
|
|
56
|
+
pickerOpen: boolean;
|
|
57
|
+
midiConnected: boolean;
|
|
58
|
+
midiActivity: boolean;
|
|
59
|
+
midiStatus: MidiAccessStatus;
|
|
60
|
+
midiDevices: MidiDeviceInfo[];
|
|
61
|
+
midiProfile: MidiProfile;
|
|
62
|
+
midiPanelOpen: boolean;
|
|
63
|
+
/** Learn mode: the next swept CC on any input becomes the breath CC. */
|
|
64
|
+
midiLearning: boolean;
|
|
65
|
+
setMidiStatus: (s: MidiAccessStatus) => void;
|
|
66
|
+
setMidiDevices: (d: MidiDeviceInfo[]) => void;
|
|
67
|
+
setMidiProfile: (p: MidiProfile) => void;
|
|
68
|
+
applyMidiPreset: (id: MidiPresetId) => void;
|
|
69
|
+
updateBreath: (patch: Partial<BreathConfig>) => void;
|
|
70
|
+
setBreathEnabled: (v: boolean) => void;
|
|
71
|
+
setMidiPanelOpen: (open: boolean) => void;
|
|
72
|
+
setMidiLearning: (v: boolean) => void;
|
|
73
|
+
debugMode: boolean;
|
|
74
|
+
online: boolean;
|
|
75
|
+
keyboardMode: boolean;
|
|
76
|
+
fxVisible: boolean;
|
|
77
|
+
previewPatchId: string | null;
|
|
78
|
+
setPickerOpen: (open: boolean) => void;
|
|
79
|
+
setPendingPatchId: (id: string | null) => void;
|
|
80
|
+
setLoadError: (e: {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
} | null) => void;
|
|
84
|
+
setKeyboardMode: (v: boolean) => void;
|
|
85
|
+
toggleKeyboardMode: () => void;
|
|
86
|
+
setFxVisible: (v: boolean) => void;
|
|
87
|
+
toggleFxVisible: () => void;
|
|
88
|
+
setPreviewPatchId: (id: string | null) => void;
|
|
89
|
+
setLoadingPatchId: (id: string | null) => void;
|
|
90
|
+
setLoadingProgress: (p: {
|
|
91
|
+
loaded: number;
|
|
92
|
+
total: number;
|
|
93
|
+
} | null) => void;
|
|
94
|
+
setCurrentSource: (s: PatchSourceTag | null) => void;
|
|
95
|
+
setCurrentPatchId: (id: string) => void;
|
|
96
|
+
markDownloaded: (id: string) => void;
|
|
97
|
+
unmarkDownloaded: (id: string) => void;
|
|
98
|
+
setDownloadingPatchId: (id: string | null) => void;
|
|
99
|
+
setWarmProgress: (p: {
|
|
100
|
+
done: number;
|
|
101
|
+
total: number;
|
|
102
|
+
} | null) => void;
|
|
103
|
+
setOctave: (n: number) => void;
|
|
104
|
+
shiftOctave: (delta: number) => void;
|
|
105
|
+
setVelocity: (n: number) => void;
|
|
106
|
+
shiftVelocity: (delta: number) => void;
|
|
107
|
+
toggleFxEnabled: (id: EffectId) => void;
|
|
108
|
+
setFxParam: (id: EffectId, paramId: string, value: number) => void;
|
|
109
|
+
setFxOrder: (order: ChainEffectId[]) => void;
|
|
110
|
+
moveFx: (id: ChainEffectId, delta: number) => void;
|
|
111
|
+
toggleFavourite: (id: string) => void;
|
|
112
|
+
setMidiConnected: (b: boolean) => void;
|
|
113
|
+
flashMidiActivity: () => void;
|
|
114
|
+
setSaveDialogOpen: (open: boolean) => void;
|
|
115
|
+
saveCurrentAsUserPatch: (name: string) => string | null;
|
|
116
|
+
deleteUserPatch: (id: string) => void;
|
|
117
|
+
setDebugMode: (v: boolean) => void;
|
|
118
|
+
toggleDebugMode: () => void;
|
|
119
|
+
setOnline: (v: boolean) => void;
|
|
120
|
+
}
|
|
121
|
+
export type PersistScope = 'app' | 'embed';
|
|
122
|
+
/** 'embed' persists only patch, octave and velocity; 'app' (default) also persists fx state. */
|
|
123
|
+
export declare function setPersistScope(scope: PersistScope): void;
|
|
124
|
+
export declare const useStore: import("zustand").UseBoundStore<import("zustand").StoreApi<State>>;
|
|
125
|
+
/**
|
|
126
|
+
* App-only startup: loads downloaded patches, user patches and debug mode from
|
|
127
|
+
* localStorage and installs online/offline listeners. Idempotent. Kept out of
|
|
128
|
+
* module load so the store can be imported by the embed without side effects.
|
|
129
|
+
*/
|
|
130
|
+
export declare function hydrateAppState(): void;
|
|
131
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pepperhorn/sound-module",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Embeddable React sound module: a sampled-instrument rack with patch browser, on-screen piano, computer keyboard and Web MIDI input.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/types/embed/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/types/embed/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./styles.css": "./dist/styles.css",
|
|
16
|
+
"./fonts.css": "./dist/fonts.css",
|
|
17
|
+
"./patches/*": "./dist/patches/*",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"*.css"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"prepublishOnly": "node scripts/verify-dist.mjs"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
33
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@fontsource/chakra-petch": "^5.3.0",
|
|
37
|
+
"@fontsource/inter": "^5.1.0",
|
|
38
|
+
"@fontsource/jetbrains-mono": "^5.1.0",
|
|
39
|
+
"@fontsource/michroma": "^5.3.0",
|
|
40
|
+
"smplr": "^0.20.0",
|
|
41
|
+
"zustand": "^5.0.12"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|