Skip to content
🚧 Under active construction — content being developed and verified.

15. Compositional Grammar: Structure, Density, and the Arc

A Poly patch is not a loop. It can behave like one — a fixed Euclidean distribution repeating at constant velocity will cycle indefinitely. But the moment you engage the macro system, the envelope modulators, the phrase gating, and the mutation engine, you are no longer building a pattern. You are composing a density curve. The patch becomes a structure with an arc: sparse openings, accumulating momentum, climactic density, and deliberate release. This is the grammar of composition applied to polymetric generation.

Every piece of music, in every tradition, follows some version of a density curve. A West African drum ensemble begins with the bell alone. One by one, support drums enter, the lead drum builds intensity, the ensemble reaches a peak, and eventually the texture thins to signal the next section. A techno DJ builds a set the same way — filtering in layers, stacking loops, peaking, then stripping back to a kick and a hi-hat.

In Poly, the Density macro is your primary shape tool. It controls how many hits in each lane actually sound. At Density 0.2, most Euclidean onsets are suppressed — only the strongest positions fire, producing a skeletal outline of the pattern. At Density 0.8, nearly everything plays, and the texture becomes thick and busy. The full range from 0 to 1.0 traces the path from silence to saturation.

The compositional insight is that you do not set Density to a single value and leave it. You automate it. A sweep from 0.2 to 0.8 over 16 bars is a crescendo. A sweep from 0.8 to 0.3 is a breakdown. The Density macro is the master volume knob of rhythmic texture — not loudness, but presence.

GrooveState resolveMacros(const GrooveState& input) {
    GrooveState out = input;
    const auto& m = input.macros;

    for (int i = 0; i < out.activeLaneCount; ++i) {
        auto& lane = out.lanes[i];
        const auto& base = input.lanes[i];
        if (base.timeline)
            continue;
        int maxSteps = base.cycle.steps;
        if (maxSteps <= 0)
            continue;

        // --- Complexity: scales hitCount and rotation, deepens envelopes ---
        // At 0: hitCount toward 1, rotation 0, envelope depth halved
        // At 0.5: no change (passthrough)
        // At 1: hitCount toward maxSteps, rotation toward half-cycle, envelope depth doubled
        int minHits = 1;
        int maxHits = maxSteps;
        if (m.complexity < 0.5f) {
            float t = m.complexity * 2.0f;
            lane.hitCount =
                static_cast<int>(std::round(lerp(static_cast<float>(minHits), static_cast<float>(base.hitCount), t)));
        } else {
            float t = (m.complexity - 0.5f) * 2.0f;
            lane.hitCount =
                static_cast<int>(std::round(lerp(static_cast<float>(base.hitCount), static_cast<float>(maxHits), t)));
        }
        lane.hitCount = std::clamp(lane.hitCount, 0, maxSteps);

        float rotRange = static_cast<float>(maxSteps / 2);
        if (m.complexity < 0.5f) {
            float t = m.complexity * 2.0f;
            lane.rotation = static_cast<int>(std::round(lerp(0.0f, static_cast<float>(base.rotation), t)));
        } else {
            float t = (m.complexity - 0.5f) * 2.0f;
            lane.rotation = static_cast<int>(std::round(lerp(static_cast<float>(base.rotation), rotRange, t)));
        }

        // M002 S02 (GP04): low Complexity damps the ghost grammar, so a clean
        // groove stays clean. Same shape as the envelope depth scaling below.
        lane.ghostGrammar = base.ghostGrammar * lerp(0.25f, 1.5f, m.complexity);

        float envDepthScale = lerp(0.5f, 2.0f, m.complexity);
        for (int e = 0; e < lane.envelopeCount; ++e) {
            lane.envelopes[e].envelope.depth = std::clamp(base.envelopes[e].envelope.depth * envDepthScale, 0.0f, 1.0f);
        }

        // --- Density: scales probability and hitCount ---
        // At 0: probability halved, hitCount toward 1
        // At 0.5: no change
        // At 1: probability toward 1.0, hitCount toward max
        if (m.density < 0.5f) {
            float t = m.density * 2.0f;
            lane.probability = lerp(base.probability * 0.5f, base.probability, t);
            int densityHits =
                static_cast<int>(std::round(lerp(static_cast<float>(minHits), static_cast<float>(lane.hitCount), t)));
            lane.hitCount = std::clamp(densityHits, 0, maxSteps);
        } else {
            float t = (m.density - 0.5f) * 2.0f;
            lane.probability = lerp(base.probability, 1.0f, t);
            int densityHits =
                static_cast<int>(std::round(lerp(static_cast<float>(lane.hitCount), static_cast<float>(maxHits), t)));
            lane.hitCount = std::clamp(densityHits, 0, maxSteps);
        }
        lane.probability = std::clamp(lane.probability, 0.0f, 1.0f);

        // --- Syncopation: timing displacement, rotation, and accent shift ---
        // At 0: no change
        // At 1: even steps pushed late (1/3 step), rotation += half cycle, emphasisProb inverted
        int syncopRotation = static_cast<int>(std::round(m.syncopation * rotRange));
        lane.rotation = (lane.rotation + syncopRotation) % maxSteps;

        lane.emphasisProb = lerp(base.emphasisProb, 1.0f - base.emphasisProb, m.syncopation);
        lane.emphasisProb = std::clamp(lane.emphasisProb, 0.0f, 1.0f);

        lane.syncopationOffset = m.syncopation;

        // --- Swing: sets swingAmount across all lanes ---
        // Additive: macro swing adds to per-lane swing
        lane.swingAmount = std::clamp(base.swingAmount + m.swing, 0.0f, 1.0f);

        // --- Tension: widens velocity spread, pushes emphasisProb, deepens envelopes ---
        // At 0: spread halved, emphasisProb toward 0
        // At 0.5: no change
        // At 1: spread doubled, emphasisProb toward 1
        if (m.tension < 0.5f) {
            float t = m.tension * 2.0f;
            lane.velocitySpread = lerp(base.velocitySpread * 0.5f, base.velocitySpread, t);
        } else {
            float t = (m.tension - 0.5f) * 2.0f;
            lane.velocitySpread = lerp(base.velocitySpread, std::min(base.velocitySpread * 2.0f, 0.5f), t);
        }

        float tensionEmphasis = lerp(0.0f, 1.0f, m.tension);
        lane.emphasisProb = lerp(lane.emphasisProb, tensionEmphasis, 0.5f);
        lane.emphasisProb = std::clamp(lane.emphasisProb, 0.0f, 1.0f);

        float tensionEnvScale = lerp(0.5f, 2.0f, m.tension);
        for (int e = 0; e < lane.envelopeCount; ++e) {
            lane.envelopes[e].envelope.depth =
                std::clamp(lane.envelopes[e].envelope.depth * tensionEnvScale, 0.0f, 1.0f);
        }

        // --- Humanize: sets humanizeMs and widens velocity spread ---
        // At 0: no humanize
        // At 1: humanizeMs up to 25ms, spread increased
        lane.humanizeMs = std::clamp(base.humanizeMs + m.humanize * 25.0f, 0.0f, 50.0f);
        lane.velocitySpread = std::clamp(lane.velocitySpread + m.humanize * 0.05f, 0.0f, 0.5f);
    }

    return out;
}
Macro parameter scaling — how Density maps to per-lane hit probability

