Choice and Selection
Selecting From Alternatives
Sometimes you donโt want to combine all patternsโyou want to choose one.
// Select patterns by name
"<a b [a b]>".inhabit({
a: sound("bd sd"),
b: sound("hh hh hh hh")
})The pattern "<a b [a b]>" selects from the lookup table. First cycle plays a (kick-snare), second plays b (hi-hats), third plays both.
inhabit: Pattern Selection
inhabit takes a lookup table and a selector pattern:
"<verse chorus verse chorus bridge chorus>".inhabit({
verse: note("c4 e4 g4 e4").sound("piano"),
chorus: note("g4 b4 d5 b4").sound("piano").gain(1.2),
bridge: note("a4 c5 e5 c5").sound("piano").lpf(2000)
}).slow(6)Each name in the selector picks the corresponding pattern. This is how you build song structures.
With arrays (index selection):
"0 1 2 1".inhabit([
sound("bd"),
sound("sd"),
sound("hh hh")
])Numbers select by index. 0 picks the first pattern, 1 picks the second, etc.
Why Selection Matters
Selection solves a fundamental problem: conditional behavior.
Without selection:
// Awkward conditional
useBass ? bassPattern : silence
With selection:
// Clean selection
"<bass drums melody>".inhabit({ bass, drums, melody })
Selection makes patterns data-driven. The structure comes from the selector pattern, the content from the lookup.
Alternative: The Choice Abstraction
In category theory, Alternative describes types with a choice operation. The key components:
- Empty: A โnothingโ choice (
silence) - Choice: Pick one or the other (
<|>oralt)
Strudel implements this through:
silenceโ the empty patterninhabit/pickโ selection from alternatives
stack combines everything: stack(a, b) plays both.
Selection chooses one: "<a b>".inhabit({a, b}) plays a first cycle, b second.
Stack is and. Selection is or.
pick vs inhabit
Both select from alternatives, but with different timing:
inhabit โ squeezes selected pattern into the selectorโs event:
// Each selection fills its time slot
"a b".inhabit({
a: sound("bd bd bd bd"),
b: sound("hh*8")
})The 4 kicks squeeze into the first half, 8 hi-hats into the second half.
pick โ selected pattern plays at its natural speed:
// Selected patterns maintain their rhythm
"a b".pick({
a: sound("bd bd bd bd"),
b: sound("hh*8")
})The patterns play at their defined speeds, sampled at selection points.
Dynamic Selection
The selector can be any pattern, including computed ones:
// Random selection
"0 1 2".fmap(() => Math.floor(Math.random() * 3)).inhabit([
sound("bd"),
sound("sd"),
sound("hh hh")
])// Probability-based
"<a a a b>".inhabit({
a: note("c4 e4").sound("piano"),
b: note("g4 b4 d5").sound("piano").gain(1.2)
})75% chance of a, 25% chance of b (in the slow cycle pattern).
Building Drum Machines
Selection is perfect for drum patterns:
const drums = {
k: sound("bd"),
s: sound("sd"),
h: sound("hh").gain(0.6),
o: sound("oh").gain(0.5),
c: sound("cp")
}
"[k h] [s h] [k h k h] [s o]".inhabit(drums)The pattern string becomes a readable drum notation. Change the sounds by editing the lookup:
const drums = {
k: sound("bd:3"),
s: sound("sd:2").gain(0.9),
h: sound("hh:1").gain(0.5),
o: sound("oh:2").gain(0.4),
c: sound("cp:1")
}
"[k h] [s h] [k h k h] [s o]".inhabit(drums)Same pattern, different sounds.
Nested Selection
Selections can contain more selections:
const fills = {
a: sound("bd sd bd sd"),
b: sound("sd sd sd [sd sd]")
}
const sections = {
verse: "[k h] [s h] [k h] [s h]".inhabit({
k: sound("bd"), s: sound("sd"), h: sound("hh").gain(0.5)
}),
fill: "<a b>".inhabit(fills)
}
"<verse verse verse fill>".inhabit(sections).slow(4)Verse plays three times, then a fill. The fill alternates between two variations.
Fallback Patterns
What if a selection is missing? Use default values:
// With fallback handling
const lookup = {
a: sound("bd"),
b: sound("sd"),
_: sound("hh") // fallback
}
"a b c b".inhabit(lookup)Unknown keys can fall back to a default (implementation depends on how you structure the lookup).
The Power of Indirection
Selection creates a layer of indirection:
// Direct: pattern IS the content
sound("bd sd hh cp")
// Indirect: pattern SELECTS content
"k s h c".inhabit({ k: sound("bd"), s: sound("sd"), ... })
Indirection enables:
- Reusability: Same selector, different lookups
- Readability: Pattern structure visible at a glance
- Flexibility: Change sounds without changing structure
// Same structure, two sound sets
const structure = "[k h] [s h] [k k] [s h]"
const acoustic = {
k: sound("bd:0"), s: sound("sd:0"), h: sound("hh:0").gain(0.5)
}
const electronic = {
k: sound("bd:3"), s: sound("cp"), h: sound("hh:3").gain(0.5)
}
stack(
structure.inhabit(acoustic),
structure.inhabit(electronic).late(0.002) // slight offset for stereo
)Quick Reference
// Selection by name
"<a b c>".inhabit({ a: patA, b: patB, c: patC })
// Selection by index
"0 1 2".inhabit([pat0, pat1, pat2])
// pick variants (different timing)
"a b".pick({ a, b }) // natural timing
"a b".inhabit({ a, b }) // squeezed timing
// Empty/silence as fallback
silence // the "nothing" pattern
Whatโs Next?
Weโve seen how to combine patterns (Semigroup) and select between them (Alternative). But what about patterns that need to see their contextโwhat comes before or after?
Continue to Context-Aware Patterns โ
Youโve learned selection and combination. Next, youโll discover how patterns can access their surrounding contextโenabling effects like legato detection, phrase-aware dynamics, and neighborhood-based transformations.