update plans

This commit is contained in:
André Michelle
2026-01-24 08:42:01 +01:00
parent 91c828e6a4
commit c5fb16b54d
3 changed files with 55 additions and 168 deletions
+2
View File
@@ -2,6 +2,8 @@
## Coding Style
- **Minimize comments.** Code should be self-explanatory. Only add comments when the logic is truly non-obvious.
- **No blank lines inside methods.** Keep method bodies compact without empty line separators.
- **Never use single-letter abbreviations in lambdas.** Use descriptive names like `entry`, `text`, `value`, `event`, etc.
- **Use types and functions from `@opendaw/lib-std` instead of inline checks:**
- Use `Optional<T>` instead of `T | undefined`
@@ -23,7 +23,18 @@ export const AudioRegionBox: BoxSchema<Pointers> = {
14: {type: "boolean", name: "mute"},
15: {type: "string", name: "label"},
16: {type: "int32", name: "hue", ...HueConstraints},
17: {type: "float32", name: "gain", constraints: "decibel", unit: "db"}
17: {type: "float32", name: "gain", constraints: "decibel", unit: "db"},
18: {
type: "object", name: "fading", class: {
name: "Fading",
fields: {
1: {type: "float32", name: "in", value: 0.0, constraints: "unipolar", unit: "ratio"},
2: {type: "float32", name: "out", value: 1.0, constraints: "unipolar", unit: "ratio"},
3: {type: "float32", name: "in-slope", value: 0.5, constraints: "unipolar", unit: "ratio"},
4: {type: "float32", name: "out-slope", value: 0.5, constraints: "unipolar", unit: "ratio"}
}
}
}
}
}, pointerRules: {accepts: [Pointers.Selection, Pointers.Editing, Pointers.MetaData], mandatory: false}
}
+41 -167
View File
@@ -77,53 +77,28 @@ import {Fading} from "@opendaw/studio-boxes"
import {FadingEnvelope} from "@opendaw/lib-dsp"
import {MutableObservableValue, unitValue} from "@opendaw/lib-std"
/**
* Adapter for the generated Fading object.
* Implements FadingEnvelope.Config so it can be passed directly to DSP functions.
*/
export class FadingAdapter implements FadingEnvelope.Config {
readonly #fading: Fading
constructor(fading: Fading) {
this.#fading = fading
}
// Direct field accessors (postfix "Field") for subscriptions/mutations
constructor(fading: Fading) {this.#fading = fading}
get inField(): MutableObservableValue<number> {return this.#fading.in}
get outField(): MutableObservableValue<number> {return this.#fading.out}
get inSlopeField(): MutableObservableValue<number> {return this.#fading.inSlope}
get outSlopeField(): MutableObservableValue<number> {return this.#fading.outSlope}
// FadingEnvelope.Config implementation - value getters
get in(): unitValue {return this.#fading.in.getValue()}
get out(): unitValue {return this.#fading.out.getValue()}
get inSlope(): unitValue {return this.#fading.inSlope.getValue()}
get outSlope(): unitValue {return this.#fading.outSlope.getValue()}
// Check if any fading is active
get hasFading(): boolean {
return this.in > 0.0 || this.out < 1.0
}
// Delegate to FadingEnvelope for gain calculation
gainAt(normalizedPosition: unitValue): number {
return FadingEnvelope.gainAt(normalizedPosition, this)
}
// Fill a gain buffer with fading envelope (delegates to FadingEnvelope)
get hasFading(): boolean {return this.in > 0.0 || this.out < 1.0}
gainAt(normalizedPosition: unitValue): number {return FadingEnvelope.gainAt(normalizedPosition, this)}
fillGainBuffer(gainBuffer: Float32Array, startNormalized: number, endNormalized: number, sampleCount: number): void {
FadingEnvelope.fillGainBuffer(gainBuffer, startNormalized, endNormalized, sampleCount, this)
}
// Copy values to another Fading object
copyTo(target: Fading): void {
target.in.setValue(this.in)
target.out.setValue(this.out)
target.inSlope.setValue(this.inSlope)
target.outSlope.setValue(this.outSlope)
}
// Reset to defaults (no fading)
reset(): void {
this.#fading.in.setValue(0.0)
this.#fading.out.setValue(1.0)
@@ -179,50 +154,27 @@ export class AudioRegionBoxAdapter implements ... {
import {Curve, int, unitValue} from "@opendaw/lib-std"
export namespace FadingEnvelope {
/**
* Interface for fading configuration.
* FadingAdapter implements this interface directly.
*/
export interface Config {
readonly in: unitValue // 0.0-1.0: normalized end of fade-in
readonly out: unitValue // 0.0-1.0: normalized start of fade-out
readonly inSlope: unitValue // 0.5 = linear
readonly in: unitValue
readonly out: unitValue
readonly inSlope: unitValue
readonly outSlope: unitValue
}
/**
* Calculate fade gain at a normalized position within region (0.0-1.0)
* Returns gain multiplier 0.0-1.0
*/
export const gainAt = (normalizedPosition: unitValue, config: Config): number => {
const {in: fadeIn, out: fadeOut, inSlope, outSlope} = config
let fadeInGain = 1.0
let fadeOutGain = 1.0
if (fadeIn > 0.0 && normalizedPosition < fadeIn) {
fadeInGain = Curve.normalizedAt(normalizedPosition / fadeIn, inSlope)
}
if (fadeOut < 1.0 && normalizedPosition > fadeOut) {
const progress = (normalizedPosition - fadeOut) / (1.0 - fadeOut)
fadeOutGain = 1.0 - Curve.normalizedAt(progress, outSlope)
}
return Math.min(fadeInGain, fadeOutGain)
}
/**
* Fill a gain buffer with the fading envelope.
* Uses Curve.walk for efficient envelope generation without per-sample pow() calls.
* The gain buffer can then be applied to audio by voices.
*
* @param gainBuffer - Float32Array to fill with gain values (length = RenderQuantum)
* @param startNormalized - Normalized position at block start (0.0-1.0)
* @param endNormalized - Normalized position at block end (0.0-1.0)
* @param sampleCount - Number of samples to fill
* @param config - Fading configuration (FadingAdapter implements this)
*/
export const fillGainBuffer = (
gainBuffer: Float32Array,
startNormalized: number,
@@ -231,26 +183,13 @@ export namespace FadingEnvelope {
config: Config
): void => {
const {in: fadeIn, out: fadeOut, inSlope, outSlope} = config
// Initialize to 1.0 (no attenuation)
gainBuffer.fill(1.0, 0, sampleCount)
// No fading active - buffer already filled with 1.0
if (fadeIn <= 0.0 && fadeOut >= 1.0) {return}
// Block entirely in unity region - buffer already filled with 1.0
if (startNormalized >= fadeIn && endNormalized <= fadeOut) {return}
const normalizedPerSample = (endNormalized - startNormalized) / sampleCount
// Fill fade-in region using Curve.walk
if (fadeIn > 0.0 && startNormalized < fadeIn) {
const fadeInEndNorm = Math.min(endNormalized, fadeIn)
const fadeInEndSample = Math.min(
sampleCount,
Math.ceil((fadeInEndNorm - startNormalized) / normalizedPerSample)
)
const fadeInEndSample = Math.min(sampleCount, Math.ceil((fadeInEndNorm - startNormalized) / normalizedPerSample))
if (fadeInEndSample > 0) {
const startProgress = startNormalized / fadeIn
const endProgress = fadeInEndNorm / fadeIn
@@ -260,28 +199,19 @@ export namespace FadingEnvelope {
}
}
}
// Fill fade-out region using Curve.walk
if (fadeOut < 1.0 && endNormalized > fadeOut) {
const fadeOutStartNorm = Math.max(startNormalized, fadeOut)
const fadeOutStartSample = Math.max(
0,
Math.floor((fadeOutStartNorm - startNormalized) / normalizedPerSample)
)
const fadeOutStartSample = Math.max(0, Math.floor((fadeOutStartNorm - startNormalized) / normalizedPerSample))
const steps = sampleCount - fadeOutStartSample
if (steps > 0) {
const startProgress = (fadeOutStartNorm - fadeOut) / (1.0 - fadeOut)
const endProgress = (endNormalized - fadeOut) / (1.0 - fadeOut)
// Walk from 1.0 down to 0.0 (inverted)
const iterator = Curve.walk(outSlope, steps, 1.0 - startProgress, 1.0 - endProgress)
for (let i = fadeOutStartSample; i < sampleCount; i++) {
gainBuffer[i] = Math.min(gainBuffer[i], iterator.next().value)
}
}
}
// Handle overlap region (already handled by Math.min above)
}
}
```
@@ -294,35 +224,15 @@ Add a shared gain buffer and compute fading envelope once per region:
```typescript
import {RenderQuantum} from "@opendaw/lib-dsp"
import {FadingEnvelope} from "@opendaw/lib-dsp"
export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProcessor, AudioGenerator {
// Shared gain buffer - reused each block to avoid allocations
readonly #fadingGainBuffer: Float32Array = new Float32Array(RenderQuantum)
// ...
#processBlock(lane: Lane, block: Block): void {
// ...
for (const region of adapter.regions.collection.iterateRange(p0, p1)) {
if (region.mute || !isInstanceOf(region, AudioRegionBoxAdapter)) {continue}
const {fading} = region
// Compute fading envelope into shared gain buffer (once per region)
if (fading.hasFading) {
const startNormalized = /* compute from cycle */
const endNormalized = /* compute from cycle */
FadingEnvelope.fillGainBuffer(
this.#fadingGainBuffer,
startNormalized,
endNormalized,
fading
)
}
// Pass gain buffer to voice processing
// Voices apply: buffer[i] *= gainBuffer[i] for both channels
fading.fillGainBuffer(this.#fadingGainBuffer, startNormalized, endNormalized, sampleCount)
// Pass to voices
}
}
}
@@ -332,40 +242,27 @@ export class TapeDeviceProcessor extends AbstractProcessor implements DeviceProc
#### File: `packages/studio/core-processors/src/devices/instruments/Tape/DirectVoice.ts`
Update `process` method to accept an optional gain buffer:
Update `process` method to accept the gain buffer:
```typescript
export class DirectVoice {
// ... existing fields ...
/**
* Renders audio samples to the output buffer.
* @param bufferStart - Start position in the output buffer
* @param bufferCount - Number of samples to process
* @param fadingGainBuffer - Pre-computed fading gain buffer (filled with 1.0 if no fading)
*/
process(bufferStart: int, bufferCount: int, fadingGainBuffer: Float32Array): void {
const [outL, outR] = this.#output.channels()
const {frames, numberOfFrames} = this.#data
const framesL = frames[0]
const framesR = frames.length === 1 ? frames[0] : frames[1]
// ... existing state variables ...
for (let i = 0; i < bufferCount; i++) {
// ... existing amplitude calculation (voice fade-in/fade-out) ...
// ... existing voice amplitude calculation ...
if (readPosition >= 0 && readPosition < numberOfFrames) {
const finalAmplitude = amplitude * fadingGainBuffer[i]
outL[j] += framesL[readPosition] * finalAmplitude
outR[j] += framesR[readPosition] * finalAmplitude
}
// ... rest of processing ...
}
}
}
```
Apply similar changes to `PitchVoice.ts` and the time-stretch sequencer.
Apply similar changes to `PitchVoice.ts` and `TimeStretchSequencer.ts` - all voice types receive the fadingGainBuffer.
---
@@ -458,32 +355,20 @@ Call `renderFading` after rendering the waveform, using a semi-transparent overl
### 6.1 Fading Handle Capturing
#### File: `packages/app/studio/src/ui/timeline/editors/audio/AudioCapturing.ts`
#### File: `packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionCapturing.ts`
Add capture targets for fading circle handles:
Add capture targets for fading circle handles (slope editing deferred to future iteration):
```typescript
export type AudioCaptureTarget =
| {type: "loop-duration", reader: AudioEventOwnerReader}
| {type: "fading-in", reader: AudioEventOwnerReader}
| {type: "fading-out", reader: AudioEventOwnerReader}
| {type: "fading-in-slope", reader: AudioEventOwnerReader}
| {type: "fading-out-slope", reader: AudioEventOwnerReader}
// Use Geom.isInsideCircle for hit detection (see geom.ts addition below)
// Fading in handle: circle at (fadeIn position, top of curve)
// Fading out handle: circle at (fadeOut position, top of curve)
// Slope handles: circles on the curve midpoint for bending
export type RegionCaptureTarget =
| {type: "fading-in", reader: RegionReader}
| {type: "fading-out", reader: RegionReader}
```
#### File: `packages/lib/std/src/geom.ts`
Add circle hit detection to the Geom namespace:
```typescript
export namespace Geom {
// ... existing code ...
export const isInsideCircle = (x: number, y: number, cx: number, cy: number, radius: number): boolean => {
const dx = x - cx
const dy = y - cy
@@ -492,60 +377,45 @@ export namespace Geom {
}
```
### 6.2 Fading Editor Integration
### 6.2 Fading Editor Integration (Main Timeline)
#### File: `packages/app/studio/src/ui/timeline/editors/audio/AudioEditorCanvas.tsx`
#### File: `packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionRenderer.ts`
Add draggable fading circle handles:
Add draggable fading circle handles on regions in the main timeline:
```typescript
// In the canvas render callback:
const {fading} = reader.audioContent
// Render fading curves and circle handles
const {fading} = region
if (fading.hasFading) {
renderFading(context, range,
fading.in, fading.out,
fading.inSlope, fading.outSlope,
{top: 0, bottom: actualHeight},
reader.offset,
reader.offset + reader.loopDuration,
`hsla(${reader.hue}, 60%, 30%, 0.5)`)
// Render circle handles at fading positions
renderFading(context, range, fading.in, fading.out, fading.inSlope, fading.outSlope,
{top, bottom}, region.position, region.position + region.duration,
`hsla(${region.hue}, 60%, 30%, 0.5)`)
const handleRadius = 5 * devicePixelRatio
if (fading.in > 0) {
const x = range.unitToX(reader.offset + reader.loopDuration * fading.in) * devicePixelRatio
const x = range.unitToX(region.position + region.duration * fading.in) * devicePixelRatio
context.beginPath()
context.arc(x, top, handleRadius, 0, Math.PI * 2)
context.fill()
}
if (fading.out < 1) {
const x = range.unitToX(reader.offset + reader.loopDuration * fading.out) * devicePixelRatio
const x = range.unitToX(region.position + region.duration * fading.out) * devicePixelRatio
context.beginPath()
context.arc(x, top, handleRadius, 0, Math.PI * 2)
context.fill()
}
}
// Add dragging handlers for fading circle handles
Dragging.attach(canvas, event => {
const target = capturing.captureEvent(event)
if (target?.type === "fading-in") {
// Drag horizontally to adjust fading.in ratio
}
if (target?.type === "fading-out") {
// Drag horizontally to adjust fading.out ratio
}
if (target?.type === "fading-in") { /* drag to adjust fading.inField */ }
if (target?.type === "fading-out") { /* drag to adjust fading.outField */ }
})
```
### 6.3 Region Context Menu
Add fading options to the region context menu for quick access:
Add fading option to the region context menu:
```typescript
MenuItem.default({label: "Reset Fades"})
MenuItem.default({label: "Reset Fading"})
.setTriggerProcedure(() => editing.modify(() => region.fading.reset()))
```
@@ -596,10 +466,11 @@ MenuItem.default({label: "Reset Fades"})
| `packages/lib/std/src/geom.ts` | Add `isInsideCircle` to Geom namespace |
| `packages/studio/core-processors/src/devices/instruments/Tape/DirectVoice.ts` | Add fadingGainBuffer parameter |
| `packages/studio/core-processors/src/devices/instruments/Tape/PitchVoice.ts` | Add fadingGainBuffer parameter |
| `packages/studio/core-processors/src/devices/instruments/Tape/TimeStretchSequencer.ts` | Add fadingGainBuffer parameter |
| `packages/studio/core-processors/src/devices/instruments/TapeDeviceProcessor.ts` | Add shared gain buffer, fill and pass to voices |
| `packages/app/studio/src/ui/timeline/renderer/audio.ts` | Add renderFading |
| `packages/app/studio/src/ui/timeline/editors/audio/AudioEditorCanvas.tsx` | Add fading circle handles and editing |
| `packages/app/studio/src/ui/timeline/editors/audio/AudioCapturing.ts` | Add fading circle capture targets |
| `packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionRenderer.ts` | Add fading circle handles and editing |
| `packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionCapturing.ts` | Add fading circle capture targets |
---
@@ -654,10 +525,13 @@ voice.process(bufferStart, bufferCount, gainBuffer)
## 10. Notes
### Looping Regions
Fades apply to each loop cycle independently. When a region loops, the fade-in and fade-out are computed relative to each individual loop iteration, not the total audible duration.
Fades are based on `position` and `duration` only - loops are ignored. The fade envelope spans the full region duration regardless of loop settings.
### Time Base
Fade ratios are independent of time base (Musical vs Seconds). They always represent a percentage of the current region/loop duration.
Fading ratios are independent of time base (Musical vs Seconds). They always represent a percentage of the region duration.
### Clips
Consider whether `AudioClipBoxAdapter` should also support fades. If so, the same pattern can be applied to clips for consistency.
### Rendering Location
Fading curves are rendered ONLY in the main timeline view on regions, NOT in the audio editor (which shows a single loop cycle).
### Slope Editing
Slope adjustment will be implemented in a future iteration. This iteration focuses on fading in/out positions only.