Where Density controls how many hits sound, Complexity controls which kinds of hits are available. At low Complexity, each lane plays its primary Euclidean pattern with minimal embellishment — no fills, no ghost-note flourishes, no secondary accents. As Complexity increases, the engine introduces ornamental detail: ghost notes become more frequent, fill events are more likely, accent patterns become more varied.

Think of Complexity as the difference between a drummer playing a groove straight and the same drummer adding flams, drags, and ghosted strokes. The underlying pattern is the same — the difference is surface detail. In a composition, low Complexity is appropriate for verses and introductions, where the rhythmic skeleton should be clear and uncluttered. High Complexity suits choruses and peaks, where the ear can absorb more information because the surrounding arrangement is full.

The interaction between Density and Complexity is multiplicative. Low Density with low Complexity is sparse and plain — a single kick and a skeletal hat pattern. Low Density with high Complexity is sparse but ornate — few hits, but each one decorated with ghost notes and micro-timing variation. High Density with low Complexity is thick but uniform — a wall of hits with no variation in articulation. High Density with high Complexity is the full orchestral peak — every lane firing, every hit embellished, every subdivision populated.

The Tension macro compresses or expands the dynamic range of the patch. At low Tension, velocity differences between hits are small — everything plays at roughly the same level. The groove sits in a narrow dynamic band, steady and controlled. At high Tension, the spread between accented and unaccented hits widens dramatically — strong beats hit hard, ghost notes are barely audible, and the pattern acquires a volatile, explosive quality.

Use low Tension for verses and passages that need to sit beneath a vocal or a melodic line. The groove supports without competing. Push Tension upward for climactic moments where the drums should dominate — the widened dynamic range creates visceral impact, the kind of snare crack that punches through a full mix.

The simplest section transition in Poly is a Density sweep. Over 4 to 8 bars, automate Density from one value to another. A sweep from 0.3 to 0.7 is a build — the classic pre-chorus intensification that every listener recognises instinctively. A sweep from 0.7 to 0.2 is a breakdown — the moment of release after a peak.

These sweeps work because Density acts on every lane simultaneously through the macro system. You do not need to automate eight individual parameters — one macro controls the collective density of the entire ensemble. The per-lane Euclidean patterns remain unchanged; what varies is how much of each pattern the listener actually hears.

For more dramatic transitions, combine Density sweeps with Complexity and Tension changes. Build Density from 0.2 to 0.8 over 8 bars while simultaneously sweeping Complexity from 0.1 to 0.6 and Tension from 0.3 to 0.7. The result is a crescendo that adds not just more hits but more detail and more dynamic variation — the ensemble does not just get louder, it gets bigger.

Phrase gating is Poly’s mechanism for introducing silence. Each lane can have a Phrase Length, a Gap, and an Offset. A lane with Length 8 and Gap 4 plays for 8 beats, rests for 4, then repeats. The pattern does not disappear during the gap — it simply does not sound. When it re-enters, it picks up where it left off in the Euclidean cycle.

