more
This commit is contained in:
parent
2fe3c69963
commit
6bc125be1c
104 changed files with 3084 additions and 2410 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { DEFAULT_AUDIO_VOLUME } from '../app-constants';
|
||||
import { DEFAULT_AUDIO_VOLUME } from '../consts';
|
||||
import type { PianoNoteRole } from './garden-audio-types';
|
||||
|
||||
export interface GardenAudioChord {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export class GardenAudio {
|
|||
private stopPianoAt: number | null = null;
|
||||
private lastEraserAt = Number.NEGATIVE_INFINITY;
|
||||
private lastVibeStingerAt = Number.NEGATIVE_INFINITY;
|
||||
private startRequestId = 0;
|
||||
|
||||
public constructor(private readonly config: GardenAudioConfig) {
|
||||
this.masterVolume = clamp01(config.masterVolume);
|
||||
|
|
@ -56,6 +57,14 @@ export class GardenAudio {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.lifecycle === 'started' &&
|
||||
this.currentVibeId === vibe.id &&
|
||||
this.graph.context?.state === 'running'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = this.graph.ensureContext(isUserGesture);
|
||||
if (!context) {
|
||||
return;
|
||||
|
|
@ -108,25 +117,49 @@ export class GardenAudio {
|
|||
return;
|
||||
}
|
||||
|
||||
const startRequestId = ++this.startRequestId;
|
||||
void this.piano
|
||||
.load(context)
|
||||
.then(() => {
|
||||
if (!this.canCompleteStart(context, startRequestId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activateStart(vibe, context, startupRampSeconds, true);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (this.canCompleteStart(context, startRequestId)) {
|
||||
this.activateStart(vibe, context, startupRampSeconds, false);
|
||||
}
|
||||
ErrorHandler.addException(error, {
|
||||
fallbackMessage: 'Could not load piano samples.',
|
||||
severity: Severity.WARNING,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private canCompleteStart(context: AudioContext, startRequestId: number): boolean {
|
||||
return (
|
||||
this.graph.context === context &&
|
||||
this.lifecycle !== 'destroyed' &&
|
||||
!this.isMuted &&
|
||||
this.startRequestId === startRequestId
|
||||
);
|
||||
}
|
||||
|
||||
private activateStart(
|
||||
vibe: VibePreset,
|
||||
context: AudioContext,
|
||||
startupRampSeconds: number,
|
||||
cuePiano: boolean
|
||||
): void {
|
||||
this.lifecycle = 'started';
|
||||
this.applyVibe(vibe);
|
||||
this.pianoEngine.prime(context.currentTime, getVibeProfile(vibe));
|
||||
this.currentVibeId = vibe.id;
|
||||
this.graph.applyDelayProfile();
|
||||
this.graph.setMasterGain(this.masterVolume, startupRampSeconds);
|
||||
|
||||
const pianoLoad = this.piano.loadIfIdle(context);
|
||||
if (pianoLoad) {
|
||||
void pianoLoad
|
||||
.then(() => {
|
||||
if (this.graph.context === context && this.lifecycle !== 'destroyed') {
|
||||
this.pianoEngine.cue(context.currentTime, getVibeProfile(vibe));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ErrorHandler.addException(error, {
|
||||
fallbackMessage: 'Could not load piano samples. Using synthesized audio.',
|
||||
severity: Severity.WARNING,
|
||||
});
|
||||
});
|
||||
if (cuePiano) {
|
||||
this.pianoEngine.cue(context.currentTime, getVibeProfile(vibe));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,13 +46,6 @@ const degreeToSemitone = (profile: GardenAudioVibeProfile, degree: number): numb
|
|||
|
||||
type GardenAudioStyleIndex = 0 | 1 | 2;
|
||||
|
||||
interface TouchDownRequest {
|
||||
vibe: VibePreset;
|
||||
now: number;
|
||||
strength: number;
|
||||
maniaAmount?: number;
|
||||
}
|
||||
|
||||
interface PitchCandidate {
|
||||
midi: number;
|
||||
preference: number;
|
||||
|
|
@ -152,7 +145,12 @@ export class GenerativePianoEngine {
|
|||
now,
|
||||
strength,
|
||||
maniaAmount = 0,
|
||||
}: TouchDownRequest): void {
|
||||
}: {
|
||||
vibe: VibePreset;
|
||||
now: number;
|
||||
strength: number;
|
||||
maniaAmount?: number;
|
||||
}): void {
|
||||
const normalizedStrength = clamp01(strength);
|
||||
const normalizedManiaAmount = clamp01(maniaAmount);
|
||||
const styleIndex = this.getStyleIndex(now);
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ const pianoSamplerTuning = {
|
|||
minDurationSeconds: 0.08,
|
||||
minFadeSeconds: 0.08,
|
||||
minGain: 0.0001,
|
||||
synthGainScale: 0.34,
|
||||
synthMaxDurationSeconds: 1.8,
|
||||
synthOscillatorType: 'triangle' as OscillatorType,
|
||||
tailStopExtraSeconds: 0.05,
|
||||
voiceStealFadeSeconds: 0.025,
|
||||
voiceStealStopSeconds: 0.05,
|
||||
|
|
@ -39,9 +36,9 @@ export class PianoSampler {
|
|||
private readonly graph: GardenAudioGraph
|
||||
) {}
|
||||
|
||||
public loadIfIdle(context: BaseAudioContext): Promise<void> | null {
|
||||
if (this.loadState !== 'idle') {
|
||||
return null;
|
||||
public load(context: BaseAudioContext): Promise<void> {
|
||||
if (this.loadState === 'loaded') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const loadedSamples = getLoadedPianoSamples();
|
||||
|
|
@ -80,82 +77,32 @@ export class PianoSampler {
|
|||
return;
|
||||
}
|
||||
|
||||
const sample = this.findNearestSample(midi);
|
||||
if (!sample) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduledStart = Math.max(
|
||||
context.currentTime + PIANO_SCHEDULE_AHEAD_SECONDS,
|
||||
startTime
|
||||
);
|
||||
const noteVelocity = clamp01(velocity);
|
||||
const sample = this.findNearestSample(midi);
|
||||
|
||||
if (sample) {
|
||||
const noteGainValue = this.computeNoteGain(noteVelocity);
|
||||
const sustainSeconds =
|
||||
profileSustainSeconds *
|
||||
(this.config.piano.sustainBase +
|
||||
noteVelocity * this.config.piano.sustainVelocityRange);
|
||||
const sustainAt =
|
||||
scheduledStart + Math.max(pianoSamplerTuning.minDurationSeconds, durationSeconds);
|
||||
const releaseAt = sustainAt + sustainSeconds;
|
||||
const stopAt = releaseAt + this.config.piano.releaseSeconds;
|
||||
const source = context.createBufferSource();
|
||||
|
||||
source.buffer = sample.buffer;
|
||||
source.playbackRate.setValueAtTime(
|
||||
Math.pow(2, (midi - sample.midi) / PITCH_SEMITONES_PER_OCTAVE),
|
||||
scheduledStart
|
||||
);
|
||||
|
||||
this.scheduleVoice({
|
||||
source,
|
||||
scheduledStart,
|
||||
stopAt,
|
||||
pan,
|
||||
lowpassHz,
|
||||
delaySend,
|
||||
eventBus,
|
||||
configureGainEnvelope: (gain) => {
|
||||
gain.gain.setValueAtTime(pianoSamplerTuning.minGain, scheduledStart);
|
||||
gain.gain.exponentialRampToValueAtTime(
|
||||
noteGainValue,
|
||||
scheduledStart + this.config.piano.gainAttackSeconds
|
||||
);
|
||||
gain.gain.setTargetAtTime(
|
||||
Math.max(
|
||||
pianoSamplerTuning.minGain,
|
||||
noteGainValue * this.config.piano.sustainLevel
|
||||
),
|
||||
sustainAt,
|
||||
Math.max(
|
||||
pianoSamplerTuning.minFadeSeconds,
|
||||
sustainSeconds * this.config.piano.sustainBase
|
||||
)
|
||||
);
|
||||
gain.gain.setTargetAtTime(
|
||||
pianoSamplerTuning.minGain,
|
||||
releaseAt,
|
||||
this.config.piano.releaseSeconds
|
||||
);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const noteGainValue = this.computeNoteGain(
|
||||
noteVelocity,
|
||||
pianoSamplerTuning.synthGainScale
|
||||
);
|
||||
const releaseAt =
|
||||
scheduledStart +
|
||||
clamp(
|
||||
durationSeconds + profileSustainSeconds * 0.5,
|
||||
pianoSamplerTuning.minDurationSeconds,
|
||||
pianoSamplerTuning.synthMaxDurationSeconds
|
||||
);
|
||||
const noteGainValue = this.computeNoteGain(noteVelocity);
|
||||
const sustainSeconds =
|
||||
profileSustainSeconds *
|
||||
(this.config.piano.sustainBase +
|
||||
noteVelocity * this.config.piano.sustainVelocityRange);
|
||||
const sustainAt =
|
||||
scheduledStart + Math.max(pianoSamplerTuning.minDurationSeconds, durationSeconds);
|
||||
const releaseAt = sustainAt + sustainSeconds;
|
||||
const stopAt = releaseAt + this.config.piano.releaseSeconds;
|
||||
const source = context.createOscillator();
|
||||
const source = context.createBufferSource();
|
||||
|
||||
source.type = pianoSamplerTuning.synthOscillatorType;
|
||||
source.frequency.setValueAtTime(getMidiFrequency(midi), scheduledStart);
|
||||
source.buffer = sample.buffer;
|
||||
source.playbackRate.setValueAtTime(
|
||||
Math.pow(2, (midi - sample.midi) / PITCH_SEMITONES_PER_OCTAVE),
|
||||
scheduledStart
|
||||
);
|
||||
|
||||
this.scheduleVoice({
|
||||
source,
|
||||
|
|
@ -171,6 +118,17 @@ export class PianoSampler {
|
|||
noteGainValue,
|
||||
scheduledStart + this.config.piano.gainAttackSeconds
|
||||
);
|
||||
gain.gain.setTargetAtTime(
|
||||
Math.max(
|
||||
pianoSamplerTuning.minGain,
|
||||
noteGainValue * this.config.piano.sustainLevel
|
||||
),
|
||||
sustainAt,
|
||||
Math.max(
|
||||
pianoSamplerTuning.minFadeSeconds,
|
||||
sustainSeconds * this.config.piano.sustainBase
|
||||
)
|
||||
);
|
||||
gain.gain.setTargetAtTime(
|
||||
pianoSamplerTuning.minGain,
|
||||
releaseAt,
|
||||
|
|
@ -312,6 +270,3 @@ export class PianoSampler {
|
|||
this.samples = samples.slice().sort((a, b) => a.midi - b.midi);
|
||||
}
|
||||
}
|
||||
|
||||
const getMidiFrequency = (midi: number): number =>
|
||||
440 * Math.pow(2, (midi - 69) / PITCH_SEMITONES_PER_OCTAVE);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,38 @@
|
|||
import type { LoadedPianoSample } from './garden-audio-types';
|
||||
import a0SampleUrl from './samples/A0v12.m4a?url&no-inline';
|
||||
import a1SampleUrl from './samples/A1v12.m4a?url&no-inline';
|
||||
import a2SampleUrl from './samples/A2v12.m4a?url&no-inline';
|
||||
import a3SampleUrl from './samples/A3v12.m4a?url&no-inline';
|
||||
import a4SampleUrl from './samples/A4v12.m4a?url&no-inline';
|
||||
import a5SampleUrl from './samples/A5v12.m4a?url&no-inline';
|
||||
import a6SampleUrl from './samples/A6v12.m4a?url&no-inline';
|
||||
import a7SampleUrl from './samples/A7v12.m4a?url&no-inline';
|
||||
import c1SampleUrl from './samples/C1v12.m4a?url&no-inline';
|
||||
import c2SampleUrl from './samples/C2v12.m4a?url&no-inline';
|
||||
import c3SampleUrl from './samples/C3v12.m4a?url&no-inline';
|
||||
import c4SampleUrl from './samples/C4v12.m4a?url&no-inline';
|
||||
import c5SampleUrl from './samples/C5v12.m4a?url&no-inline';
|
||||
import c6SampleUrl from './samples/C6v12.m4a?url&no-inline';
|
||||
import c7SampleUrl from './samples/C7v12.m4a?url&no-inline';
|
||||
import c8SampleUrl from './samples/C8v12.m4a?url&no-inline';
|
||||
import dSharp1SampleUrl from './samples/Dsharp1v12.m4a?url&no-inline';
|
||||
import dSharp2SampleUrl from './samples/Dsharp2v12.m4a?url&no-inline';
|
||||
import dSharp3SampleUrl from './samples/Dsharp3v12.m4a?url&no-inline';
|
||||
import dSharp4SampleUrl from './samples/Dsharp4v12.m4a?url&no-inline';
|
||||
import dSharp5SampleUrl from './samples/Dsharp5v12.m4a?url&no-inline';
|
||||
import dSharp6SampleUrl from './samples/Dsharp6v12.m4a?url&no-inline';
|
||||
import dSharp7SampleUrl from './samples/Dsharp7v12.m4a?url&no-inline';
|
||||
import fSharp1SampleUrl from './samples/Fsharp1v12.m4a?url&no-inline';
|
||||
import fSharp2SampleUrl from './samples/Fsharp2v12.m4a?url&no-inline';
|
||||
import fSharp3SampleUrl from './samples/Fsharp3v12.m4a?url&no-inline';
|
||||
import fSharp4SampleUrl from './samples/Fsharp4v12.m4a?url&no-inline';
|
||||
import fSharp5SampleUrl from './samples/Fsharp5v12.m4a?url&no-inline';
|
||||
import fSharp6SampleUrl from './samples/Fsharp6v12.m4a?url&no-inline';
|
||||
import fSharp7SampleUrl from './samples/Fsharp7v12.m4a?url&no-inline';
|
||||
|
||||
interface PianoSampleDefinition {
|
||||
midi: number;
|
||||
path: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
|
|
@ -10,48 +41,39 @@ export interface PianoSampleLoadProgress {
|
|||
totalCount: number;
|
||||
}
|
||||
|
||||
const sampleFiles: Array<[fileName: string, midi: number]> = [
|
||||
['A0v12.m4a', 21],
|
||||
['C1v12.m4a', 24],
|
||||
['Dsharp1v12.m4a', 27],
|
||||
['Fsharp1v12.m4a', 30],
|
||||
['A1v12.m4a', 33],
|
||||
['C2v12.m4a', 36],
|
||||
['Dsharp2v12.m4a', 39],
|
||||
['Fsharp2v12.m4a', 42],
|
||||
['A2v12.m4a', 45],
|
||||
['C3v12.m4a', 48],
|
||||
['Dsharp3v12.m4a', 51],
|
||||
['Fsharp3v12.m4a', 54],
|
||||
['A3v12.m4a', 57],
|
||||
['C4v12.m4a', 60],
|
||||
['Dsharp4v12.m4a', 63],
|
||||
['Fsharp4v12.m4a', 66],
|
||||
['A4v12.m4a', 69],
|
||||
['C5v12.m4a', 72],
|
||||
['Dsharp5v12.m4a', 75],
|
||||
['Fsharp5v12.m4a', 78],
|
||||
['A5v12.m4a', 81],
|
||||
['C6v12.m4a', 84],
|
||||
['Dsharp6v12.m4a', 87],
|
||||
['Fsharp6v12.m4a', 90],
|
||||
['A6v12.m4a', 93],
|
||||
['C7v12.m4a', 96],
|
||||
['Dsharp7v12.m4a', 99],
|
||||
['Fsharp7v12.m4a', 102],
|
||||
['A7v12.m4a', 105],
|
||||
['C8v12.m4a', 108],
|
||||
const pianoSampleDefinitions: Array<PianoSampleDefinition> = [
|
||||
{ url: a0SampleUrl, path: './samples/A0v12.m4a', midi: 21 },
|
||||
{ url: c1SampleUrl, path: './samples/C1v12.m4a', midi: 24 },
|
||||
{ url: dSharp1SampleUrl, path: './samples/Dsharp1v12.m4a', midi: 27 },
|
||||
{ url: fSharp1SampleUrl, path: './samples/Fsharp1v12.m4a', midi: 30 },
|
||||
{ url: a1SampleUrl, path: './samples/A1v12.m4a', midi: 33 },
|
||||
{ url: c2SampleUrl, path: './samples/C2v12.m4a', midi: 36 },
|
||||
{ url: dSharp2SampleUrl, path: './samples/Dsharp2v12.m4a', midi: 39 },
|
||||
{ url: fSharp2SampleUrl, path: './samples/Fsharp2v12.m4a', midi: 42 },
|
||||
{ url: a2SampleUrl, path: './samples/A2v12.m4a', midi: 45 },
|
||||
{ url: c3SampleUrl, path: './samples/C3v12.m4a', midi: 48 },
|
||||
{ url: dSharp3SampleUrl, path: './samples/Dsharp3v12.m4a', midi: 51 },
|
||||
{ url: fSharp3SampleUrl, path: './samples/Fsharp3v12.m4a', midi: 54 },
|
||||
{ url: a3SampleUrl, path: './samples/A3v12.m4a', midi: 57 },
|
||||
{ url: c4SampleUrl, path: './samples/C4v12.m4a', midi: 60 },
|
||||
{ url: dSharp4SampleUrl, path: './samples/Dsharp4v12.m4a', midi: 63 },
|
||||
{ url: fSharp4SampleUrl, path: './samples/Fsharp4v12.m4a', midi: 66 },
|
||||
{ url: a4SampleUrl, path: './samples/A4v12.m4a', midi: 69 },
|
||||
{ url: c5SampleUrl, path: './samples/C5v12.m4a', midi: 72 },
|
||||
{ url: dSharp5SampleUrl, path: './samples/Dsharp5v12.m4a', midi: 75 },
|
||||
{ url: fSharp5SampleUrl, path: './samples/Fsharp5v12.m4a', midi: 78 },
|
||||
{ url: a5SampleUrl, path: './samples/A5v12.m4a', midi: 81 },
|
||||
{ url: c6SampleUrl, path: './samples/C6v12.m4a', midi: 84 },
|
||||
{ url: dSharp6SampleUrl, path: './samples/Dsharp6v12.m4a', midi: 87 },
|
||||
{ url: fSharp6SampleUrl, path: './samples/Fsharp6v12.m4a', midi: 90 },
|
||||
{ url: a6SampleUrl, path: './samples/A6v12.m4a', midi: 93 },
|
||||
{ url: c7SampleUrl, path: './samples/C7v12.m4a', midi: 96 },
|
||||
{ url: dSharp7SampleUrl, path: './samples/Dsharp7v12.m4a', midi: 99 },
|
||||
{ url: fSharp7SampleUrl, path: './samples/Fsharp7v12.m4a', midi: 102 },
|
||||
{ url: a7SampleUrl, path: './samples/A7v12.m4a', midi: 105 },
|
||||
{ url: c8SampleUrl, path: './samples/C8v12.m4a', midi: 108 },
|
||||
];
|
||||
|
||||
const sampleBaseUrl = `${import.meta.env.BASE_URL}audio/`;
|
||||
|
||||
const pianoSampleDefinitions: Array<PianoSampleDefinition> = sampleFiles
|
||||
.map(([fileName, midi]) => ({
|
||||
midi,
|
||||
url: `${sampleBaseUrl}${fileName}`,
|
||||
}))
|
||||
.sort((a, b) => a.midi - b.midi);
|
||||
|
||||
let loadedPianoSamples: Array<LoadedPianoSample> | null = null;
|
||||
let pianoSampleLoadPromise: Promise<Array<LoadedPianoSample>> | null = null;
|
||||
|
||||
|
|
@ -111,11 +133,11 @@ export const loadPianoSamples = (
|
|||
}
|
||||
).then(
|
||||
(samples) => {
|
||||
loadedPianoSamples = samples
|
||||
.filter((sample): sample is LoadedPianoSample => sample !== null)
|
||||
.sort((a, b) => a.midi - b.midi);
|
||||
if (loadedPianoSamples.length === 0) {
|
||||
throw new Error('Unable to load any piano samples.');
|
||||
loadedPianoSamples = samples.sort((a, b) => a.midi - b.midi);
|
||||
if (loadedPianoSamples.length !== pianoSampleDefinitions.length) {
|
||||
throw new Error(
|
||||
`Loaded ${loadedPianoSamples.length}/${pianoSampleDefinitions.length} piano samples.`
|
||||
);
|
||||
}
|
||||
return [...loadedPianoSamples];
|
||||
},
|
||||
|
|
@ -138,7 +160,7 @@ const loadPianoSample = async (
|
|||
): Promise<LoadedPianoSample> => {
|
||||
const response = await fetch(sample.url, { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load piano sample ${sample.url}`);
|
||||
throw new Error(`Unable to load piano sample ${sample.path}`);
|
||||
}
|
||||
|
||||
const audioData = await response.arrayBuffer();
|
||||
|
|
@ -148,17 +170,13 @@ const loadPianoSample = async (
|
|||
|
||||
const loadPianoSampleBatch = async (
|
||||
samples: Array<PianoSampleDefinition>,
|
||||
loadSample: (
|
||||
sample: PianoSampleDefinition
|
||||
) => Promise<LoadedPianoSample | null>
|
||||
): Promise<Array<LoadedPianoSample | null>> => {
|
||||
const results: Array<LoadedPianoSample | null> = [];
|
||||
loadSample: (sample: PianoSampleDefinition) => Promise<LoadedPianoSample>
|
||||
): Promise<Array<LoadedPianoSample>> => {
|
||||
const results: Array<LoadedPianoSample> = [];
|
||||
|
||||
for (let index = 0; index < samples.length; index += sampleLoadTuning.concurrency) {
|
||||
const batch = samples.slice(index, index + sampleLoadTuning.concurrency);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map((sample) => loadSample(sample).catch(() => null))
|
||||
);
|
||||
const batchResults = await Promise.all(batch.map((sample) => loadSample(sample)));
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
src/audio/samples/A0v12.m4a
Normal file
BIN
src/audio/samples/A0v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/A1v12.m4a
Normal file
BIN
src/audio/samples/A1v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/A2v12.m4a
Normal file
BIN
src/audio/samples/A2v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/A3v12.m4a
Normal file
BIN
src/audio/samples/A3v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/A4v12.m4a
Normal file
BIN
src/audio/samples/A4v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/A5v12.m4a
Normal file
BIN
src/audio/samples/A5v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/A6v12.m4a
Normal file
BIN
src/audio/samples/A6v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/A7v12.m4a
Normal file
BIN
src/audio/samples/A7v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C1v12.m4a
Normal file
BIN
src/audio/samples/C1v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C2v12.m4a
Normal file
BIN
src/audio/samples/C2v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C3v12.m4a
Normal file
BIN
src/audio/samples/C3v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C4v12.m4a
Normal file
BIN
src/audio/samples/C4v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C5v12.m4a
Normal file
BIN
src/audio/samples/C5v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C6v12.m4a
Normal file
BIN
src/audio/samples/C6v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C7v12.m4a
Normal file
BIN
src/audio/samples/C7v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/C8v12.m4a
Normal file
BIN
src/audio/samples/C8v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Dsharp1v12.m4a
Normal file
BIN
src/audio/samples/Dsharp1v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Dsharp2v12.m4a
Normal file
BIN
src/audio/samples/Dsharp2v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Dsharp3v12.m4a
Normal file
BIN
src/audio/samples/Dsharp3v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Dsharp4v12.m4a
Normal file
BIN
src/audio/samples/Dsharp4v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Dsharp5v12.m4a
Normal file
BIN
src/audio/samples/Dsharp5v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Dsharp6v12.m4a
Normal file
BIN
src/audio/samples/Dsharp6v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Dsharp7v12.m4a
Normal file
BIN
src/audio/samples/Dsharp7v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Fsharp1v12.m4a
Normal file
BIN
src/audio/samples/Fsharp1v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Fsharp2v12.m4a
Normal file
BIN
src/audio/samples/Fsharp2v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Fsharp3v12.m4a
Normal file
BIN
src/audio/samples/Fsharp3v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Fsharp4v12.m4a
Normal file
BIN
src/audio/samples/Fsharp4v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Fsharp5v12.m4a
Normal file
BIN
src/audio/samples/Fsharp5v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Fsharp6v12.m4a
Normal file
BIN
src/audio/samples/Fsharp6v12.m4a
Normal file
Binary file not shown.
BIN
src/audio/samples/Fsharp7v12.m4a
Normal file
BIN
src/audio/samples/Fsharp7v12.m4a
Normal file
Binary file not shown.
7
src/audio/samples/README.md
Normal file
7
src/audio/samples/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Piano samples are Salamander Grand Piano V3 samples by Alexander Holm,
|
||||
transcoded from OGG Vorbis to AAC M4A for iOS browser playback and distributed
|
||||
under CC BY 3.0.
|
||||
|
||||
Source package: @audio-samples/piano-velocity12
|
||||
Source recording: https://archive.org/details/SalamanderGrandPianoV3
|
||||
License: https://creativecommons.org/licenses/by/3.0/
|
||||
Loading…
Reference in a new issue