Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app/studio/src/audio/AudioImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion packages/app/studio/src/project/SampleImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/app/studio/src/project/SampleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions packages/docs/docs-dev/architecture/audio-decoding.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions packages/docs/docs-user/workflows/sample-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>Delete</kbd> 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.
12 changes: 11 additions & 1 deletion packages/lib/dsp/src/fragmentor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ppqn> {
let index = Math.ceil(p0 / stepSize)
let position = index * stepSize
Expand All @@ -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 }> {
Expand Down
92 changes: 81 additions & 11 deletions packages/lib/dsp/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions packages/studio/core/src/samples/MainThreadSampleLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ export class MainThreadSampleLoader implements SampleLoader {
* If loading has not yet completed the promise resolves once data becomes
* available.
*
* @param zip archive instance to receive the files
* @returns resolves once the files have been added
* @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<void> {
const exec: Exec = async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/studio/core/src/samples/SampleProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface SampleProvider {
*
* @param uuid identifier of the requested sample
* @param progress callback receiving download progress between 0 and 1
* @returns audio data and metadata for the sample
* @returns Tuple containing decoded audio and metadata for the sample.
*/
fetch(uuid: UUID.Format, progress: Progress.Handler): Promise<[AudioData, SampleMetaData]>
}
33 changes: 17 additions & 16 deletions packages/studio/core/src/samples/SampleStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export namespace SampleStorage {
/**
* Write decoded audio, peaks and metadata to OPFS.
*
* @param uuid identifier of the sample to store
* @param audio decoded audio frames
* @param peaks precomputed peak data
* @param meta additional sample metadata
* @returns resolves when the files have been written
* @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,
Expand All @@ -42,9 +42,9 @@ 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
* @returns resolves once the metadata file was written
* @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<void> => {
const path = `${Folder}/${UUID.toString(uuid)}`
Expand All @@ -54,9 +54,9 @@ 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 audio data, peak information and metadata
* @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)}`
Expand All @@ -75,19 +75,20 @@ 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 delete
* @returns resolves when the files have been removed
* @param uuid Identifier of the sample to remove from OPFS.
*/
export const remove = async (uuid: UUID.Format): Promise<void> => {
const path = `${Folder}/${UUID.toString(uuid)}`
return WorkerAgents.Opfs.delete(`${path}`)
}

/** List metadata for all stored samples.
/**
* List metadata for all stored samples.
*
* @returns metadata for each sample stored in OPFS
* @returns Array containing metadata for each stored sample.
*/
export const list = async (): Promise<ReadonlyArray<Sample>> => {
return WorkerAgents.Opfs.list(Folder)
Expand Down