More clean up
This commit is contained in:
parent
c94ffcc506
commit
f03da42b5e
43 changed files with 818 additions and 1076 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { DEFAULT_AUDIO_VOLUME } from '../app-constants';
|
||||
import type { PianoNoteRole } from './garden-audio-types';
|
||||
|
||||
type GardenAudioChordQuality = 'major' | 'minor';
|
||||
|
|
@ -7,81 +8,106 @@ export interface GardenAudioChord {
|
|||
quality: GardenAudioChordQuality;
|
||||
}
|
||||
|
||||
export interface GardenAudioVibeProfile {
|
||||
export interface GardenAudioVibeSettings {
|
||||
idleIntensity: number;
|
||||
bpm: number;
|
||||
rampUpIntensity: number;
|
||||
rampUpTime: number;
|
||||
noteLength: number;
|
||||
notePitchOffset: number;
|
||||
brightness: number;
|
||||
}
|
||||
|
||||
export interface GardenAudioVibeProfile extends GardenAudioVibeSettings {
|
||||
rootMidi: number;
|
||||
scale: Array<number>;
|
||||
brightness: number;
|
||||
delayTimeMultiplier: number;
|
||||
progression: Array<GardenAudioChord>;
|
||||
}
|
||||
|
||||
export interface GardenAudioConfig {
|
||||
masterVolume: number;
|
||||
fadeInSeconds: number;
|
||||
updateRampSeconds: number;
|
||||
export const createGardenAudioConfig = () => ({
|
||||
masterVolume: DEFAULT_AUDIO_VOLUME,
|
||||
fadeInSeconds: 0.45,
|
||||
updateRampSeconds: 0.08,
|
||||
delay: {
|
||||
timeSeconds: number;
|
||||
feedback: number;
|
||||
wetGain: number;
|
||||
erasingActivity: number;
|
||||
activityFeedbackWeight: number;
|
||||
feedbackMax: number;
|
||||
feedbackMin: number;
|
||||
outputActivityWeight: number;
|
||||
outputBase: number;
|
||||
outputActivityDuck: number;
|
||||
timeRampSeconds: number;
|
||||
};
|
||||
timeSeconds: 0.46,
|
||||
feedback: 0.12,
|
||||
wetGain: 0.044,
|
||||
erasingActivity: 0.12,
|
||||
activityFeedbackWeight: 0.08,
|
||||
feedbackMax: 0.32,
|
||||
feedbackMin: 0.04,
|
||||
outputActivityWeight: 0.5,
|
||||
outputBase: 0.65,
|
||||
outputActivityDuck: 0.28,
|
||||
timeRampSeconds: 0.12,
|
||||
},
|
||||
piano: {
|
||||
maxVoices: number;
|
||||
gain: number;
|
||||
sustainSeconds: number;
|
||||
sustainLevel: number;
|
||||
releaseSeconds: number;
|
||||
lowpassHz: number;
|
||||
gainAttackSeconds: number;
|
||||
lowpassMaxHz: number;
|
||||
lowpassMinHz: number;
|
||||
sustainBase: number;
|
||||
sustainVelocityRange: number;
|
||||
};
|
||||
maxVoices: 24,
|
||||
gain: 0.48,
|
||||
sustainSeconds: 0.42,
|
||||
sustainLevel: 0.32,
|
||||
releaseSeconds: 0.24,
|
||||
lowpassHz: 7600,
|
||||
gainAttackSeconds: 0.006,
|
||||
lowpassMaxHz: 12000,
|
||||
lowpassMinHz: 1400,
|
||||
sustainBase: 0.45,
|
||||
sustainVelocityRange: 0.55,
|
||||
},
|
||||
rhythm: {
|
||||
bpm: number;
|
||||
stepsPerBeat: number;
|
||||
stepsPerBar: number;
|
||||
sparseActivity: number;
|
||||
};
|
||||
idleIntensity: 0.08,
|
||||
bpm: 74,
|
||||
stepsPerBeat: 4,
|
||||
stepsPerBar: 16,
|
||||
sparseActivity: 0.055,
|
||||
},
|
||||
eraser: {
|
||||
minIntervalSeconds: number;
|
||||
noiseGain: number;
|
||||
filterMinHz: number;
|
||||
filterMaxHz: number;
|
||||
durationSeconds: number;
|
||||
pan: number;
|
||||
pianoActivity: number;
|
||||
};
|
||||
minIntervalSeconds: 0.12,
|
||||
noiseGain: 0.028,
|
||||
filterMinHz: 650,
|
||||
filterMaxHz: 3600,
|
||||
durationSeconds: 0.08,
|
||||
pan: 0,
|
||||
pianoActivity: 0,
|
||||
},
|
||||
energy: {
|
||||
attackSeconds: number;
|
||||
decaySeconds: number;
|
||||
immediateActivityScale: number;
|
||||
releaseSeconds: number;
|
||||
strokeDecaySeconds: number;
|
||||
};
|
||||
attackSeconds: 0.08,
|
||||
decaySeconds: 0.9,
|
||||
immediateActivityScale: 0.85,
|
||||
releaseSeconds: 1.15,
|
||||
strokeDecaySeconds: 0.32,
|
||||
},
|
||||
graph: {
|
||||
pianoBusGains: Record<PianoNoteRole, number>;
|
||||
pianoBusActivityDucking: Record<PianoNoteRole, number>;
|
||||
noiseBusGain: number;
|
||||
};
|
||||
pianoBusGains: {
|
||||
pad: 0.86,
|
||||
support: 0.94,
|
||||
texture: 0.88,
|
||||
gesture: 1,
|
||||
brush: 0.9,
|
||||
stinger: 0.92,
|
||||
} satisfies Record<PianoNoteRole, number>,
|
||||
pianoBusActivityDucking: {
|
||||
pad: 0.42,
|
||||
support: 0.18,
|
||||
texture: -0.06,
|
||||
gesture: 0,
|
||||
brush: -0.08,
|
||||
stinger: 0,
|
||||
} satisfies Record<PianoNoteRole, number>,
|
||||
noiseBusGain: 0.72,
|
||||
},
|
||||
input: {
|
||||
fullActivitySpeed: number;
|
||||
activityNoiseFloorSpeed: number;
|
||||
activityCurve: number;
|
||||
activitySoftCeiling: number;
|
||||
activityAttackSeconds: number;
|
||||
activityReleaseSeconds: number;
|
||||
minAudibleDistance: number;
|
||||
manicActivityThreshold: number;
|
||||
manicReleaseThreshold: number;
|
||||
maniaSmoothingSeconds: number;
|
||||
};
|
||||
}
|
||||
fullActivitySpeed: 0.86,
|
||||
activityNoiseFloorSpeed: 0.025,
|
||||
activityCurve: 0.74,
|
||||
activitySoftCeiling: 0.96,
|
||||
activityAttackSeconds: 0.055,
|
||||
activityReleaseSeconds: 0.2,
|
||||
minAudibleDistance: 0.0025,
|
||||
manicActivityThreshold: 0.9,
|
||||
manicReleaseThreshold: 0.76,
|
||||
maniaSmoothingSeconds: 0.12,
|
||||
},
|
||||
});
|
||||
|
||||
export type GardenAudioConfig = ReturnType<typeof createGardenAudioConfig>;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { approach, clamp01 } from '../utils/math';
|
||||
import type { GardenAudioConfig } from './garden-audio-config';
|
||||
import type { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
||||
|
||||
export class GardenAudioEnergy {
|
||||
private isGestureActive = false;
|
||||
|
|
@ -19,13 +19,10 @@ export class GardenAudioEnergy {
|
|||
this.targetEnergy = 0;
|
||||
}
|
||||
|
||||
public recordStroke(strokeEnergy: number): void {
|
||||
public recordStroke(strokeEnergy: number, profile: GardenAudioVibeProfile): void {
|
||||
this.targetEnergy = Math.max(this.targetEnergy, strokeEnergy);
|
||||
if (this.isGestureActive) {
|
||||
this.energy = Math.max(
|
||||
this.energy,
|
||||
strokeEnergy * this.config.energy.immediateActivityScale
|
||||
);
|
||||
this.energy = Math.max(this.energy, strokeEnergy * profile.rampUpIntensity);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +31,7 @@ export class GardenAudioEnergy {
|
|||
this.energy = 0;
|
||||
}
|
||||
|
||||
public update(now: number): void {
|
||||
public update(now: number, profile: GardenAudioVibeProfile): void {
|
||||
if (this.lastEnergyUpdateAt <= 0) {
|
||||
this.lastEnergyUpdateAt = now;
|
||||
return;
|
||||
|
|
@ -51,7 +48,7 @@ export class GardenAudioEnergy {
|
|||
if (!this.isGestureActive) {
|
||||
timeConstant = this.config.energy.releaseSeconds;
|
||||
} else if (target > this.energy) {
|
||||
timeConstant = this.config.energy.attackSeconds;
|
||||
timeConstant = profile.rampUpTime;
|
||||
}
|
||||
this.energy = approach(this.energy, target, elapsedSeconds, timeConstant);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import { clamp } from '../utils/math';
|
||||
import type { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
||||
import type { GardenAudioConfig } from './garden-audio-config';
|
||||
import type { PianoNoteRole } from './garden-audio-types';
|
||||
|
||||
type NavigatorWithAudioSession = Navigator & {
|
||||
audioSession?: { type: 'auto' | 'playback' | 'ambient' | 'transient' | 'transient-solo' | 'play-and-record' };
|
||||
audioSession?: {
|
||||
type:
|
||||
| 'auto'
|
||||
| 'playback'
|
||||
| 'ambient'
|
||||
| 'transient'
|
||||
| 'transient-solo'
|
||||
| 'play-and-record';
|
||||
};
|
||||
};
|
||||
|
||||
const outputHighPassFrequencyHz = 45;
|
||||
|
|
@ -113,19 +121,19 @@ export class GardenAudioGraph {
|
|||
);
|
||||
}
|
||||
|
||||
public applyDelayProfile(profile: GardenAudioVibeProfile): void {
|
||||
public applyDelayProfile(): void {
|
||||
if (!this.context || !this.delayNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.delayNode.delayTime.setTargetAtTime(
|
||||
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
|
||||
this.config.delay.timeSeconds,
|
||||
this.context.currentTime,
|
||||
this.config.delay.timeRampSeconds
|
||||
);
|
||||
}
|
||||
|
||||
public updateDelay(profile: GardenAudioVibeProfile, activity: number): void {
|
||||
public updateDelay(activity: number): void {
|
||||
if (!this.context || !this.delayNode || !this.delayFeedback || !this.delayOutput) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -133,7 +141,7 @@ export class GardenAudioGraph {
|
|||
const now = this.context.currentTime;
|
||||
const normalizedActivity = clamp(activity, 0, 1);
|
||||
this.delayNode.delayTime.setTargetAtTime(
|
||||
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
|
||||
this.config.delay.timeSeconds,
|
||||
now,
|
||||
this.config.delay.timeRampSeconds
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,34 @@
|
|||
import type { VibePreset } from '../vibes';
|
||||
import type { GardenAudioVibeProfile } from './garden-audio-config';
|
||||
import type { GardenAudioChord, GardenAudioVibeProfile } from './garden-audio-config';
|
||||
|
||||
export const PITCH_SEMITONES_PER_OCTAVE = 12;
|
||||
|
||||
export const getVibeProfile = (vibe: VibePreset): GardenAudioVibeProfile => vibe.audio;
|
||||
const DEFAULT_PROGRESSION: ReadonlyArray<GardenAudioChord> = [
|
||||
{ rootOffset: 0, quality: 'major' },
|
||||
{ rootOffset: 9, quality: 'minor' },
|
||||
{ rootOffset: 5, quality: 'major' },
|
||||
{ rootOffset: 7, quality: 'major' },
|
||||
];
|
||||
|
||||
const DEFAULT_ROOT_MIDI = 57;
|
||||
const DEFAULT_SCALE: ReadonlyArray<number> = [0, 2, 4, 7, 9];
|
||||
|
||||
const profileCache = new WeakMap<VibePreset, GardenAudioVibeProfile>();
|
||||
|
||||
export const getVibeProfile = (vibe: VibePreset): GardenAudioVibeProfile => {
|
||||
let profile = profileCache.get(vibe);
|
||||
if (!profile) {
|
||||
profile = {
|
||||
...vibe.audio,
|
||||
rootMidi: DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset,
|
||||
scale: DEFAULT_SCALE as Array<number>,
|
||||
progression: DEFAULT_PROGRESSION as Array<GardenAudioChord>,
|
||||
};
|
||||
profileCache.set(vibe, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
Object.assign(profile, vibe.audio);
|
||||
profile.rootMidi = DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset;
|
||||
return profile;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export interface PianoNote {
|
|||
role?: PianoNoteRole;
|
||||
delaySend?: number;
|
||||
lowpassHz?: number;
|
||||
sustainSeconds?: number;
|
||||
}
|
||||
|
||||
export type PianoNoteRole =
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export class GardenAudio {
|
|||
|
||||
this.lifecycle = 'started';
|
||||
this.applyVibe(vibe);
|
||||
this.pianoEngine.prime(context.currentTime);
|
||||
this.pianoEngine.prime(context.currentTime, getVibeProfile(vibe));
|
||||
this.graph.setMasterGain(this.masterVolume, startupRampSeconds);
|
||||
|
||||
const pianoLoad = this.piano.loadIfIdle(context);
|
||||
|
|
@ -122,7 +122,7 @@ export class GardenAudio {
|
|||
void pianoLoad
|
||||
.then(() => {
|
||||
if (this.graph.context === context && this.lifecycle !== 'destroyed') {
|
||||
this.pianoEngine.cue(context.currentTime);
|
||||
this.pianoEngine.cue(context.currentTime, getVibeProfile(vibe));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
@ -207,7 +207,8 @@ export class GardenAudio {
|
|||
}
|
||||
|
||||
this.applyVibe(snapshot.vibe);
|
||||
this.energy.update(context.currentTime);
|
||||
const profile = getVibeProfile(snapshot.vibe);
|
||||
this.energy.update(context.currentTime, profile);
|
||||
|
||||
if (snapshot.isErasing) {
|
||||
this.energy.silence();
|
||||
|
|
@ -254,7 +255,8 @@ export class GardenAudio {
|
|||
return;
|
||||
}
|
||||
|
||||
this.energy.recordStroke(strokeEnergy);
|
||||
const profile = getVibeProfile(stroke.vibe);
|
||||
this.energy.recordStroke(strokeEnergy, profile);
|
||||
this.pianoEngine.recordStroke({
|
||||
vibe: stroke.vibe,
|
||||
now,
|
||||
|
|
@ -347,11 +349,10 @@ export class GardenAudio {
|
|||
return;
|
||||
}
|
||||
|
||||
const profile = getVibeProfile(snapshot.vibe);
|
||||
const activity = snapshot.isErasing
|
||||
? this.config.delay.erasingActivity
|
||||
: this.energy.getLevel();
|
||||
this.graph.updateDelay(profile, activity);
|
||||
this.graph.updateDelay(activity);
|
||||
}
|
||||
|
||||
private applyVibe(vibe: VibePreset): void {
|
||||
|
|
@ -360,7 +361,8 @@ export class GardenAudio {
|
|||
}
|
||||
|
||||
this.currentVibeId = vibe.id;
|
||||
this.graph.applyDelayProfile(getVibeProfile(vibe));
|
||||
this.pianoEngine.cue(this.graph.context.currentTime);
|
||||
const profile = getVibeProfile(vibe);
|
||||
this.graph.applyDelayProfile();
|
||||
this.pianoEngine.cue(this.graph.context.currentTime, profile);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,7 @@ const getChordIntervals = (
|
|||
: chordVoicings.minorClosed;
|
||||
};
|
||||
|
||||
const degreeToSemitone = (
|
||||
profile: GardenAudioVibeProfile,
|
||||
degree: number
|
||||
): number => {
|
||||
const degreeToSemitone = (profile: GardenAudioVibeProfile, degree: number): number => {
|
||||
const scaleIndex =
|
||||
((degree % profile.scale.length) + profile.scale.length) % profile.scale.length;
|
||||
const octave = Math.floor(degree / profile.scale.length);
|
||||
|
|
@ -96,6 +93,7 @@ interface BrushPhraseLayer {
|
|||
export class GenerativePianoEngine {
|
||||
private nextBeatStep: number | null = null;
|
||||
private timelineStartedAt: number | null = null;
|
||||
private activeProfile: GardenAudioVibeProfile | null = null;
|
||||
private isWaitingForGestureAccent = false;
|
||||
private lastGestureAccentAt = Number.NEGATIVE_INFINITY;
|
||||
private lastStrokeAccentStep = Number.NEGATIVE_INFINITY;
|
||||
|
|
@ -124,13 +122,17 @@ export class GenerativePianoEngine {
|
|||
return generativePianoTuning;
|
||||
}
|
||||
|
||||
public prime(now: number): void {
|
||||
public prime(now: number, profile: GardenAudioVibeProfile): void {
|
||||
this.activeProfile = profile;
|
||||
this.timelineStartedAt ??= now;
|
||||
this.nextBeatStep ??= 0;
|
||||
this.nextBrushStreamStep ??= 0;
|
||||
}
|
||||
|
||||
public cue(now: number): void {
|
||||
public cue(now: number, profile?: GardenAudioVibeProfile): void {
|
||||
if (profile) {
|
||||
this.activeProfile = profile;
|
||||
}
|
||||
this.nextBeatStep = 0;
|
||||
this.timelineStartedAt = now;
|
||||
this.nextBrushStreamStep = 0;
|
||||
|
|
@ -148,10 +150,9 @@ export class GenerativePianoEngine {
|
|||
}
|
||||
|
||||
public release(vibe: VibePreset, now: number): number {
|
||||
this.prime(now);
|
||||
this.isWaitingForGestureAccent = false;
|
||||
|
||||
const profile = getVibeProfile(vibe);
|
||||
this.prime(now, profile);
|
||||
this.isWaitingForGestureAccent = false;
|
||||
const releaseStep = this.getReleaseResolutionStep(now);
|
||||
const releaseStart = Math.max(now, this.getTimeForStep(releaseStep));
|
||||
|
||||
|
|
@ -198,7 +199,8 @@ export class GenerativePianoEngine {
|
|||
activity,
|
||||
maniaAmount = 0,
|
||||
}: StrokeAccentRequest): void {
|
||||
this.prime(now);
|
||||
const profile = getVibeProfile(vibe);
|
||||
this.prime(now, profile);
|
||||
const strength = clamp01(activity);
|
||||
const normalizedManiaAmount = clamp01(maniaAmount);
|
||||
const styleIndex = this.getStyleIndex(now);
|
||||
|
|
@ -242,14 +244,14 @@ export class GenerativePianoEngine {
|
|||
activity,
|
||||
lookaheadSeconds = GENERATIVE_LOOKAHEAD_SECONDS,
|
||||
}: RenderLookaheadRequest): void {
|
||||
this.prime(now);
|
||||
const profile = getVibeProfile(vibe);
|
||||
this.prime(now, profile);
|
||||
this.skipLateBeats(now);
|
||||
|
||||
if (this.nextBeatStep === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = getVibeProfile(vibe);
|
||||
const lookaheadEnd = now + lookaheadSeconds;
|
||||
while (this.getTimeForStep(this.nextBeatStep) <= lookaheadEnd) {
|
||||
const beatIndex = this.getBeatIndexForStep(this.nextBeatStep);
|
||||
|
|
@ -286,7 +288,7 @@ export class GenerativePianoEngine {
|
|||
{ baseMidi: rootMidi, offsets },
|
||||
this.generation.padRegisters[index]
|
||||
);
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity: stinger.velocities[index],
|
||||
pan: stinger.pans[index],
|
||||
|
|
@ -302,6 +304,7 @@ export class GenerativePianoEngine {
|
|||
public reset(): void {
|
||||
this.nextBeatStep = null;
|
||||
this.timelineStartedAt = null;
|
||||
this.activeProfile = null;
|
||||
this.isWaitingForGestureAccent = false;
|
||||
this.lastGestureAccentAt = Number.NEGATIVE_INFINITY;
|
||||
this.lastStrokeAccentStep = Number.NEGATIVE_INFINITY;
|
||||
|
|
@ -314,6 +317,13 @@ export class GenerativePianoEngine {
|
|||
this.brushStreamNoteCountsByBar.clear();
|
||||
}
|
||||
|
||||
private playProfileNote(profile: GardenAudioVibeProfile, note: PianoNote): void {
|
||||
this.playNote({
|
||||
...note,
|
||||
sustainSeconds: profile.noteLength,
|
||||
});
|
||||
}
|
||||
|
||||
private renderBeat({
|
||||
profile,
|
||||
beatIndex,
|
||||
|
|
@ -398,7 +408,7 @@ export class GenerativePianoEngine {
|
|||
false
|
||||
);
|
||||
this.lastPadMidiByVoice[index] = midi;
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity:
|
||||
velocity + expression * this.generation.padChord.expressionVelocityWeight,
|
||||
|
|
@ -433,8 +443,13 @@ export class GenerativePianoEngine {
|
|||
|
||||
offsetsByVoice.forEach((offsets, index) => {
|
||||
const register = this.generation.padRegisters[index];
|
||||
const midi = this.chooseMidi({ baseMidi: rootMidi, offsets }, register, null, false);
|
||||
this.playNote({
|
||||
const midi = this.chooseMidi(
|
||||
{ baseMidi: rootMidi, offsets },
|
||||
register,
|
||||
null,
|
||||
false
|
||||
);
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity: release.velocities[index],
|
||||
startTime: startTime + index * release.strumSeconds,
|
||||
|
|
@ -470,7 +485,7 @@ export class GenerativePianoEngine {
|
|||
);
|
||||
|
||||
this.lastMidiByStyle[styleIndex] = midi;
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity:
|
||||
(this.generation.supportNote.velocityBase +
|
||||
|
|
@ -516,7 +531,7 @@ export class GenerativePianoEngine {
|
|||
);
|
||||
|
||||
this.lastMidiByStyle[styleIndex] = midi;
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity:
|
||||
(this.generation.textureNote.velocityBase +
|
||||
|
|
@ -563,7 +578,7 @@ export class GenerativePianoEngine {
|
|||
);
|
||||
|
||||
this.lastMidiByStyle[styleIndex] = midi;
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity:
|
||||
(this.generation.gestureAccent.velocityBase +
|
||||
|
|
@ -608,7 +623,7 @@ export class GenerativePianoEngine {
|
|||
|
||||
this.lastMidiByStyle[styleIndex] = midi;
|
||||
this.lastBrushStreamMidi = midi;
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity:
|
||||
(this.generation.touchNote.velocityBase +
|
||||
|
|
@ -818,7 +833,7 @@ export class GenerativePianoEngine {
|
|||
|
||||
this.lastBrushStreamMidi = midi;
|
||||
this.lastMidiByStyle[styleIndex] = midi;
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi,
|
||||
velocity:
|
||||
(this.generation.brushStream.velocityBase +
|
||||
|
|
@ -851,7 +866,7 @@ export class GenerativePianoEngine {
|
|||
this.generation.brushStreamEcho.maxMidi
|
||||
? midi + this.generation.brushStreamEcho.octaveSemitones
|
||||
: midi - this.generation.brushStreamEcho.octaveSemitones;
|
||||
this.playNote({
|
||||
this.playProfileNote(profile, {
|
||||
midi: echoMidi,
|
||||
velocity:
|
||||
(this.generation.brushStreamEcho.velocityBase +
|
||||
|
|
@ -1159,14 +1174,18 @@ export class GenerativePianoEngine {
|
|||
}
|
||||
|
||||
private getExpression(activity: number): number {
|
||||
const liftedActivity = Math.max(
|
||||
activity,
|
||||
this.activeProfile?.idleIntensity ?? this.config.rhythm.idleIntensity
|
||||
);
|
||||
return clamp01(
|
||||
(activity - this.config.rhythm.sparseActivity) /
|
||||
(liftedActivity - this.config.rhythm.sparseActivity) /
|
||||
(1 - this.config.rhythm.sparseActivity)
|
||||
);
|
||||
}
|
||||
|
||||
private getBeatDurationSeconds(): number {
|
||||
return 60 / this.config.rhythm.bpm;
|
||||
return 60 / (this.activeProfile?.bpm ?? this.config.rhythm.bpm);
|
||||
}
|
||||
|
||||
private getStepDurationSeconds(): number {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export class PianoSampler {
|
|||
role,
|
||||
delaySend = 0,
|
||||
lowpassHz = this.config.piano.lowpassHz,
|
||||
sustainSeconds: profileSustainSeconds = this.config.piano.sustainSeconds,
|
||||
}: PianoNote): void {
|
||||
const { context } = this.graph;
|
||||
const eventBus = this.graph.getPianoBus(role);
|
||||
|
|
@ -92,12 +93,11 @@ export class PianoSampler {
|
|||
this.config.piano.gain * noteVelocity
|
||||
);
|
||||
const sustainSeconds =
|
||||
this.config.piano.sustainSeconds *
|
||||
profileSustainSeconds *
|
||||
(this.config.piano.sustainBase +
|
||||
noteVelocity * this.config.piano.sustainVelocityRange);
|
||||
const sustainAt =
|
||||
scheduledStart +
|
||||
Math.max(pianoSamplerTuning.minDurationSeconds, durationSeconds);
|
||||
scheduledStart + Math.max(pianoSamplerTuning.minDurationSeconds, durationSeconds);
|
||||
const releaseAt = sustainAt + sustainSeconds;
|
||||
const stopAt = releaseAt + this.config.piano.releaseSeconds;
|
||||
const source = context.createBufferSource();
|
||||
|
|
@ -150,7 +150,7 @@ export class PianoSampler {
|
|||
const releaseAt =
|
||||
scheduledStart +
|
||||
clamp(
|
||||
durationSeconds + this.config.piano.sustainSeconds * 0.5,
|
||||
durationSeconds + profileSustainSeconds * 0.5,
|
||||
pianoSamplerTuning.minDurationSeconds,
|
||||
pianoSamplerTuning.synthMaxDurationSeconds
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue