This article is part 2 of a 5 part series introducing the core concepts of Intonal. It assumes a basic knowledge of music and terms MIDI, hz, and tempo. It assumes no prior programming knowledge, and is written for all audiences new to Intonal.
In Part 1 of this series, we looked at how streams and transforms form the foundation of Intonal. But we only looked at steams of numbers, and streams can be any type.
Intonal has a rich type system with user defined data & enums, first class transforms and more. For this post we'll dig into arrays to show how to play simple melody using the sine wave generator we made in Part 1.
Let's make a melody, defined as a series of MIDI note numbers
notes: [uint8] = [76, 74, 66, 66, 68, 68,
73, 71, 62, 62, 64, 64,
71, 69, 61, 61, 64, 64,
69, 69, 69, 69,
0]
The [uint8]
after the colon is called a type annotation. Type annotations are optional but are good practice to include. The brackets indicate it's an array of unsigned 8-bit integers. The only problem is, we need these values as floating-point hz values. Here's a transform that can do that heavy lifting.
midiToHz = {n in
out = pow(2, (float32(n)-69)/12) * 440.0
}
And to apply that transform to the entire notes
array:
hzs = map(notes, midiToHz)
Later we'll define a transform called streamMelody
which takes an array and converts it into a stream every X samples.
There is a special form of applying transforms called dot syntax which makes it easier to chain together multiple calls.
// Defining notes in the melody
notes: [uint8] = [76, 74, 66, 66, 68, 68,
73, 71, 62, 62, 64, 64,
71, 69, 61, 61, 64, 64,
69, 69, 69, 69,
0]
// Tempo pulse duration in seconds
durationInSecs = 1.0 / 7.0
main = {sr: float32 in
// Convert seconds to samples
durationInSamples = uint64(
sr * durationInSecs
)
// Map MIDI array to hz array
// then convert to a stream of hz
hz = notes
.map(midiToHz)
.streamMelody(durationInSamples)
// Mute if we encounter a frequency lower than 10
amp = if hz < 10 {0} else {0.2}
// From "Intro Part 1" example
p = phasor(hz, sr)
out = sin(p * 2 * PI) * amp
}
/* From stdlib */
PI = 3.1415926535
/* From stdlib */
midiToHz = {n in
out = pow(2, (float32(n)-69)/12) * 440.0
}
/* From stdlib */
phasor = {hz: float32, sr: float32 in
out = 0 fby ((prev + (hz/sr)) % 1)
}
Constants like PI, and transforms such as midiToHz and phasor shown above will exist as part of the Intonal standard library (stdlib) and in practice don't need to be defined. For educational purposes we are explicitly defining all these within the examples and not importing anything from the standard library.
Once last piece of the puzzle remains - how to convert an array of values into a stream. This task is so common there exists a transform streamify
in stdlib that takes an array and converts it to a stream that infinitely loops through the contents.
/* From stdlib */
streamify = {a in
i = 0 fby (prev + 1) % len(a)
out = a[i]
}
Each time this stream is advanced i
is advanced, looping around the length of the array.
However this would result in advancing through the frequencies once every sample. Instead we want to advance the stream at the tempo pulse. To achieve that, we use fby on
. Here's an example:
count: uint64 = 0 fby (prev + 1) % 4
advance: bool = count == 0
index: uint64 = 0 fby prev + 1 on advance
We'll dive deeper into controlling how streams advance in the next post, but this gives us enough tools to construct streamMelody
. Let's also play the melody once instead of looping forever by using by using min(prev + 1, len(hzs) - 1)
instead of (prev + 1) % len(hzs)
streamMelody = {hzs: [float32], durationInSamples: uint64 in
advanceNote = (0 fby (prev + 1) % durationInSamples) == 0
i = 0 fby min(prev + 1, len(hzs) - 1) on advanceNote
out = hzs[i]
}
/* From stdlib */
min = {x, y in if x > y {y} else {x}}
We have just dipped our feet into what Intonal can do, and in upcoming posts we'll dig deeper into controlling streams, types, and how polyphony works.
Feel free to reach out or join our Discord channel to ask questions or just say hi!
BONUS ROUND: try applying glide to the hz
stream:
... etc etc ...
hz = notes
.map(midiToHz)
.streamMelody(durationInSamples)
.glide(0.9995)
... etc etc ...
/* From stdlib */
glide = {s, a in
out = s fby (prev * a) + (s * (1-a))
}