Silence is compositional material. A 4-beat gap in a lead lane creates a call-and-response structure — the ensemble plays, the lead rests, then the lead re-enters with renewed energy. Staggered gaps across multiple lanes create a texture that breathes — different voices dropping out and re-entering at different times, so the ensemble is always full but never static.

The Offset parameter determines when each lane’s phrase cycle begins. To make two lanes alternate, point one at the other as its response lane: its gate becomes the complement of the call’s, so it plays exactly when the call rests. A lead-in lets the answer anticipate the call’s end, or a negative one lets it dovetail past. This is the rhythmic equivalent of antiphonal singing, where two choirs alternate phrases — and unlike matching Length and Gap by hand and offsetting one lane, the relationship survives a change to either lane’s phrasing, because it is structural rather than a coincidence of numbers.

Layered Composition: Building From the Anchor

Section titled “Layered Composition: Building From the Anchor”

The most reliable approach to building a composition in Poly is additive layering. Start with the anchor — typically a kick drum on Lane 1 — and build outward.

Step 1: The anchor. A single lane with a sparse, unambiguous Euclidean pattern. E(4,4) for straight four-on-the-floor, E(3,8) for a tresillo-based groove, E(3,7) for aksak. No ghost notes, no mutation, no phrase gating. This lane is the foundation.

Step 2: The timeline. Add a second lane that establishes the metric framework — a bell pattern, a rimshot, a hi-hat. Its step count can differ from the kick, introducing the first polymetric tension. Keep velocity moderate and mutation low.

Step 3: The texture. A third lane fills subdivision space — a ghost-heavy hi-hat pattern, a shaker, a busy conga line. This lane provides continuous rhythmic motion. Higher ghost values, moderate spread.

Step 4: The ornament. Additional lanes add colour — accents, fills, kotekan interlocking, lead-drum improvisation. These lanes use phrase gating, mutation, and higher Complexity sensitivity. They should appear and disappear, adding interest without cluttering the core groove.

Composition Arc: Sparse to FullCustom: Layered Build
Lane Role Steps Hits Rotation Subdivision Note Velocity Ghost Spread Phrase Len Gap
1 Kick anchor 4 4 0 1/4 36 110 0 0 0 0
2 Bell timeline 12 7 0 1/8 56 90 0 0 0 0
3 Ghost hat 16 11 2 1/16 42 55 50 25 0 0
4 Conga texture 8 5 1 1/8 63 75 40 15 12 4
5 Rim accent 7 3 0 1/8 37 85 0 0 8 8
6 Lead ornament 5 3 2 1/8 47 80 55 30 6 6

Lanes 1 through 3 are ungated — they play continuously, forming the permanent rhythmic bed. Lanes 4 through 6 use phrase gating with varying Length and Gap values, so they enter and exit the texture at different times. At Density 0.2, only the kick and fragments of the bell are audible. At Density 0.8, all six lanes are active and the full polymetric ensemble is sounding.

Compositional ArcComposition arc — layered build from kick anchor through bell, ghost hat, and gated ornamental lanes
Cubase automation lane — Density macro swept from 0.2 to 0.8 over 16 bars for a textural crescendo
Screenshot pending
Cubase automation lane — Density macro swept from 0.2 to 0.8 over 16 bars for a textural crescendo

Poly supports a maximum of eight simultaneous lanes. This is not a limitation — it is a creative constraint that enforces economy. Eight lanes of independent Euclidean patterns, each with its own step count, density, mutation, and phrase gating, already produces a rhythmic texture of enormous complexity. Adding more would not increase interest — it would decrease clarity.

The eight-lane ceiling forces you to make choices. Every lane must earn its place in the ensemble. If a lane is not contributing something that no other lane provides — a unique rhythmic role, a distinct timbral voice, a specific polymetric relationship — it should be removed. The strongest patches in Poly use four to six lanes, with each lane occupying a clearly defined function in the density hierarchy.

Think of it like an acoustic ensemble. A West African drum group has three to five voices. A Cuban son ensemble has five to seven. A jazz combo has four. Each member has a defined role. Nobody doubles another part. The constraint produces clarity.

The final principle is that composition in Poly is, fundamentally, density management. You are not writing melodies or chord progressions. You are shaping the density curve — deciding where the texture should be sparse and where it should be full, where the rhythmic interactions should be simple and where they should be complex, where silence should breathe and where every subdivision should be populated.

The macros — Density, Complexity, Syncopation, Tension, Humanize — are your compositional tools. The Euclidean patterns are your raw material. The envelopes and scene morphing are your temporal structure. And the phrase gating is your silence.

Every great drummer knows that what you do not play is as important as what you do. In Poly, that principle operates at every level: which hits sound (Density), which lanes play (phrase gating), which beats are emphasised (Tension), and how the whole ensemble evolves over time (scene morphing). The grammar of composition is the grammar of controlled density — and the silence between the hits is where the groove lives.

Preview audio uses CC0 and CC-BY drum samples. Every sample is credited on theCredits & Licenses page.