โ† JamDojo Combining Events

Combining Events

When Events Overlap

What happens when two notes land at the same time?

stack(
note("c4"),
note("e4"),
note("g4")
).sound("piano")

Three notes, same time. We hear a chord. But what actually happened? Did they merge into one event, or stay as three?

stack(
note("c4"),
note("e4"),
note("g4")
).sound("piano").log()

Check the console. Three separate events, all playing at once. stack doesnโ€™t mergeโ€”it layers.


Different Ways to Combine

There are multiple valid ways to handle overlapping events:

Layer (keep all):

stack(a, b)  // Both events play

Union (merge properties):

a.set(b)     // Properties from b override a

Replace (last wins):

layer(a, b)  // Similar to stack, but with context merging

Each strategy makes sense in different contexts. This is the Semigroup question: when two things combine, whatโ€™s the rule?


Semigroup: Things That Combine

A Semigroup is anything with a combining operation. The rule: (a โŠ• b) โŠ• c = a โŠ• (b โŠ• c)โ€”grouping doesnโ€™t matter.

You already know semigroups:

  • Numbers under addition: (1 + 2) + 3 = 1 + (2 + 3)
  • Strings under concatenation: ("a" + "b") + "c" = "a" + ("b" + "c")
  • Arrays under concatenation: [1,2].concat([3]) same either way

Patterns form a semigroup under stack:

// These are equivalent:
stack(stack(sound("bd"), sound("sd")), sound("hh"))

// stack(sound("bd"), stack(sound("sd"), sound("hh")))

Grouping doesnโ€™t change the resultโ€”all three play together.


Stack: Layering Events

stack is the most common way to combine patterns. It layers them:

stack(
sound("bd ~ bd ~"),
sound("~ sd ~ sd"),
sound("hh*8").gain(0.4)
)

Each pattern contributes its events. Nothing mergesโ€”they coexist.

Variants with alignment:

// stackLeft: align to start, fill gaps
stackLeft(
sound("bd bd"),
sound("sd sd sd sd")
)
// stackRight: align to end
stackRight(
sound("bd bd"),
sound("sd sd sd sd")
)

Cat: Sequential Combining

cat (also called slowcat) plays one pattern per cycle:

// One pattern per cycle (3 cycles to hear all)
cat(
sound("bd bd bd bd"),
sound("sd sd"),
sound("hh hh hh hh")
)

Cycle 1 plays kicks, cycle 2 plays snares, cycle 3 plays hi-hats, then repeats.

fastcat: Squeeze all into one cycle

// All three patterns in ONE cycle
fastcat(
sound("bd bd bd bd"),
sound("sd sd"),
sound("hh hh hh hh")
)

Same three patterns, but squeezed into a single cycle. Each takes its proportional share.


Combining Parameters

When control patterns overlap, parameters merge:

note("c4 e4 g4 c5")
.sound("sawtooth")
.lpf("800 1600 2400 3200")
.gain("1 0.8 0.6 0.4")

Each note gets its corresponding filter and gain value. The parameters combine by matching timing.

What if patterns have different lengths?

// 4 notes, 2 filter values
note("c4 e4 g4 c5")
.sound("sawtooth")
.lpf("800 2400")

The filter cycles: 800, 2400, 800, 2400. Patterns with different lengths create evolving combinations.


Union: Merging Properties

Sometimes you want to merge, not layer. The .set() method combines control patterns:

// Base pattern
const base = note("c4 e4 g4").sound("piano")

// Override with new values
base.set(gain("0.5 0.8 1"))

Properties from the right override the left. This is useful for creating variations:

const melody = note("c4 e4 g4 b4").sound("piano")

stack(
melody,
melody.set(note("e4 g4 b4 d5")),  // harmonize up
melody.set(note("g3 b3 d4 f4"))   // harmonize down
)

The Semigroup Laws

For any semigroup operation โŠ•:

Associativity: (a โŠ• b) โŠ• c = a โŠ• (b โŠ• c)

This is why you can write:

stack(a, b, c, d, e)  // instead of nested stack(stack(stack(...)))
cat(a, b, c, d)       // instead of nested cat(cat(...))

The implementation can group however it wantsโ€”the result is the same.


From Semigroup to Monoid

Remember Building with Patterns? A Monoid is a Semigroup plus an identity element.

OperationSemigroupIdentity (Monoid)
stackLayer patternssilence
catSequence patternssilence
+ on numbersAdd0
* on numbersMultiply1

The identity doesnโ€™t change anything:

// silence is the identity for stack
stack(
sound("bd ~ bd ~"),
silence,
sound("~ sd ~ sd")
)

Same result as without silence. This lets you write conditional patterns cleanly:

const useBass = true
const useHats = false

stack(
sound("bd ~ bd ~"),
sound("~ sd ~ sd"),
useBass ? note("c2").sound("sawtooth").lpf(400) : silence,
useHats ? sound("hh*8").gain(0.3) : silence
)

Practical Applications

Building arrangements:

const kick = sound("bd ~ bd ~")
const snare = sound("~ sd ~ sd")
const hats = sound("hh*8").gain(0.4)
const bass = note("c2 c2 g1 g1").sound("sawtooth").lpf(600)

// Build up section by section
cat(
kick,
stack(kick, hats),
stack(kick, hats, snare),
stack(kick, hats, snare, bass)
).slow(4)

Layering variations:

const melody = note("c4 e4 g4 e4")

stack(
melody.sound("piano"),
melody.add(12).sound("piano").gain(0.3).delay(0.1),
melody.sub(12).sound("sawtooth").lpf(800).gain(0.5)
)

Quick Reference

// Layering (simultaneous)
stack(a, b, c)          // all play together
stackLeft(a, b)         // align left
stackRight(a, b)        // align right

// Sequencing (consecutive)
cat(a, b, c)            // divide cycle equally
slowcat(a, b, c)        // one per cycle
fastcat(a, b, c)        // squeeze into one cycle

// Parameter merging
pattern.set(overrides)  // right overrides left

// Identity
silence                 // empty pattern

Whatโ€™s Next?

Weโ€™ve seen how to combine patterns by layering or sequencing. But what if you want to choose between patterns based on some condition?

Continue to Choice and Selection โ†’

Youโ€™ve learned to combine all patterns together. Next, youโ€™ll discover how to select from alternativesโ€”playing one pattern or another based on names, indices, or conditions.