Clean up
This commit is contained in:
parent
ea0304356f
commit
7c70f15e49
65 changed files with 1122 additions and 1906 deletions
|
|
@ -5,17 +5,7 @@ import type {
|
|||
GardenAudioConfig,
|
||||
GardenAudioVibeProfile,
|
||||
} from './garden-audio-config';
|
||||
import {
|
||||
degreeToSemitone,
|
||||
getChordIntervals,
|
||||
getVibeProfile,
|
||||
PITCH_SEMITONES_PER_OCTAVE,
|
||||
} from './garden-audio-music';
|
||||
import {
|
||||
GENERATIVE_LOOKAHEAD_SECONDS,
|
||||
GENERATIVE_START_DELAY_SECONDS,
|
||||
PIANO_SCHEDULE_AHEAD_SECONDS,
|
||||
} from './garden-audio-scheduling';
|
||||
import { getVibeProfile, PITCH_SEMITONES_PER_OCTAVE } from './garden-audio-music';
|
||||
import type { PianoNote } from './garden-audio-types';
|
||||
import {
|
||||
generativePianoTuning,
|
||||
|
|
@ -23,6 +13,39 @@ import {
|
|||
type GardenAudioRegister,
|
||||
type GardenAudioStylePool,
|
||||
} from './generative-piano-tuning';
|
||||
import { PIANO_SCHEDULE_AHEAD_SECONDS } from './piano-sampler';
|
||||
|
||||
const GENERATIVE_LOOKAHEAD_SECONDS = 0.3;
|
||||
const GENERATIVE_START_DELAY_SECONDS = 0.02;
|
||||
|
||||
const chordVoicings = {
|
||||
majorOpen: [0, 7, 12, 16],
|
||||
minorOpen: [0, 7, 12, 15],
|
||||
majorClosed: [0, 4, 7, 12, 16],
|
||||
minorClosed: [0, 3, 7, 12, 15],
|
||||
};
|
||||
|
||||
const getChordIntervals = (
|
||||
chord: GardenAudioChord,
|
||||
openVoicing: boolean
|
||||
): Array<number> => {
|
||||
if (openVoicing) {
|
||||
return chord.quality === 'major' ? chordVoicings.majorOpen : chordVoicings.minorOpen;
|
||||
}
|
||||
return chord.quality === 'major'
|
||||
? chordVoicings.majorClosed
|
||||
: chordVoicings.minorClosed;
|
||||
};
|
||||
|
||||
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);
|
||||
return profile.scale[scaleIndex] + octave * PITCH_SEMITONES_PER_OCTAVE;
|
||||
};
|
||||
|
||||
type GardenAudioStyleIndex = 0 | 1 | 2;
|
||||
|
||||
|
|
@ -124,6 +147,23 @@ export class GenerativePianoEngine {
|
|||
this.isWaitingForGestureAccent = false;
|
||||
}
|
||||
|
||||
public release(vibe: VibePreset, now: number): number {
|
||||
this.prime(now);
|
||||
this.isWaitingForGestureAccent = false;
|
||||
|
||||
const profile = getVibeProfile(vibe);
|
||||
const releaseStep = this.getReleaseResolutionStep(now);
|
||||
const releaseStart = Math.max(now, this.getTimeForStep(releaseStep));
|
||||
|
||||
this.playReleaseResolution(profile, releaseStep, releaseStart);
|
||||
this.nextBeatStep = null;
|
||||
this.nextBrushStreamStep = null;
|
||||
this.brushPhraseLayers = [];
|
||||
this.brushStreamNoteCountsByBar.clear();
|
||||
|
||||
return releaseStart + this.generation.releaseResolution.fadeAfterSeconds;
|
||||
}
|
||||
|
||||
private recordTouchDown({
|
||||
vibe,
|
||||
now,
|
||||
|
|
@ -234,47 +274,27 @@ export class GenerativePianoEngine {
|
|||
const chord = this.getChord(profile, 0);
|
||||
const intervals = getChordIntervals(chord, true);
|
||||
const rootMidi = profile.rootMidi + chord.rootOffset;
|
||||
const notes = [
|
||||
{
|
||||
midi: this.chooseMidi(
|
||||
{ baseMidi: rootMidi, offsets: [0] },
|
||||
this.generation.padRegisters[0]
|
||||
),
|
||||
velocity: this.generation.vibeChangeStinger.velocities[0],
|
||||
pan: this.generation.vibeChangeStinger.pans[0],
|
||||
delaySend: this.generation.vibeChangeStinger.delaySends[0],
|
||||
},
|
||||
{
|
||||
midi: this.chooseMidi(
|
||||
{ baseMidi: rootMidi, offsets: [intervals[1], intervals[2]] },
|
||||
this.generation.padRegisters[1]
|
||||
),
|
||||
velocity: this.generation.vibeChangeStinger.velocities[1],
|
||||
pan: this.generation.vibeChangeStinger.pans[1],
|
||||
delaySend: this.generation.vibeChangeStinger.delaySends[1],
|
||||
},
|
||||
{
|
||||
midi: this.chooseMidi(
|
||||
{ baseMidi: rootMidi, offsets: [intervals[3], intervals[2]] },
|
||||
this.generation.padRegisters[2]
|
||||
),
|
||||
velocity: this.generation.vibeChangeStinger.velocities[2],
|
||||
pan: this.generation.vibeChangeStinger.pans[2],
|
||||
delaySend: this.generation.vibeChangeStinger.delaySends[2],
|
||||
},
|
||||
const stinger = this.generation.vibeChangeStinger;
|
||||
const offsetsByVoice: ReadonlyArray<ReadonlyArray<number>> = [
|
||||
[0],
|
||||
[intervals[1], intervals[2]],
|
||||
[intervals[3], intervals[2]],
|
||||
];
|
||||
|
||||
notes.forEach((note, index) => {
|
||||
offsetsByVoice.forEach((offsets, index) => {
|
||||
const midi = this.chooseMidi(
|
||||
{ baseMidi: rootMidi, offsets },
|
||||
this.generation.padRegisters[index]
|
||||
);
|
||||
this.playNote({
|
||||
...note,
|
||||
durationSeconds: this.generation.stingerDurationSeconds,
|
||||
midi,
|
||||
velocity: stinger.velocities[index],
|
||||
pan: stinger.pans[index],
|
||||
delaySend: stinger.delaySends[index],
|
||||
durationSeconds: stinger.noteDurationSeconds,
|
||||
role: 'stinger',
|
||||
lowpassHz: this.getLowpassHz(
|
||||
profile,
|
||||
note.midi,
|
||||
this.generation.vibeChangeStinger.lowpassExpression
|
||||
),
|
||||
startTime: now + index * this.generation.stingerSpacingSeconds,
|
||||
lowpassHz: this.getLowpassHz(profile, midi, stinger.lowpassExpression),
|
||||
startTime: now + index * stinger.spacingSeconds,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -285,12 +305,8 @@ export class GenerativePianoEngine {
|
|||
this.isWaitingForGestureAccent = false;
|
||||
this.lastGestureAccentAt = Number.NEGATIVE_INFINITY;
|
||||
this.lastStrokeAccentStep = Number.NEGATIVE_INFINITY;
|
||||
this.lastMidiByStyle[0] = null;
|
||||
this.lastMidiByStyle[1] = null;
|
||||
this.lastMidiByStyle[2] = null;
|
||||
this.lastPadMidiByVoice[0] = null;
|
||||
this.lastPadMidiByVoice[1] = null;
|
||||
this.lastPadMidiByVoice[2] = null;
|
||||
this.lastMidiByStyle.fill(null);
|
||||
this.lastPadMidiByVoice.fill(null);
|
||||
this.brushPhraseLayers = [];
|
||||
this.nextBrushStreamStep = null;
|
||||
this.brushStreamNoteIndex = 0;
|
||||
|
|
@ -400,6 +416,37 @@ export class GenerativePianoEngine {
|
|||
});
|
||||
}
|
||||
|
||||
private playReleaseResolution(
|
||||
profile: GardenAudioVibeProfile,
|
||||
stepIndex: number,
|
||||
startTime: number
|
||||
): void {
|
||||
const chord = this.getChord(profile, this.getBarIndexForStep(stepIndex));
|
||||
const intervals = getChordIntervals(chord, true);
|
||||
const rootMidi = profile.rootMidi + chord.rootOffset;
|
||||
const release = this.generation.releaseResolution;
|
||||
const offsetsByVoice: ReadonlyArray<ReadonlyArray<number>> = [
|
||||
[0],
|
||||
[intervals[1], intervals[2]],
|
||||
[intervals[3], intervals[2]],
|
||||
];
|
||||
|
||||
offsetsByVoice.forEach((offsets, index) => {
|
||||
const register = this.generation.padRegisters[index];
|
||||
const midi = this.chooseMidi({ baseMidi: rootMidi, offsets }, register, null, false);
|
||||
this.playNote({
|
||||
midi,
|
||||
velocity: release.velocities[index],
|
||||
startTime: startTime + index * release.strumSeconds,
|
||||
durationSeconds: release.durationSeconds,
|
||||
pan: register.pan,
|
||||
role: 'pad',
|
||||
delaySend: release.delaySend,
|
||||
lowpassHz: this.getLowpassHz(profile, midi, release.lowpassExpression),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private playSupportNote(
|
||||
profile: GardenAudioVibeProfile,
|
||||
barIndex: number,
|
||||
|
|
@ -546,10 +593,6 @@ export class GenerativePianoEngine {
|
|||
}): void {
|
||||
const profile = getVibeProfile(vibe);
|
||||
const pool = this.generation.stylePools[styleIndex];
|
||||
const register = this.getBiasedRegister(
|
||||
pool,
|
||||
this.generation.touchNote.registerBiasManiaAmount
|
||||
);
|
||||
const chord = this.getChord(profile, this.getGlobalBarIndex(now));
|
||||
const chordIntervals = getChordIntervals(chord, false);
|
||||
const rootMidi = profile.rootMidi + chord.rootOffset;
|
||||
|
|
@ -558,7 +601,7 @@ export class GenerativePianoEngine {
|
|||
baseMidi: rootMidi,
|
||||
offsets: this.getSupportOffsets(chordIntervals, styleIndex),
|
||||
},
|
||||
register,
|
||||
pool,
|
||||
this.lastMidiByStyle[styleIndex],
|
||||
true
|
||||
);
|
||||
|
|
@ -877,41 +920,26 @@ export class GenerativePianoEngine {
|
|||
|
||||
private getBrushStreamIntervalSteps(intensity: number): number {
|
||||
const intervalBeats =
|
||||
intensity >= this.generation.brushStream.manicThreshold
|
||||
? this.generation.brushStreamManicIntervalBeats
|
||||
: intensity >= this.generation.brushStream.intenseThreshold
|
||||
? this.generation.brushStreamIntenseIntervalBeats
|
||||
: intensity >= this.generation.brushStream.activeThreshold
|
||||
? this.generation.brushStreamActiveIntervalBeats
|
||||
: this.generation.brushStreamIdleIntervalBeats;
|
||||
intensity >= this.generation.brushStream.intenseThreshold
|
||||
? this.generation.brushStreamIntenseIntervalBeats
|
||||
: intensity >= this.generation.brushStream.activeThreshold
|
||||
? this.generation.brushStreamActiveIntervalBeats
|
||||
: this.generation.brushStreamIdleIntervalBeats;
|
||||
return Math.max(1, Math.round(intervalBeats * this.config.rhythm.stepsPerBeat));
|
||||
}
|
||||
|
||||
private getBrushPhraseFade(layer: BrushPhraseLayer, startTime: number): number {
|
||||
const lifetimeSeconds = layer.expiresAt - layer.startedAt;
|
||||
const lifetimeSeconds = Math.max(0.001, layer.expiresAt - layer.startedAt);
|
||||
const ageSeconds = startTime - layer.startedAt;
|
||||
return clamp01(
|
||||
1 -
|
||||
ageSeconds /
|
||||
Math.max(
|
||||
this.generation.brushPhrase.fadeMinimumLifetimeSeconds,
|
||||
lifetimeSeconds
|
||||
)
|
||||
);
|
||||
return clamp01(1 - ageSeconds / lifetimeSeconds);
|
||||
}
|
||||
|
||||
private getMotifOffset(strength: number): number {
|
||||
const energyStep =
|
||||
strength >= this.generation.brushMotif.highThreshold
|
||||
? this.generation.brushMotif.highOffset
|
||||
: strength >= this.generation.brushMotif.mediumThreshold
|
||||
? this.generation.brushMotif.mediumOffset
|
||||
: this.generation.brushMotif.lowOffset;
|
||||
return clamp(
|
||||
energyStep,
|
||||
this.generation.brushMotif.minOffset,
|
||||
this.generation.brushMotif.maxOffset
|
||||
);
|
||||
return strength >= this.generation.brushMotif.highThreshold
|
||||
? this.generation.brushMotif.highOffset
|
||||
: strength >= this.generation.brushMotif.mediumThreshold
|
||||
? this.generation.brushMotif.mediumOffset
|
||||
: this.generation.brushMotif.lowOffset;
|
||||
}
|
||||
|
||||
private getBrushMotifDegrees({
|
||||
|
|
@ -1185,6 +1213,19 @@ export class GenerativePianoEngine {
|
|||
);
|
||||
}
|
||||
|
||||
private getReleaseResolutionStep(startTime: number): number {
|
||||
const currentStep = this.getStepIndexAtTime(startTime);
|
||||
const nextBeatStep =
|
||||
Math.ceil((currentStep + 1) / this.config.rhythm.stepsPerBeat) *
|
||||
this.config.rhythm.stepsPerBeat;
|
||||
const nextBarStep =
|
||||
Math.ceil((currentStep + 1) / this.config.rhythm.stepsPerBar) *
|
||||
this.config.rhythm.stepsPerBar;
|
||||
const barIsClose = nextBarStep - currentStep <= this.config.rhythm.stepsPerBeat * 2;
|
||||
|
||||
return barIsClose ? nextBarStep : nextBeatStep;
|
||||
}
|
||||
|
||||
private getNextBarTimeAt(startTime: number): number {
|
||||
const nextBarStep =
|
||||
Math.ceil(this.getStepIndexAtTime(startTime) / this.config.rhythm.stepsPerBar) *
|
||||
|
|
|
|||
Loading…
Reference in a new issue