Grooves - andremichelle/openDAW GitHub Wiki

Groove Mapping via Time Distortion in openDAW

Abstract

We introduce a generalized model for groove manipulation in DAWs based on a time-warping function that transforms rhythmic positions using monotonic curves. This model also enables pattern-based feel variations such as shuffle and swing, and works for arbitrary input positions, not limited to grid-bound steps. The method is lightweight and precisely implemented in openDAW using TypeScript.

Audio Files


1. Introduction

Classic groove implementations shift fixed rhythmic positions to achieve a more "human" or musical feel. However, they typically lack an efficient method for locating grooved events in time. With traditional systems, one would begin searching for events from the start of each groove cycle, which is inefficient. By introducing an inverse function, we can directly compute the exact seek window for any given time range, enabling accurate event lookup and quantization. It also guarantees that all located events map back into the original search window in the correct order.

Any strictly monotonically increasing function can serve as a groove function, as such functions are inherently unambiguously invertible.

Why Monotonicity Matters:

  • The order of events remains untouched
  • Predictable, continuous deformation
  • Allows distorting arbitrary event positions

Pseudo Code

// a, b: current render block in musical time
a', b' = groove.inverse(a), groove.inverse(b)

// Step through all semiquaver (16th note) triggers within the seek window
for each x' in [a', b'] with step 1/16:
    x = groove.forward(x')
    process_trigger_at(x)

2. Groove Example: Semiquaver Distortion

The diagram below demonstrates how a groove function alters semiquaver (16th note) timing. Every second semiquaver is shifted forward in time, creating a shuffle-like feel. The original time positions are marked in gray at integer fractions (x/16), while the grooved positions are shown in black.

Figure: Groove Effect on Semiquaver Grid

groove_sequencer_diagram

Figure: Function Plot

This function was initially developed in October 2010 to accomplish phase modulation, but works here fine too. It has been simplified now and accepts a more controllable parameter h, which is the value at x=1/2.

desmos-graph(https://www.desmos.com/calculator/ht8cytaxsz)

Let x in [0, 1] represent the normalized position within a pattern cycle (e.g., 8th notes), and let h be the strength of the groove.

$$f(x, h) = \frac{hx}{(2h - 1)(x - 1) + h}$$

The inverse function is obtained by replacing the shape parameter with h'=1-h.


3. Repeating the Groove Pattern

To repeat the pattern across the timeline:

$$y = \frac{f(\omega x - \lfloor \omega x \rfloor) + \lfloor \omega x \rfloor}{\omega}$$

Where:

  • ω is the pattern frequency

4. Chaining Grooves

Grooves can be chained to create more complex timing patterns. This is achieved by composing multiple monotonic groove functions. For example, if two groove functions (A(x)) and (B(x)) are applied in sequence, the result is:

$$Y = A(B(X))$$

To reverse the effect and retrieve the original input, the corresponding inverse functions must be applied in reverse order:

$$X = B^{-1}(A^{-1}(Y))$$

Since each function in the chain is strictly monotonic, the composition remains monotonic and thus invertible. This allows for flexible, layered groove processing while maintaining full control over both forward transformation and reverse lookup.


5. Typescript Implementation in openDAW

// Definitions
export type unitValue = number // 0...1

export const quantizeFloor = (value: number, interval: number): number => Math.floor(value / interval) * interval

export interface Bijective<X, Y> {
    fx: (x: X) => Y
    fy: (y: Y) => X
}

export interface GrooveFunction extends Bijective<unitValue, unitValue> {}

export interface GroovePatternFunction extends GrooveFunction {
    duration(): ppqn
}

export interface Groove {
    warp(position: ppqn): ppqn
    unwarp(position: ppqn): ppqn
}

export class GroovePattern implements Groove {
    readonly #func: GroovePatternFunction

    constructor(func: GroovePatternFunction) {this.#func = func}

    warp(position: ppqn): ppqn {return this.#transform(true, position)}
    unwarp(position: ppqn): ppqn {return this.#transform(false, position)}

    #transform(forward: boolean, position: ppqn): ppqn {
        const duration = this.#func.duration()
        const start = quantizeFloor(position, duration)
        const normalized = (position - start) / duration
        const transformed = forward ? this.#func.fx(normalized) : this.#func.fy(normalized)
        return start + transformed * duration
    }
}

export class GrooveChain implements Groove {
    readonly #grooves: ReadonlyArray<Groove>

    constructor(grooves: ReadonlyArray<Groove>) {this.#grooves = grooves}

    warp(position: ppqn): ppqn {
        for (let i = 0; i < this.#grooves.length; i++) {position = this.#grooves[i].warp(position)}
        return position
    }

    unwarp(position: ppqn): ppqn {
        for (let i = this.#grooves.length - 1; i >= 0; i--) {position = this.#grooves[i].unwarp(position)}
        return position
    }
}
// Example Implementation
// Möbius-Ease Curve: https://www.desmos.com/calculator/ht8cytaxsz
export const moebiusEase = (x: unitValue, h: unitValue): unitValue => (x * h) / ((2.0 * h - 1.0) * (x - 1.0) + h)
const h = 0.6 // creates a light shuffle
const groove: GroovePattern = new GroovePattern({
    duration: (): ppqn => PPQN.SemiQuaver * 2,
    fx: x => moebiusEase(x, h),
    fy: y => moebiusEase(y, 1.0 - h)
} satisfies GroovePatternFunction)

6. Conclusion

By abstracting groove into a mathematical function with an inverse, we decouple it from fixed patterns and bring precision to groove-based timing. The approach is extensible, suitable for real-time usage.


André Michelle – May 29, 2025

⚠️ **GitHub.com Fallback** ⚠️