← JamDojo Context-Aware Patterns

Context-Aware Patterns

Beyond Individual Events

So far, every transformation we’ve seen works on events in isolation. .fmap(x => x + 7) adds 7 to each note—it doesn’t know or care what the neighboring notes are.

But music often depends on context:

  • Legato: Is this note connected to the next?
  • Accents: Is this the first beat of a phrase?
  • Dynamics: Should this note crescendo based on its position?

These require seeing beyond the single event.


Events Carry Context

Every event in Strudel carries metadata in its context:

note("c4 e4 g4")
.withContext(ctx => ({ ...ctx, custom: "hello" }))
.log()

Check the console. Each event has a context field. This is where Strudel tracks information like source location, tags, and custom data.


withContext: Transforming Metadata

.withContext() lets you modify the context of each event:

note("c4 e4 g4 b4")
.withContext((ctx, i) => ({ ...ctx, index: i }))
.log()

The second parameter is the event index within the cycle. This gives you position awareness.


Tagging Events

Tags let you identify events for later filtering or processing:

stack(
sound("bd sd bd sd").tag("drums"),
note("c4 e4 g4 e4").sound("piano").tag("melody")
).log()

Check the context—each event carries its tag. You can filter by tag:

stack(
sound("bd sd bd sd").tag("drums"),
note("c4 e4 g4 e4").sound("piano").tag("melody")
).filterByTag("melody")

Only the melody survives.


The Comonad Intuition

In category theory, a Comonad is the dual of a Monad. Where Monad lets you flatten nested containers, Comonad lets you extend context to each position.

Think of it this way:

  • Functor: Transform each value independently
  • Comonad: Transform each value with awareness of its neighborhood

Strudel’s context system is an implicit Comonad. Events carry context, and that context can include information about surrounding events.


Position-Based Transformations

Using the index parameter, you can create position-aware effects:

// Crescendo through the phrase
note("c4 d4 e4 f4 g4 a4 b4 c5")
.sound("piano")
.gain("0 1 2 3 4 5 6 7".fmap(i => 0.3 + (i / 7) * 0.7))
// Accent every 4th note
note("c4 d4 e4 f4 g4 a4 b4 c5")
.sound("piano")
.gain("0 1 2 3 4 5 6 7".fmap(i => i % 4 === 0 ? 1 : 0.5))

Collecting Neighbors

The .collect() method groups events that overlap in time:

// Stack creates overlapping events
stack(
note("c4"),
note("e4"),
note("g4")
).collect().log()

Instead of three separate events, you get one event containing an array of all three. This is useful for chord detection and voicing operations.


arpWith: Context-Aware Arpeggiation

arpWith uses .collect() internally to see all notes at once, then selects from them:

note("[c4,e4,g4,b4]")
.arpWith((notes, i) => notes[i % notes.length])
.sound("piano")

The function receives the full chord and an index. You decide how to walk through:

// Custom arpeggio pattern
note("[c4,e4,g4,b4]")
.arpWith((notes, i) => {
  const patterns = [0, 2, 1, 3, 2, 0]  // custom walk
  return notes[patterns[i % patterns.length]]
})
.fast(1.5)
.sound("piano")

Building Context-Aware Effects

Here’s how to build effects that depend on musical context:

First note accent:

note("c4 e4 g4 e4 c4 e4 g4 b4")
.sound("piano")
.gain(pure(i => i === 0 ? 1 : 0.6).squeeze(8))

Alternating pan:

note("c4 d4 e4 f4 g4 a4 b4 c5")
.sound("piano")
.pan("0 1 2 3 4 5 6 7".fmap(i => i % 2 === 0 ? 0.3 : 0.7))

Position-based filter sweep:

note("c3 e3 g3 c4".fast(2))
.sound("sawtooth")
.lpf("0 1 2 3 4 5 6 7".fmap(i => 400 + i * 200))

Location Tracking

Strudel automatically tracks where each event came from:

stack(
note("c4").tag("first"),
note("e4").tag("second")
).log()

The context.locations field shows the chain of transformations that produced each event. This is used for debugging and the visual highlighter.


Why Context Matters

Context enables effects that understand musical structure:

EffectRequires
LegatoKnowing if next note overlaps
Phrase dynamicsPosition within phrase
VoicingSeeing all notes in chord
HumanizationVarying by position
FollowingKnowing what came before

Without context, every note is an island. With context, notes understand their place in the musical fabric.


Limitations

Strudel’s context system is implicit rather than fully comonadic:

  • You can’t directly query neighboring events by time offset
  • Context is per-event, not a sliding window
  • Full comonad operations (extend, duplicate) aren’t exposed

For most musical purposes, tags, indices, and .collect() provide enough context awareness. True comonadic patterns would require deeper API changes.


Quick Reference

// Add custom context
pattern.withContext(ctx => ({ ...ctx, key: value }))

// Tag events
pattern.tag("name")

// Filter by tag
pattern.filterByTag("name")

// Group overlapping events
pattern.collect()

// Context-aware arpeggiation
chord.arpWith((notes, index) => notes[index % notes.length])

// Position-based values
"0 1 2 3".fmap(i => /* use index */)

What’s Next?

We’ve explored how events can carry and use context. The final advanced concept: what happens when you need to flip the structure of nested patterns?

Continue to Flipping Structure

You’ve seen context-aware patterns. Next, you’ll discover Traversable—how to turn a pattern of possibilities into possibilities of patterns, essential for controlled randomness and generative music.