From 9b13827215f506e5829b28b166380d7e187f473c Mon Sep 17 00:00:00 2001 From: RMO Date: Thu, 28 Aug 2025 03:44:13 -0600 Subject: [PATCH] docs: add sample tsdoc and audio decoding docs --- packages/app/studio/src/audio/AudioImport.ts | 1 + .../app/studio/src/project/SampleImporter.ts | 5 +- .../app/studio/src/project/SampleUtils.ts | 1 + .../docs-dev/architecture/audio-decoding.md | 17 ++++ .../docs-user/workflows/sample-management.md | 2 + packages/lib/dsp/src/fragmentor.ts | 12 ++- packages/lib/dsp/src/utils.ts | 92 ++++++++++++++++--- .../src/samples/MainThreadSampleLoader.ts | 5 + .../studio/core/src/samples/SampleProvider.ts | 1 + .../studio/core/src/samples/SampleStorage.ts | 26 +++++- 10 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 packages/docs/docs-dev/architecture/audio-decoding.md diff --git a/packages/app/studio/src/audio/AudioImport.ts b/packages/app/studio/src/audio/AudioImport.ts index 22760c764..fd924b90b 100644 --- a/packages/app/studio/src/audio/AudioImport.ts +++ b/packages/app/studio/src/audio/AudioImport.ts @@ -30,6 +30,7 @@ export namespace AudioImporter { * * @param context Audio context used for decoding. * @param creation Parameters describing the file to import. + * @returns Metadata describing the stored sample. */ export const run = async ( context: AudioContext, diff --git a/packages/app/studio/src/project/SampleImporter.ts b/packages/app/studio/src/project/SampleImporter.ts index c62c11d5d..f090fc33e 100644 --- a/packages/app/studio/src/project/SampleImporter.ts +++ b/packages/app/studio/src/project/SampleImporter.ts @@ -2,7 +2,10 @@ import { Progress, UUID } from "@opendaw/lib-std"; import { Sample } from "@opendaw/studio-adapters"; /** - * Interface for importing audio samples into a project. + * Interface for converting raw audio files into project samples. + * + * Implementations typically decode the provided file, generate peak data and + * persist the result via {@link SampleStorage}. * * @example * ```ts diff --git a/packages/app/studio/src/project/SampleUtils.ts b/packages/app/studio/src/project/SampleUtils.ts index 3e4b56f91..147aefdb6 100644 --- a/packages/app/studio/src/project/SampleUtils.ts +++ b/packages/app/studio/src/project/SampleUtils.ts @@ -24,6 +24,7 @@ export namespace SampleUtils { * @param boxGraph Graph containing the boxes to inspect. * @param importer Utility used to load replacement samples. * @param audioManager Manager responsible for caching sample data. + * @returns Promise that resolves once verification has finished. * * @example * ```ts diff --git a/packages/docs/docs-dev/architecture/audio-decoding.md b/packages/docs/docs-dev/architecture/audio-decoding.md new file mode 100644 index 000000000..7fd07ca55 --- /dev/null +++ b/packages/docs/docs-dev/architecture/audio-decoding.md @@ -0,0 +1,17 @@ +# Audio Decoding + +The studio decodes imported audio files in order to generate waveforms and +make data available to playback components. + +## Pipeline + +1. `AudioImporter.run` receives a file's `ArrayBuffer`, decodes it with the + Web Audio API and estimates metadata such as BPM and duration. +2. Peak information is generated in a worker via `WorkerAgents.Peak.generateAsync`. +3. `SampleStorage.store` persists the decoded audio, peaks and metadata in the + browser's OPFS for later reuse. +4. `MainThreadSampleLoader` retrieves the cached data on demand and exposes + it through a `SampleLoader` interface. + +This workflow keeps heavy decoding work off the UI thread while ensuring +subsequent loads are served from the local cache. diff --git a/packages/docs/docs-user/workflows/sample-management.md b/packages/docs/docs-user/workflows/sample-management.md index ec4e758ca..5f0430ee0 100644 --- a/packages/docs/docs-user/workflows/sample-management.md +++ b/packages/docs/docs-user/workflows/sample-management.md @@ -9,6 +9,8 @@ This guide explains how to organize and edit audio samples within the browser: information. Changes are stored locally. 4. **Delete** – select one or more local samples and press Delete to remove them. Samples in use by a project cannot be deleted while offline. +5. **Replace** – if a project references a missing sample, a dialog lets you choose a + new audio file to take its place. These tools help keep your library tidy and make it easy to audition sounds before bringing them into a project. diff --git a/packages/lib/dsp/src/fragmentor.ts b/packages/lib/dsp/src/fragmentor.ts index b94bacc26..8eb1ad889 100644 --- a/packages/lib/dsp/src/fragmentor.ts +++ b/packages/lib/dsp/src/fragmentor.ts @@ -3,7 +3,14 @@ import {ppqn} from "./ppqn" /** Helpers to iterate over PPQN ranges. */ export class Fragmentor { - /** Iterates positions between two PPQN values using a fixed step size. */ + /** + * Iterates positions between two PPQN values using a fixed step size. + * + * @param p0 Start position in pulses per quarter note. + * @param p1 End position in pulses per quarter note. + * @param stepSize Distance between subsequent positions. + * @returns Generator yielding PPQN positions. + */ static* iterate(p0: ppqn, p1: ppqn, stepSize: ppqn): Generator { let index = Math.ceil(p0 / stepSize) let position = index * stepSize @@ -16,6 +23,9 @@ export class Fragmentor { /** * Iterates positions and their corresponding index within the range. * + * @param p0 Start position in pulses per quarter note. + * @param p1 End position in pulses per quarter note. + * @param stepSize Distance between subsequent positions. * @returns An iterator yielding objects with position and step index. */ static* iterateWithIndex(p0: ppqn, p1: ppqn, stepSize: ppqn): Generator<{ position: ppqn, index: int }> { diff --git a/packages/lib/dsp/src/utils.ts b/packages/lib/dsp/src/utils.ts index 5c75c6389..c820ae215 100644 --- a/packages/lib/dsp/src/utils.ts +++ b/packages/lib/dsp/src/utils.ts @@ -2,32 +2,102 @@ import {int, panic, unitValue} from "@opendaw/lib-std" const LogDb = Math.log(10.0) / 20.0 -/** Converts MIDI note numbers to frequency in hertz. */ +/** + * Converts a MIDI note number to frequency in hertz. + * + * @param note MIDI note to convert (defaults to 60 – middle C). + * @param baseFrequency Reference tuning in hertz (defaults to 440). + * @returns Frequency in hertz for the provided note. + */ export const midiToHz = (note: number = 60.0, baseFrequency: number = 440.0): number => baseFrequency * Math.pow(2.0, (note + 3.0) / 12.0 - 6.0) -/** Converts a frequency to its nearest MIDI note number. */ + +/** + * Converts a frequency to the nearest MIDI note number. + * + * @param hz Frequency in hertz to convert. + * @param baseFrequency Reference tuning in hertz (defaults to 440). + * @returns MIDI note number corresponding to the frequency. + */ export const hzToMidi = (hz: number, baseFrequency: number = 440.0): number => (12.0 * Math.log(hz / baseFrequency) + 69.0 * Math.LN2) / Math.LN2 -/** Converts decibels to linear gain. */ + +/** + * Converts decibels to linear gain. + * + * @param db Value in decibels. + * @returns Linear gain value. + */ export const dbToGain = (db: number): number => Math.exp(db * LogDb) -/** Converts linear gain to decibels. */ + +/** + * Converts linear gain to decibels. + * + * @param gain Linear gain value. + * @returns Gain expressed in decibels. + */ export const gainToDb = (gain: number): number => Math.log(gain) / LogDb -/** Converts MIDI velocity (0–1) to linear gain. */ + +/** + * Converts a MIDI velocity to linear gain. + * + * @param velocity MIDI velocity between 0 and 1. + * @returns Linear gain corresponding to the velocity. + */ export const velocityToGain = (velocity: unitValue): number => dbToGain(20 * Math.log10(velocity)) -/** Calculates BPM from a number of bars and duration in seconds. */ + +/** + * Calculates BPM from a number of bars and duration in seconds. + * + * @param bars Number of bars played. + * @param duration Duration in seconds. + * @returns Beats per minute. + */ export const barsToBpm = (bars: number, duration: number): number => (bars * 240.0) / duration -/** Calculates number of bars played over a duration at given BPM. */ + +/** + * Calculates number of bars played over a duration at a given BPM. + * + * @param bpm Tempo in beats per minute. + * @param duration Duration in seconds. + * @returns Number of bars covered. + */ export const bpmToBars = (bpm: number, duration: number): number => (bpm * duration) / 240.0 -/** Estimates a likely BPM given a duration and maximum allowed tempo. */ + +/** + * Estimates a likely BPM given a duration and maximum allowed tempo. + * + * @param duration Length of the audio region in seconds. + * @param maxBpm Maximum tempo to consider. + * @returns Rounded BPM estimate. + */ export const estimateBpm = (duration: number, maxBpm: number = 180.0): number => { const bpm = barsToBpm(Math.pow(2.0, Math.floor(Math.log(bpmToBars(maxBpm, duration)) / Math.LN2)), duration) return Math.round(bpm * 1000.0) / 1000.0 } -/** Converts a MIDI semitone value to frequency. */ + +/** + * Converts a MIDI semitone value to frequency in hertz. + * + * @param semitones MIDI note number. + * @returns Frequency in hertz. + */ export const semitoneToHz = (semitones: number) => 440 * Math.pow(2.0, (semitones - 69.0) / 12.0) -/** Converts frequency to a MIDI semitone value. */ + +/** + * Converts frequency to a MIDI semitone value. + * + * @param hz Frequency in hertz. + * @returns MIDI note number. + */ export const hzToSemitone = (hz: number) => 69.0 + 12.0 * Math.log2(hz / 440.0) -/** Parses a time signature string (e.g. "4/4"). */ + +/** + * Parses a time signature string (e.g. "4/4"). + * + * @param input Time signature as a string "numerator/denominator". + * @returns Tuple `[numerator, denominator]`. + */ export const parseTimeSignature = (input: string): [int, int] => { const [first, second] = input.split("/") const numerator = parseInt(first, 10) diff --git a/packages/studio/core/src/samples/MainThreadSampleLoader.ts b/packages/studio/core/src/samples/MainThreadSampleLoader.ts index 1ac7be163..caf3bc69b 100644 --- a/packages/studio/core/src/samples/MainThreadSampleLoader.ts +++ b/packages/studio/core/src/samples/MainThreadSampleLoader.ts @@ -92,6 +92,11 @@ export class MainThreadSampleLoader implements SampleLoader { * * If loading has not yet completed the promise resolves once data becomes * available. + * + * @param zip ZIP archive that receives the sample's audio, peaks and + * metadata files. + * @returns Promise that resolves when the files have been written to the + * archive. */ async pipeFilesInto(zip: JSZip): Promise { const exec: Exec = async () => { diff --git a/packages/studio/core/src/samples/SampleProvider.ts b/packages/studio/core/src/samples/SampleProvider.ts index d417ff10f..2ced405f2 100644 --- a/packages/studio/core/src/samples/SampleProvider.ts +++ b/packages/studio/core/src/samples/SampleProvider.ts @@ -10,6 +10,7 @@ export interface SampleProvider { * * @param uuid identifier of the requested sample * @param progress callback receiving download progress between 0 and 1 + * @returns Tuple containing decoded audio and metadata for the sample. */ fetch(uuid: UUID.Format, progress: Progress.Handler): Promise<[AudioData, SampleMetaData]> } diff --git a/packages/studio/core/src/samples/SampleStorage.ts b/packages/studio/core/src/samples/SampleStorage.ts index b371cf053..eed6077f3 100644 --- a/packages/studio/core/src/samples/SampleStorage.ts +++ b/packages/studio/core/src/samples/SampleStorage.ts @@ -16,6 +16,12 @@ export namespace SampleStorage { /** * Write decoded audio, peaks and metadata to OPFS. + * + * @param uuid Identifier of the sample being stored. + * @param audio Decoded audio buffer to persist as `audio.wav`. + * @param peaks Pre‑computed peak data used for waveform rendering. + * @param meta Descriptive information written to `meta.json`. + * @returns Promise that resolves once all files have been written. */ export const store = async (uuid: UUID.Format, audio: AudioData, @@ -35,6 +41,10 @@ export namespace SampleStorage { /** * Overwrite only the metadata file of a stored sample. + * + * @param uuid Identifier of the sample to update. + * @param meta New metadata to persist to disk. + * @returns Promise that resolves when the metadata has been written. */ export const updateMeta = async (uuid: UUID.Format, meta: SampleMetaData): Promise => { const path = `${Folder}/${UUID.toString(uuid)}` @@ -43,6 +53,10 @@ export namespace SampleStorage { /** * Load a sample from OPFS and decode it into {@link AudioData} and peaks. + * + * @param uuid Identifier of the sample to load. + * @param context Audio context used for decoding. + * @returns Tuple containing decoded audio, peaks and metadata. */ export const load = async (uuid: UUID.Format, context: AudioContext): Promise<[AudioData, Peaks, SampleMetaData]> => { const path = `${Folder}/${UUID.toString(uuid)}` @@ -61,13 +75,21 @@ export namespace SampleStorage { }, peaks, meta]) } - /** Delete a sample and all related files. */ + /** + * Delete a sample and all related files. + * + * @param uuid Identifier of the sample to remove from OPFS. + */ export const remove = async (uuid: UUID.Format): Promise => { const path = `${Folder}/${UUID.toString(uuid)}` return WorkerAgents.Opfs.delete(`${path}`) } - /** List metadata for all stored samples. */ + /** + * List metadata for all stored samples. + * + * @returns Array containing metadata for each stored sample. + */ export const list = async (): Promise> => { return WorkerAgents.Opfs.list(Folder) .then(files => Promise.all(files.filter(file => file.kind === "directory")