diff --git a/packages/app/studio/src/ui/header/TimeStateDisplay.tsx b/packages/app/studio/src/ui/header/TimeStateDisplay.tsx
index ab734cea8..63a5e5e6c 100644
--- a/packages/app/studio/src/ui/header/TimeStateDisplay.tsx
+++ b/packages/app/studio/src/ui/header/TimeStateDisplay.tsx
@@ -29,14 +29,22 @@ import { Propagation } from "@opendaw/lib-box";
const className = Html.adoptStyleSheet(css, "TimeStateDisplay");
-type Construct = {
+/** Parameters for constructing {@link TimeStateDisplay}. */
+export type Construct = {
+ /** Lifecycle managing subscriptions. */
lifecycle: Lifecycle;
+ /** Service providing access to engine and project. */
service: StudioService;
};
const minBpm = 30.0;
const maxBpm = 1000.0;
+/**
+ * Displays transport position, tempo and meter.
+ *
+ * @public
+ */
export const TimeStateDisplay = ({ lifecycle, service }: Construct) => {
const barDigits = Inject.value("001");
const beatDigit = Inject.value("1");
diff --git a/packages/app/studio/src/ui/timeline/SnapSelector.tsx b/packages/app/studio/src/ui/timeline/SnapSelector.tsx
index 7f2b25cbe..3d0155b70 100644
--- a/packages/app/studio/src/ui/timeline/SnapSelector.tsx
+++ b/packages/app/studio/src/ui/timeline/SnapSelector.tsx
@@ -10,8 +10,11 @@ import {Colors} from "@opendaw/studio-core"
const className = Html.adoptStyleSheet(css, "SnapSelector")
-type Construct = {
+/** Parameters for constructing {@link SnapSelector}. */
+export type Construct = {
+ /** Lifecycle managing subscriptions. */
lifecycle: Lifecycle
+ /** Snap settings that provide available options. */
snapping: Snapping
}
@@ -22,6 +25,8 @@ type Construct = {
* ```tsx
*
* ```
+ *
+ * @public
*/
export const SnapSelector = ({lifecycle, snapping}: Construct) => {
const snappingName = Inject.value(snapping.unit.name)
@@ -35,4 +40,4 @@ export const SnapSelector = ({lifecycle, snapping}: Construct) => {
)
-}
\ No newline at end of file
+}
diff --git a/packages/app/studio/src/ui/timeline/TimeAxis.tsx b/packages/app/studio/src/ui/timeline/TimeAxis.tsx
index 262419034..c290052d9 100644
--- a/packages/app/studio/src/ui/timeline/TimeAxis.tsx
+++ b/packages/app/studio/src/ui/timeline/TimeAxis.tsx
@@ -21,7 +21,7 @@ const MIN_TRACK_DURATION = 8 * PPQN.Bar
const MAX_TRACK_DURATION = 1024 * PPQN.Bar
/** Parameters for constructing {@link TimeAxis}. */
-type Construct = {
+export type Construct = {
/** Lifecycle managing subscriptions. */
lifecycle: Lifecycle
/** Service providing access to engine and project. */
@@ -34,7 +34,11 @@ type Construct = {
mapper?: TimeAxisCursorMapper
}
-/** Maps playback position to a different pulse value when drawing the cursor. */
+/**
+ * Maps playback position to a different pulse value when drawing the cursor.
+ *
+ * @public
+ */
export interface TimeAxisCursorMapper {
mapPlaybackCursor(position: ppqn): ppqn
}
@@ -47,6 +51,8 @@ export interface TimeAxisCursorMapper {
* @param snapping - Snap settings for cursor interactions.
* @param range - Range object describing current viewport.
* @param mapper - Optional mapper for custom cursor rendering.
+ *
+ * @public
*/
export const TimeAxis = ({lifecycle, service, snapping, range, mapper}: Construct) => {
let endMarkerPosition: Nullable = null
@@ -165,4 +171,4 @@ export const TimeAxis = ({lifecycle, service, snapping, range, mapper}: Construc
{cursorElement}
)
-}
\ No newline at end of file
+}
diff --git a/packages/app/studio/src/ui/timeline/TimeGrid.ts b/packages/app/studio/src/ui/timeline/TimeGrid.ts
index 9d7f45d09..099bae94c 100644
--- a/packages/app/studio/src/ui/timeline/TimeGrid.ts
+++ b/packages/app/studio/src/ui/timeline/TimeGrid.ts
@@ -8,10 +8,15 @@ import {TimelineRange} from "@/ui/timeline/TimelineRange.ts"
/** Utilities for iterating over timeline grid divisions. */
export namespace TimeGrid {
+ /**
+ * Time signature expressed as `[nominator, denominator]`.
+ */
export type Signature = [int, int]
/** Optional parameters for {@link fragment}. */
export type Options = { minLength?: number }
+ /** Information about a single grid fragment. */
export type Fragment = { bars: int, beats: int, ticks: int, isBar: boolean, isBeat: boolean, pulse: number }
+ /** Callback invoked for each grid fragment. */
export type Designer = (fragment: Fragment) => void
/**
@@ -49,4 +54,4 @@ export namespace TimeGrid {
designer({bars, beats, ticks, isBar, isBeat, pulse})
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/docs/docs-dev/architecture/tempo.md b/packages/docs/docs-dev/architecture/tempo.md
new file mode 100644
index 000000000..c78863fd6
--- /dev/null
+++ b/packages/docs/docs-dev/architecture/tempo.md
@@ -0,0 +1,15 @@
+# Tempo
+
+openDAW expresses musical time in pulses per quarter note (PPQN). A bar in 4/4 time contains 3,840 pulses, providing a stable grid for sequencing and rendering.
+
+## Conversions
+
+```ts
+import {PPQN} from "@opendaw/lib-dsp";
+
+const pulses = PPQN.secondsToPulses(1.5, 120);
+const seconds = PPQN.pulsesToSeconds(PPQN.Bar, 90);
+```
+
+The timeline renders this grid via `TimeGrid` and `TimeAxis`, while `BPMTools.detect` can estimate the tempo of audio buffers.
+
diff --git a/packages/docs/docs-user/features/timeline.md b/packages/docs/docs-user/features/timeline.md
index 4aadf47f6..7bfd6160a 100644
--- a/packages/docs/docs-user/features/timeline.md
+++ b/packages/docs/docs-user/features/timeline.md
@@ -28,6 +28,19 @@ Drag handles on the navigation bar to resize the visible range. Double-click
the end marker on the time axis to set the project length. Scroll the mouse
wheel over the time axis to zoom and pan.
+## Tempo
+
+The transport header shows the current BPM and time signature. Double‑click
+either value to enter a new tempo or meter. The timeline grid adapts
+immediately to these changes.
+
+```ts
+import {PPQN} from "@opendaw/lib-dsp";
+
+// Seconds per bar at 128 BPM
+const barSeconds = PPQN.pulsesToSeconds(PPQN.Bar, 128);
+```
+
## Example Integration
```ts
diff --git a/packages/lib/dsp/src/bpm-tools.ts b/packages/lib/dsp/src/bpm-tools.ts
index 3f3fe6ecb..ddab7b524 100644
--- a/packages/lib/dsp/src/bpm-tools.ts
+++ b/packages/lib/dsp/src/bpm-tools.ts
@@ -11,8 +11,10 @@ export namespace BPMTools {
/**
* Options controlling the tempo search space and analysis parameters.
* All fields are optional and fall back to sensible defaults.
+ *
+ * @public
*/
- type Options = Partial<{
+ export type Options = Partial<{
/** Number of samples between energy taps. */
interval: number
/** Number of coarse scan steps across the search interval. */
@@ -35,6 +37,7 @@ export namespace BPMTools {
* @param sampleRate - Sampling rate of `buf`.
* @param options - Configuration such as search bounds.
* @returns Detected BPM.
+ * @public
*/
export function detect(buf: Float32Array, sampleRate: number, options: Options = {}): number {
const {
diff --git a/packages/lib/dsp/src/fractions.ts b/packages/lib/dsp/src/fractions.ts
index b18d68e2b..ac544500a 100644
--- a/packages/lib/dsp/src/fractions.ts
+++ b/packages/lib/dsp/src/fractions.ts
@@ -4,6 +4,7 @@ import {PPQN, ppqn} from "./ppqn"
/** Tuple representing a musical fraction `n/d`. */
export type Fraction = Readonly<[int, int]>
+/** Helpers for working with musical fractions. */
export namespace Fraction {
/** Creates a builder for accumulating fractions. */
export const builder = () => new Builder()
@@ -12,16 +13,21 @@ export namespace Fraction {
/** Converts a time fraction to {@link ppqn} pulses. */
export const toPPQN = ([n, d]: Fraction): ppqn => PPQN.fromSignature(n, d)
+ /** Mutable accumulator for composing fraction sets. */
class Builder {
readonly #list: Array = []
+ /** Adds a fraction to the collection. */
add(fraction: Fraction): this {
this.#list.push(fraction)
return this
}
+ /** Returns fractions in insertion order. */
asArray(): ReadonlyArray {return this.#list}
+ /** Returns fractions sorted ascending by ratio. */
asAscendingArray(): ReadonlyArray {return this.#list.toSorted((a, b) => toDouble(a) - toDouble(b))}
+ /** Returns fractions sorted descending by ratio. */
asDescendingArray(): ReadonlyArray {return this.#list.toSorted((a, b) => toDouble(b) - toDouble(a))}
}
-}
\ No newline at end of file
+}
diff --git a/packages/lib/dsp/src/ppqn.ts b/packages/lib/dsp/src/ppqn.ts
index 1cb897ffa..af58aef19 100644
--- a/packages/lib/dsp/src/ppqn.ts
+++ b/packages/lib/dsp/src/ppqn.ts
@@ -6,10 +6,26 @@ import {int} from "@opendaw/lib-std"
/** Musical position expressed in pulses per quarter note. */
export type ppqn = number
+/** Pulses contained in one quarter note. */
const Quarter = 960 as const
+/** Pulses contained in a 4/4 bar. */
const Bar = Quarter << 2 // 3_840
+/** Pulses contained in a semiquaver. */
const SemiQuaver = Quarter >>> 2 // 240
+/**
+ * Calculates pulses for a time signature.
+ *
+ * @param nominator - Beats per bar.
+ * @param denominator - Note value representing one beat.
+ */
const fromSignature = (nominator: int, denominator: int) => Math.floor(Bar / denominator) * nominator
+/**
+ * Breaks a pulse value down into musical parts.
+ *
+ * @param ppqn - Pulse count to decompose.
+ * @param nominator - Beats per bar.
+ * @param denominator - Note value representing one beat.
+ */
const toParts = (ppqn: ppqn, nominator: int = 4, denominator: int = 4) => {
const lowerPulses = fromSignature(1, denominator)
const beats = Math.floor(ppqn / lowerPulses)
@@ -26,9 +42,13 @@ const toParts = (ppqn: ppqn, nominator: int = 4, denominator: int = 4) => {
} as const
}
+/** Converts seconds to pulses for a given tempo. */
const secondsToPulses = (seconds: number, bpm: number): ppqn => seconds * bpm / 60.0 * Quarter
+/** Converts pulses to seconds for a given tempo. */
const pulsesToSeconds = (pulses: ppqn, bpm: number): number => (pulses * 60.0 / Quarter) / bpm
+/** Converts sample counts to pulses. */
const samplesToPulses = (samples: number, bpm: number, sampleRate: number): ppqn => secondsToPulses(samples / sampleRate, bpm)
+/** Converts pulses to sample counts. */
const pulsesToSamples = (pulses: ppqn, bpm: number, sampleRate: number): number => pulsesToSeconds(pulses, bpm) * sampleRate
/** Utility conversions for {@link ppqn} timing. */
@@ -46,4 +66,4 @@ export const PPQN = {
const {bars, beats, semiquavers, ticks} = toParts(pulses | 0, nominator, denominator)
return `${bars + 1}.${beats + 1}.${semiquavers + 1}:${ticks}`
}
-} as const
\ No newline at end of file
+} as const
diff --git a/packages/lib/std/src/time-span.ts b/packages/lib/std/src/time-span.ts
index 900999b15..b42f0b0dd 100644
--- a/packages/lib/std/src/time-span.ts
+++ b/packages/lib/std/src/time-span.ts
@@ -3,15 +3,25 @@
*/
import { int, Unhandled } from "./lang";
+/**
+ * Represents a span of time with millisecond precision and provides
+ * convenient factory methods and conversions.
+ */
export class TimeSpan {
+ /** Time span representing positive infinity. */
static readonly POSITIVE_INFINITY = new TimeSpan(Number.POSITIVE_INFINITY);
+ /** Creates a span from milliseconds. */
static readonly millis = (value: number) => new TimeSpan(value);
+ /** Creates a span from seconds. */
static readonly seconds = (value: number) =>
new TimeSpan(value * TimeSpan.#MILLI_SECONDS_PER_SECOND);
+ /** Creates a span from minutes. */
static readonly minutes = (value: number) =>
new TimeSpan(value * TimeSpan.#MILLI_SECONDS_PER_MINUTE);
+ /** Creates a span from hours. */
static readonly hours = (value: number) =>
new TimeSpan(value * TimeSpan.#MILLI_SECONDS_PER_HOUR);
+ /** Creates a span from days. */
static readonly days = (value: number) =>
new TimeSpan(value * TimeSpan.#MILLI_SECONDS_PER_DAY);
@@ -26,21 +36,27 @@ export class TimeSpan {
this.#ms = ms;
}
+ /** Returns the span in milliseconds. */
millis(): number {
return this.#ms;
}
+ /** Absolute value in seconds. */
absSeconds(): number {
return Math.abs(this.#ms) / TimeSpan.#MILLI_SECONDS_PER_SECOND;
}
+ /** Absolute value in minutes. */
absMinutes(): number {
return Math.abs(this.#ms) / TimeSpan.#MILLI_SECONDS_PER_MINUTE;
}
+ /** Absolute value in hours. */
absHours(): number {
return Math.abs(this.#ms) / TimeSpan.#MILLI_SECONDS_PER_HOUR;
}
+ /** Absolute value in days. */
absDays(): number {
return Math.abs(this.#ms) / TimeSpan.#MILLI_SECONDS_PER_DAY;
}
+ /** Splits the span into days, hours, minutes and seconds. */
split(): { d: int; h: int; m: int; s: int } {
return {
d: Math.floor(this.absDays()),
@@ -49,15 +65,19 @@ export class TimeSpan {
s: Math.floor(this.absSeconds()) % 60,
};
}
+ /** Checks whether the span equals zero. */
isNow(): boolean {
return this.#ms === 0.0;
}
+ /** Checks whether the span is negative. */
isPast(): boolean {
return this.#ms < 0.0;
}
+ /** Checks whether the span is positive. */
isFuture(): boolean {
return this.#ms > 0.0;
}
+ /** Formats the span as a relative unit string such as `2 minutes`. */
toUnitString(): string {
let value: number, unit: Intl.RelativeTimeFormatUnit;
const seconds = Math.floor(Math.abs(this.#ms) / 1000);
@@ -82,6 +102,7 @@ export class TimeSpan {
style: "long",
}).format(value * Math.sign(this.#ms), unit);
}
+ /** Human readable representation such as `1 h, 2 m`. */
toString(): string {
if (isNaN(this.#ms)) {
return "NaN";
@@ -132,3 +153,4 @@ export class TimeSpan {
}
};
}
+
diff --git a/packages/studio/core/src/Engine.ts b/packages/studio/core/src/Engine.ts
index cfeb3a95d..e4f8b9ce9 100644
--- a/packages/studio/core/src/Engine.ts
+++ b/packages/studio/core/src/Engine.ts
@@ -18,6 +18,8 @@ import {Project} from "./Project"
/**
* Event describing a note-on or note-off trigger emitted by the engine.
+ *
+ * @public
*/
export type NoteTrigger =
| { type: "note-on", uuid: UUID.Format, pitch: byte, velocity: unitValue }
@@ -25,6 +27,8 @@ export type NoteTrigger =
/**
* Abstraction of the playback engine exposing transport and sequencing control.
+ *
+ * @public
*/
export interface Engine extends Terminable {
/** Starts playback. */
diff --git a/packages/studio/core/src/RenderQuantum.ts b/packages/studio/core/src/RenderQuantum.ts
index 5647a3541..fbbef64b0 100644
--- a/packages/studio/core/src/RenderQuantum.ts
+++ b/packages/studio/core/src/RenderQuantum.ts
@@ -1,6 +1,9 @@
/**
* Number of frames processed per Web Audio render quantum.
*
+ * @remarks
+ * This constant matches the fixed buffer size used by the Web Audio API.
+ *
* @public
*/
export const RenderQuantum = 128