Optimise
All checks were successful
Check & deploy / build (pull_request) Successful in 1m51s

This commit is contained in:
Andras Schmelczer 2026-05-21 20:33:49 +01:00
commit ed5a4379db
76 changed files with 1418 additions and 988 deletions

View file

@ -22,12 +22,22 @@ export interface GardenAudioVibeProfile extends GardenAudioVibeSettings {
progression: Array<GardenAudioChord>;
}
export const defaultGardenAudioVibeSettings: GardenAudioVibeSettings = {
idleIntensity: 0.08,
bpm: 74,
rampUpIntensity: 0.85,
rampUpTime: 0.08,
noteLength: 0.42,
notePitchOffset: 0,
brightness: 1,
};
export const createGardenAudioConfig = () => ({
masterVolume: DEFAULT_AUDIO_VOLUME,
fadeInSeconds: 0.45,
updateRampSeconds: 0.08,
delay: {
timeSeconds: 0.46,
timeSeconds: 0.405,
feedback: 0.12,
wetGain: 0.044,
erasingActivity: 0.12,
@ -43,9 +53,9 @@ export const createGardenAudioConfig = () => ({
maxVoices: 24,
gain: 0.48,
sustainSeconds: 0.42,
sustainLevel: 0.32,
releaseSeconds: 0.24,
lowpassHz: 7600,
sustainLevel: 0.26,
releaseSeconds: 0.34,
lowpassHz: 7000,
gainAttackSeconds: 0.006,
lowpassMaxHz: 12000,
lowpassMinHz: 1400,
@ -53,8 +63,8 @@ export const createGardenAudioConfig = () => ({
sustainVelocityRange: 0.55,
},
rhythm: {
idleIntensity: 0.08,
bpm: 74,
idleIntensity: defaultGardenAudioVibeSettings.idleIntensity,
bpm: defaultGardenAudioVibeSettings.bpm,
stepsPerBeat: 4,
stepsPerBar: 16,
sparseActivity: 0.055,
@ -69,9 +79,7 @@ export const createGardenAudioConfig = () => ({
pianoActivity: 0,
},
energy: {
attackSeconds: 0.08,
decaySeconds: 0.9,
immediateActivityScale: 0.85,
releaseSeconds: 1.15,
strokeDecaySeconds: 0.32,
},

View file

@ -13,7 +13,10 @@ export const getStrokeMetrics = (stroke: GardenAudioStroke): GardenAudioStrokeMe
const dy = stroke.to[1] - stroke.from[1];
const distancePixels = Math.hypot(dx, dy);
const elapsedSeconds = Math.max(minElapsedSeconds, stroke.elapsedSeconds ?? 0);
const normalizationPixels = Math.max(1, Math.min(stroke.canvasSize[0], stroke.canvasSize[1]));
const normalizationPixels = Math.max(
1,
Math.min(stroke.canvasSize[0], stroke.canvasSize[1])
);
const normalizedDistance = distancePixels / normalizationPixels;
return {

View file

@ -260,11 +260,10 @@ export class GardenAudio {
}
public stroke(stroke: GardenAudioStroke): void {
if (this.lifecycle === 'destroyed' || this.isMuted) {
if (this.lifecycle !== 'started' || this.isMuted) {
return;
}
this.start(stroke.vibe);
const context = this.graph.context;
if (!context) {
return;

View file

@ -207,8 +207,8 @@ export const generativePianoTuning: GenerativePianoTuning = {
},
{
midiMin: 62,
midiMax: 81,
preferredMidi: 72,
midiMax: 78,
preferredMidi: 70,
pan: 0.18,
scaleDegrees: [2, 3, 4, 6],
},
@ -254,10 +254,10 @@ export const generativePianoTuning: GenerativePianoTuning = {
expressionMultiplier: 0.9,
},
padChord: {
velocities: [0.052, 0.041, 0.033],
expressionVelocityWeight: 0.02,
velocities: [0.046, 0.036, 0.029],
expressionVelocityWeight: 0.018,
delaySend: 0.008,
lowpassExpressionWeight: 0.28,
lowpassExpressionWeight: 0.24,
},
supportNote: {
velocityBase: 0.105,
@ -316,7 +316,7 @@ export const generativePianoTuning: GenerativePianoTuning = {
brushStream: {
inferredManiaThreshold: 0.82,
inferredManiaRange: 0.18,
registerManiaShift: 0.45,
registerManiaShift: 0.3,
chordToneEverySteps: 4,
durationBaseSeconds: 0.48,
durationIntensitySeconds: 0.08,
@ -329,22 +329,22 @@ export const generativePianoTuning: GenerativePianoTuning = {
delaySendMin: 0.006,
delaySendMax: 0.032,
velocityBase: 0.1,
velocityIntensityWeight: 0.13,
velocityIntensityWeight: 0.1,
lowpassBaseExpression: 0.39,
lowpassIntensityWeight: 0.48,
lowpassManiaWeight: 0.18,
intenseThreshold: 0.62,
intenseThreshold: 0.68,
activeThreshold: 0.34,
},
brushStreamEcho: {
maniaThreshold: 0.86,
stepModulo: 2,
maniaThreshold: 0.92,
stepModulo: 3,
stepRemainder: 1,
intensityThreshold: 0.95,
octaveSemitones: 12,
maxMidi: 88,
velocityBase: 0.045,
velocityIntensityWeight: 0.05,
maxMidi: 84,
velocityBase: 0.035,
velocityIntensityWeight: 0.04,
durationMinSeconds: 0.11,
durationScale: 0.68,
panScale: -0.75,
@ -361,7 +361,7 @@ export const generativePianoTuning: GenerativePianoTuning = {
lowOffset: -1,
},
registerBias: {
maniaShiftSemitones: 4,
maniaShiftSemitones: 2,
midiMin: 36,
midiMaxForMin: 86,
minimumSpan: 4,
@ -375,7 +375,7 @@ export const generativePianoTuning: GenerativePianoTuning = {
lowpass: {
midiBase: 48,
midiRange: 33,
midiLiftHz: 720,
midiLiftHz: 500,
expressionBase: 0.58,
expressionWeight: 0.32,
},
@ -396,16 +396,16 @@ export const generativePianoTuning: GenerativePianoTuning = {
strokeAccentMinSteps: 12,
strokeAccentThreshold: 0.58,
maxBrushPhraseLayers: 3,
maxBrushStreamNotesPerBar: 9,
maxBrushStreamNotesPerBar: 7,
brushLayerBaseSeconds: 5.5,
brushLayerEnergySeconds: 2.5,
brushLayerMinIntensity: 0.12,
brushStreamIdleIntervalBeats: 2,
brushStreamActiveIntervalBeats: 1,
brushStreamIntenseIntervalBeats: 0.5,
brushStreamIntenseIntervalBeats: 0.75,
brushMotifMaxSteps: 8,
brushMotifCanonDelaySeconds: 0.055,
padDurationBarScale: 0.46,
padDurationBarScale: 0.82,
};
export const styleVoices: [

View file

@ -196,7 +196,8 @@ export class GenerativePianoEngine {
if (
this.isWaitingForGestureAccent &&
now - this.lastGestureAccentAt >= generativePianoTuning.gestureAccentMinIntervalSeconds
now - this.lastGestureAccentAt >=
generativePianoTuning.gestureAccentMinIntervalSeconds
) {
this.recordTouchDown({
vibe,
@ -554,7 +555,9 @@ export class GenerativePianoEngine {
const chordIntervals = getChordIntervals(chord, false);
const degrees = this.rotate(
pool.scaleDegrees,
Math.round(strength * generativePianoTuning.gestureAccent.rotationStrengthMultiplier)
Math.round(
strength * generativePianoTuning.gestureAccent.rotationStrengthMultiplier
)
);
const midi = this.chooseMidi(
@ -705,7 +708,9 @@ export class GenerativePianoEngine {
);
layer.motifOffsets.push(this.getMotifOffset(strength));
if (layer.motifOffsets.length > generativePianoTuning.brushMotifMaxSteps) {
layer.motifOffsets = layer.motifOffsets.slice(-generativePianoTuning.brushMotifMaxSteps);
layer.motifOffsets = layer.motifOffsets.slice(
-generativePianoTuning.brushMotifMaxSteps
);
}
}
@ -789,7 +794,9 @@ export class GenerativePianoEngine {
const chordIntervals = getChordIntervals(chord, false);
const rootMidi = profile.rootMidi + chord.rootOffset;
const useChordTone =
this.brushStreamNoteIndex % generativePianoTuning.brushStream.chordToneEverySteps === 0;
this.brushStreamNoteIndex %
generativePianoTuning.brushStream.chordToneEverySteps ===
0;
const source = useChordTone
? {
baseMidi: rootMidi,
@ -897,7 +904,8 @@ export class GenerativePianoEngine {
layer.energy *
this.getBrushPhraseFade(layer, startTime) *
(generativePianoTuning.brushPhrase.layerIntensityBase +
layer.maniaAmount * generativePianoTuning.brushPhrase.layerIntensityManiaWeight),
layer.maniaAmount *
generativePianoTuning.brushPhrase.layerIntensityManiaWeight),
}));
const dominant = layerStates.reduce<{
layer: BrushPhraseLayer;
@ -1073,7 +1081,8 @@ export class GenerativePianoEngine {
}
return (
barIndex % generativePianoTuning.supportBarSpacing === generativePianoTuning.supportBarOffset
barIndex % generativePianoTuning.supportBarSpacing ===
generativePianoTuning.supportBarOffset
);
}
@ -1136,7 +1145,8 @@ export class GenerativePianoEngine {
): number {
const midiLift =
clamp01(
(midi - generativePianoTuning.lowpass.midiBase) / generativePianoTuning.lowpass.midiRange
(midi - generativePianoTuning.lowpass.midiBase) /
generativePianoTuning.lowpass.midiRange
) * generativePianoTuning.lowpass.midiLiftHz;
return clamp(
this.config.piano.lowpassHz *

View file

@ -7,8 +7,6 @@ import { getLoadedPianoSamples, loadPianoSamples } from './piano-samples';
export const PIANO_SCHEDULE_AHEAD_SECONDS = 0.002;
type PianoLoadState = 'idle' | 'loading' | 'loaded';
interface ActivePianoVoice {
gain: GainNode;
source: AudioScheduledSourceNode;
@ -27,7 +25,6 @@ const pianoSamplerTuning = {
};
export class PianoSampler {
private loadState: PianoLoadState = 'idle';
private samples: Array<LoadedPianoSample> = [];
private activeVoices: Array<ActivePianoVoice> = [];
@ -37,27 +34,19 @@ export class PianoSampler {
) {}
public load(context: BaseAudioContext): Promise<void> {
if (this.loadState === 'loaded') {
if (this.samples.length > 0) {
return Promise.resolve();
}
const loadedSamples = getLoadedPianoSamples();
if (loadedSamples) {
this.setSamples(loadedSamples);
this.loadState = 'loaded';
return Promise.resolve();
}
this.loadState = 'loading';
return loadPianoSamples(context)
.then((samples) => {
this.setSamples(samples);
this.loadState = 'loaded';
})
.catch((error) => {
this.loadState = 'idle';
throw error;
});
return loadPianoSamples(context).then((samples) => {
this.setSamples(samples);
});
}
public play({
@ -154,7 +143,6 @@ export class PianoSampler {
}
public reset(): void {
this.loadState = 'idle';
this.samples = [];
this.activeVoices = [];
}