Class NoteStream
- Namespace
- Sigil.Sound
- Assembly
- Sigil.Sound.Notes.dll
Notes as they happen: a description of live notes (from a keyboard, a device, a pattern) and what is done to them on the way to an instrument or the next device. Where a Sigil.Sound.Pattern`1 is a score, a function of time, a stream is a performance: its notes start when a key goes down and end when it comes up, and each has an identity that every operator keeps.
public sealed class NoteStreamInheritance
Inherited Members
Examples
var keys = new NoteInput();
// Every key you press sounds with a fifth above it, and twice more a beat later, softer.
var plucks = keys.Stream.Harmonize(0, 7).Echo(2, Beats.Quarter, 0.5).Play(v => Saw(v.Frequency).LowPass(2000).Adsr(0.003, 0.2, 0, 0.2));
keys.NoteOn("c3 e3");
plucks * 0.3
Remarks
A stream is immutable, like a Sigil.Sound.Sound: every operator returns a new one, and nothing runs until something plays it
(Sigil.Sound.NoteStream.Play(System.Func{Sigil.Sound.Voice,Sigil.Sound.Sound},System.Int32)), sends it on (Sigil.Sound.NoteStream.Compile(Sigil.Sound.HostClock,System.Int32)) or reads it (Sigil.Sound.NoteStream.Chord(System.Double,Sigil.Sound.ChordLatch,Sigil.Sound.ChordSwitch), Sigil.Sound.NoteStream.Gate). Start from
Sigil.Sound.NoteInput.Stream; pattern.ToStream() turns a score into one.
Notes always end. Whatever an operator does, the end of a note ends exactly the notes it made from it, even a function of your own (Sigil.Sound.NoteStream.Select(System.Func{Sigil.Sound.NoteEvent,Sigil.Sound.NoteEvent}), Sigil.Sound.NoteStream.Where(System.Func{Sigil.Sound.NoteEvent,System.Boolean}), Sigil.Sound.NoteStream.SelectMany(Sigil.Sound.NoteExpander)), and everything still sounding is ended when the stream is reset, replaced or disposed. Capacities are fixed (128 notes sounding per step, 512 events a block); past them a note's start is dropped and its end never sent, never the other way round.
Time. Operators that work in beats (Sigil.Sound.NoteStream.Delay(System.Double), Sigil.Sound.NoteStream.Echo(System.Int32,System.Double,System.Double), Sigil.Sound.NoteStream.Strum(System.Double)) use the tempo of the Sigil.Sound.HostClock of the input (or 120 bpm without one) and keep running while the transport is stopped. Events are quantized to the block they fall in, under 6 ms at the default block size.
Properties
Silence
A stream with no notes.
public static NoteStream Silence { get; }Property Value
Methods
Chord(double, ChordLatch, ChordSwitch)
The chord the stream is playing, as a Sigil.Sound.Pattern`1 to write against: stream.Chord().Arp(ArpMode.Down) arpeggiates
whatever chord arrives, and stream.Chord().Steps(Mini.Integers("7 6 5 4")) plays steps of it. The same choices as the
AutoArp device decide what counts as a chord and when a new one takes over.
public Pattern<Chord> Chord(double settle = 0.035, ChordLatch latch = ChordLatch.Infinite, ChordSwitch switchOn = ChordSwitch.Beat)Parameters
settledouble-
Notes that start within this many seconds of each other are one chord, so a strum or a source that releases and presses voice by voice never shows a half-changed chord. 0 to 1; defaults to 0.035 (35 ms), as the AutoArp device.
latchChordLatch-
Whether the chord outlives its keys. Defaults to keeping the last chord.
switchOnChordSwitch-
When a new chord takes over. Sigil.Sound.ChordSwitch.Cycle makes every bar-long event (a run, an arpeggio) use one chord from start to finish; Sigil.Sound.ChordSwitch.Beat (the default) lets it change on the beat; Sigil.Sound.ChordSwitch.Instant as soon as it settles. The first chord is taken at once, and so is every chord while the transport is stopped.
Returns
- Pattern<Chord>
-
A live pattern: one event per cycle holding the chord, answering for the present whatever time it is asked about. It is empty before the first chord, and (with Sigil.Sound.ChordLatch.Gated) while no key is down. It cannot be written out with Sigil.Sound.NotePatterns.ToPattern(Sigil.Sound.Pattern{Sigil.Sound.Note},System.Int32), which throws.
Examples
// Whatever chord is held, a run through its tones: step 0 is the lowest tone, 1 the next, past the top it climbs an octave.
// Hold a different chord and the run follows it. (settle: 0 takes the chord at once, which a quick demo needs; the default is 35 ms.)
var keys = new NoteInput();
var chord = keys.Stream.Chord(settle: 0, switchOn: ChordSwitch.Instant);
var run = chord.Steps(Mini.Integers("0 1 2 3 4 3 2 1")).Transpose(12)
.Play(v => Triangle(v.Frequency).LowPass(2400).Adsr(0.005, 0.15, 0, 0.2), bpm: 120);
keys.NoteOn("c3 e3 g3");
run * 0.4
Remarks
The chord is the set of keys down when the last note of a group has settled, sorted from low to high. Letting go of keys never changes it (with the default latch), so a chord released key by key keeps its shape. The pattern is caught up whenever it is queried, once per block by whatever plays it, and reads the time from Sigil.Sound.NoteInput.Clock.
Exceptions
- ArgumentOutOfRangeException
-
The settle time is outside 0 to 1, or an option is not defined.
Compile(HostClock?, int)
Builds the stream so a host can run it block by block and read the notes that come out: the way to send a stream to a device's Notes out jack. Dispose the runner when done; it stops listening to the input.
public NoteStreamRunner Compile(HostClock? clock = null, int sampleRate = 48000)Parameters
clockHostClock?-
The clock the host advances, which gives beats their tempo and the grid its position. Null uses the Sigil.Sound.NoteInput.Clock of the input, and without one 120 bpm and a clock of the runner's own.
sampleRateint-
Frames per second of the blocks passed to Sigil.Sound.NoteStreamRunner.Process(System.Int32).
Returns
Delay(double)
Plays every note later, start and end alike, so each keeps its length.
public NoteStream Delay(double beats)Parameters
beatsdouble-
How much later, 0 to 64 beats (Sigil.Sound.Beats).
Returns
Examples
// The notes arrive half a beat late, at 120 bpm a quarter of a second.
var keys = new NoteInput();
var sound = keys.Stream.Delay(0.5)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3");
sound * 0.3
Exceptions
- ArgumentOutOfRangeException
-
Outside 0 to 64.
Echo(int, double, double)
Repeats every note, each repeat softer and as long as the note itself.
public NoteStream Echo(int repeats, double beats, double decay = 0.6)Parameters
repeatsint-
How many repeats, 1 to 16.
beatsdouble-
The time between repeats, above 0 up to 8 beats.
decaydouble-
The velocity of each repeat as a fraction of the one before, 0 to 1.
Returns
Examples
// Each note comes back three times, a sixteenth apart, each a little softer.
var keys = new NoteInput();
var sound = keys.Stream.Echo(3, Beats.Sixteenth, 0.6)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3");
sound * 0.3
Exceptions
- ArgumentOutOfRangeException
-
An argument is out of range.
Fold<TState>(TState, Func<TState, NoteEvent, TState>)
Folds the stream's events into a state with a function of your own and exposes it as a live pattern: one event per cycle holding the state as it is now. This is the reduce of the stream (a count of keys down, the last velocity, a chord of your own making).
public Pattern<TState> Fold<TState>(TState seed, Func<TState, NoteEvent, TState> step)Parameters
seedTState-
The state before the first event.
stepFunc<TState, NoteEvent, TState>-
Called for every start and end with the state so far; returns the new state. Runs on the thread that queries the pattern.
Returns
- Pattern<TState>
Type Parameters
TState-
The state.
Examples
// The number of keys held is a control signal: the more keys, the brighter the sound.
var keys = new NoteInput();
var held = keys.Stream.Fold(0, (n, e) => e.IsOn ? n + 1 : n - 1);
var cutoff = held.Select(n => 300.0 + n * 700.0).ToSignal(smoothSeconds: 0.05);
keys.NoteOn("c3 e3 g3");
Saw(110).LowPass(cutoff, q: 2) * 0.3
Remarks
Like Sigil.Sound.NoteStream.Chord(System.Double,Sigil.Sound.ChordLatch,Sigil.Sound.ChordSwitch), a live pattern that cannot be written out. Graph-mode scripts cannot call this.
Gate()
A control signal that is 1 while any note of the stream sounds and 0 otherwise: it lets a held key open a filter, gate an effect or trigger an envelope elsewhere. It steps; smooth it with a lowpass if you need a ramp.
public Sound Gate()Returns
Examples
// The gate is 1 while any key is down, so a held key opens a sound that would otherwise be shut.
var keys = new NoteInput();
var gate = keys.Stream.Gate();
keys.NoteOn("c3");
Saw(110).LowPass(1500) * gate * 0.3
Harmonize(params int[])
Plays each note and the notes semitones away from it: Harmonize(0, 7) is the note and its fifth,
Harmonize(-12) only the note an octave down. Include 0 to keep the original.
public NoteStream Harmonize(params int[] semitones)Parameters
semitonesint[]-
One to eight distances from the note.
Returns
Examples
// A major triad on every key: the note, a major third and a fifth above it.
var keys = new NoteInput();
var sound = keys.Stream.Harmonize(0, 4, 7)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3");
sound * 0.3
Exceptions
- ArgumentException
-
None, or more than eight.
Harmonize(Scale, params int[])
Plays each note and the tones of scale degrees steps away from it (a diatonic harmony):
in C major, Harmonize(scale, 0, 2) adds a third above every note. A note between two scale tones keeps its distance
from the one below.
public NoteStream Harmonize(Scale scale, params int[] degrees)Parameters
scaleScale-
The scale the steps are counted in.
degreesint[]-
One to eight steps from the note's own degree; 0 is the note, 2 a third above, -3 a fourth below.
Returns
Examples
// Diatonic triads: each key with the tones two and four steps above it in C major.
var keys = new NoteInput();
var sound = keys.Stream.Harmonize(Scale.Parse("c major"), 0, 2, 4)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("d3 g3");
sound * 0.3
Exceptions
- ArgumentException
-
None, or more than eight.
Humanize(double, double, int)
Makes the playing a little less exact: each note starts later by up to timing beats and its velocity moves by up
to velocity. The amounts depend only on the seed and the note, so the same performance humanizes the same way.
public NoteStream Humanize(double timing, double velocity = 0.1, int seed = 0)Parameters
timingdouble-
The most a note is late, 0 to 1 beat.
velocitydouble-
The most a velocity changes either way, 0 to 1.
seedint-
Another seed gives another, equally repeatable, feel.
Returns
Examples
// A chord that is not quite together: each note a little late and a little softer or louder, the same way every time.
var keys = new NoteInput();
var sound = keys.Stream.Humanize(0.15, 0.2, seed: 3)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3 g3 c4");
sound * 0.3
Exceptions
- ArgumentOutOfRangeException
-
Outside the range.
Latch()
Keeps notes sounding after their keys come up, until the next group of keys goes down.
public NoteStream Latch()Returns
Examples
// Each note of this tune lasts a quarter, but the latch holds it until the next note starts, so the tune is played legato.
Mini.Notes("c3 ~ e3 ~ g3 ~ e3 ~").ToStream().Latch()
.Play(v => Triangle(v.Frequency).LowPass(2000).Adsr(0.01, 0.2, 0.7, 0.4)) * 0.3
Length(double)
Gives every note the same length, whatever the key does: it ends after beats, held or not.
public NoteStream Length(double beats)Parameters
beatsdouble-
The length, above 0 up to 64 beats.
Returns
Examples
// Each note lasts exactly a quarter of a beat, however long the key is held.
var keys = new NoteInput();
var sound = keys.Stream.Length(0.25)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 g3");
sound * 0.3
Exceptions
- ArgumentOutOfRangeException
-
Outside the range.
Merge(params NoteStream[])
This stream and the others as one. Each keeps its notes; their ids are made distinct so two streams can sound the same pitch at once and each ends its own.
public NoteStream Merge(params NoteStream[] others)Parameters
othersNoteStream[]-
The streams to add, at least one.
Returns
Examples
// A note and the same note an octave down, as one stream.
var keys = new NoteInput();
var sound = keys.Stream.Merge(keys.Stream.Transpose(-12))
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3");
sound * 0.3
Mono(NotePriority)
One note at a time: of the keys held, only the one priority picks sounds, and it changes as keys go down and up.
public NoteStream Mono(NotePriority priority = NotePriority.Last)Parameters
priorityNotePriority-
Which held key wins. Defaults to the last pressed.
Returns
Examples
// Of the keys held, only the lowest sounds.
var keys = new NoteInput();
var sound = keys.Stream.Mono(NotePriority.Lowest)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("e3 c3 g3");
sound * 0.3
Play(Func<Voice, Sound>, int)
Plays the notes of the stream: each start sounds a voice built from voice until its end. Like
Sigil.Sound.NoteInput.Play(System.Func{Sigil.Sound.Voice,Sigil.Sound.Sound},System.Int32), but for notes that have been through operators. Its notes carry no bend, pressure or slide.
public Sound Play(Func<Voice, Sound> voice, int polyphony = 8)Parameters
voiceFunc<Voice, Sound>-
Describes the sound of one note from its Sigil.Sound.Voice signals. Called while the sound is built, not per note.
polyphonyint-
The most notes sounding at once, from 1 to 64. Defaults to 8.
Returns
Examples
// A voice for every note of the stream, here the keys with a fifth added.
var keys = new NoteInput();
var sound = keys.Stream.Harmonize(0, 7)
.Play(v => Triangle(v.Frequency).LowPass(2200).Adsr(0.01, 0.2, 0.6, 0.4), polyphony: 6);
keys.NoteOn("a2 e3");
sound * 0.3
Scan<TState>(TState, Func<TState, NoteEvent, NoteEmitter, TState>)
Carries a state through the stream's events, with a function of your own that sees every start and every end and may start notes. The notes it makes end when the note they came from ends.
public NoteStream Scan<TState>(TState seed, Func<TState, NoteEvent, NoteEmitter, TState> step)Parameters
seedTState-
The state before the first event.
stepFunc<TState, NoteEvent, NoteEmitter, TState>-
Called for each event with the state so far; returns the new state. Add notes to make to
emit(only on a start).
Returns
Type Parameters
TState-
What the function remembers.
Examples
// The second key held gets a fifth above it. The function counts the keys down and starts a note of its own.
var keys = new NoteInput();
var sound = keys.Stream.Scan(0, (down, e, emit) =>
{
if (e.IsOn && down == 1)
emit.Add(e.Pitch + 7, 0.6f);
return e.IsOn ? down + 1 : down - 1;
})
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3 g3");
sound * 0.3
Select(Func<NoteEvent, NoteEvent>)
Changes each note with a function of your own. The function sees the note as it starts and returns the note to play; its
Sigil.Sound.NoteEvent.Id and kind are ignored, so the engine still ends the note when the key comes up, and a result outside
the MIDI range drops it. Everything else about a note is one expression away: n with { Velocity = 0.5f },
n.Transpose(7), n.Snap(scale), n.Detune(20).
public NoteStream Select(Func<NoteEvent, NoteEvent> map)Parameters
mapFunc<NoteEvent, NoteEvent>-
Called once per note, on the audio thread: it must not allocate, lock, or take long.
Returns
Examples
// The note an octave up when it is struck hard, and a fifth below it when it is not.
var keys = new NoteInput();
var sound = keys.Stream.Select(n => n.Velocity > 0.7f ? n.Transpose(12) : n.Transpose(-5))
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.4, 0.2));
keys.NoteOn("c3:0.9 e3:0.4");
sound * 0.3
Remarks
A host that runs scripts it did not write may accept only a body that is an expression over the note and arithmetic. The Sigil Workbench's Graph mode does, and refuses anything that calls other code. A function that throws stops the stream; the host decides what that means.
SelectMany(NoteExpander)
Turns each note into none, one or several with a function of your own: add the notes to make to emit. See Sigil.Sound.NoteStream.Select(System.Func{Sigil.Sound.NoteEvent,Sigil.Sound.NoteEvent}).
public NoteStream SelectMany(NoteExpander expand)Parameters
expandNoteExpander-
Called once per note, on the audio thread. At most Sigil.Sound.NoteEmitter.Capacity notes are taken.
Returns
Examples
// Each note also sounds an octave up, softer. A function of your own that makes two notes from one.
var keys = new NoteInput();
var sound = keys.Stream.SelectMany((n, emit) =>
{
emit.Add(n);
emit.Add(n.Pitch + 12, 0.4f);
})
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3");
sound * 0.3
Strum(double)
Spreads notes that start together (a chord) one after the other, in the order they came, each beats after the
one before. Each keeps its length.
public NoteStream Strum(double beats)Parameters
beatsdouble-
The gap between notes, above 0 up to 1 beat.
Returns
Examples
// A chord played one note at a time, a sixteenth apart, in the order the notes came.
var keys = new NoteInput();
var sound = keys.Stream.Strum(0.25)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3 g3 c4");
sound * 0.3
Exceptions
- ArgumentOutOfRangeException
-
Outside the range.
Transpose(int)
Moves every note by semitones. A note that would leave the MIDI range is dropped.
public NoteStream Transpose(int semitones)Parameters
semitonesint-
How far to move, up (positive) or down.
Returns
Examples
// Every key you press sounds a fifth higher.
var keys = new NoteInput();
var sound = keys.Stream.Transpose(7)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3");
sound * 0.3
Velocity(double)
Plays every note at the same velocity.
public NoteStream Velocity(double velocity)Parameters
velocitydouble-
Above 0 up to 1.
Returns
Examples
// Every note at the same soft velocity, however hard it was played.
var keys = new NoteInput();
var sound = keys.Stream.Velocity(0.4)
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3:1 e3:0.2");
sound * 0.3
Exceptions
- ArgumentOutOfRangeException
-
Not above 0 and at most 1.
Where(Func<NoteEvent, bool>)
Keeps the notes a function of your own says yes to; a note it refuses has no end to send either. See Sigil.Sound.NoteStream.Select(System.Func{Sigil.Sound.NoteEvent,Sigil.Sound.NoteEvent}) for what the function may do.
public NoteStream Where(Func<NoteEvent, bool> keep)Parameters
Returns
Examples
// Only the notes of D dorian, and only a third of them, picked the same way every time.
var keys = new NoteInput();
var dorian = Scale.Parse("d dorian");
var sound = keys.Stream.Where(n => n.InScale(dorian) && n.Chance(0.7))
.Play(v => Square(v.Frequency).LowPass(1500).Adsr(0.005, 0.2, 0.4, 0.2));
keys.NoteOn("d3 e3 f3 g3 a3 b3 c4 f#3");
sound * 0.2
Operators
operator +(NoteStream, NoteStream)
The notes of both streams as one; the same as Sigil.Sound.NoteStream.Merge(Sigil.Sound.NoteStream[]).
public static NoteStream operator +(NoteStream a, NoteStream b)Parameters
aNoteStream-
The first stream.
bNoteStream-
The second stream.
Returns
Examples
// Two streams added: the key itself, and a fifth above it.
var keys = new NoteInput();
var sound = (keys.Stream + keys.Stream.Transpose(7))
.Play(v => Saw(v.Frequency).LowPass(1800).Adsr(0.005, 0.2, 0.5, 0.25));
keys.NoteOn("c3 e3");
sound * 0.3