Sequencing with TWNote

I designed TWNote (and its constituent classes, TWPitch and TWDuration) to make working with traditional musical notes in SuperCollider easier. This tutorial will demonstrate how to use TWNote and SuperCollider's Task object to create sequences.

This tutorial assumes that you have basic working knowledge of the SuperCollider language. If you need help, the SuperCollider documentation is a good place to start.

Installing TWSuperColliderExtensions

Follow the TWSuperColliderExtensions installation instructions before continuing with this tutorial.

Introducing TWPitch, TWDuration and TWNote

TWPitch

TWPitch describes a pitch as a combination of a pitch class and octave. The pitch class and octave may be provided or inferred from a MIDI note number:

(
    var pitch1 = TWPitch.newPitch(pitchClass: 9, octave: 5);
    pitch1.pitchClass.postln; // 9
    pitch1.octave.postln; // 5
    pitch1.midiNoteNumber.postln; // 69
    pitch1.frequency.postln; // 440

    var pitch2 = TWPitch.newPitchWithMIDINoteNumber(noteNumber: 69);
    pitch2.pitchClass.postln; // 9
    pitch2.octave.postln; // 5
    pitch2.midiNoteNumber.postln; // 69
    pitch2.frequency.postln; // 440
)

TWPitch calculates frequency based on a reference frequency that defaults to 440Hz. You can change the reference frequency globally for all instances of TWPitch:

TWPitch.setReferenceFrequency(442.0);

If the new reference frequency does not relate to the note A in the 5th octave, you will also need to change the reference pitch class and octave:

// Set the reference frequency to approximately C (pitch class 0) in the 4th octave (slightly flat):
TWPitch.setReferenceFrequency(130.8);
TWPitch.setReferencePitchClass(0);
TWPitch.setReferenceOctave(4);

(
    var pitch = TWPitch.newPitch(pitchClass: 9, octave: 5);
    pitch.pitchClass.postln; // 9
    pitch.octave.postln; // 5
    pitch.midiNoteNumber.postln; // 69
    pitch.frequency.postln; // ~439.957
)

TWPitch can also provide a MIDI-compatible octave number. By default, the MIDI octave is equal to pitch.octave - 1. This is appropriate for most MIDI systems. Some systems (eg. Yamaha) use pitch.octave - 2, which can be obtained by adjusting the MIDI base octave:

(
    var pitch1 = TWPitch.newPitch(pitchClass: 9, octave: 5);
    pitch1.octave.postln; // 5
    pitch1.midiOctave.postln; // 4
)

TWPitch.setMIDIBaseOctave(-2); // Allowable range is -2..0

(
    var pitch2 = TWPitch.newPitch(pitchClass: 9, octave: 5);
    pitch2.octave.postln; // 5
    pitch2.midiOctave.postln; // 3
)

TWDuration

TWDuration describes a note duration as a fraction, in which the denominator represents a division of a typical 4 beat measure (eg. 4 for a quarter note or 8 for an eighth note) and the numerator represents the number of divisions:

var wholeNote = TWDuration.newDuration(numerator: 1, denominator: 1);
var halfNote = TWDuration.newDuration(numerator: 1, denominator: 2);
var quarterNote = TWDuration.newDuration(numerator: 1, denominator: 4);
var eighthNote = TWDuration.newDuration(numerator: 1, denominator: 8);
var sixteenthNote = TWDuration.newDuration(numerator: 1, denominator: 16);
var dottedHalfNote = TWDuration.newDuration(numerator: 3, denominator: 4);
var dottedQuarterNote = TWDuration.newDuration(numerator: 3, denominator: 8);
var eighthNoteTriplet = TWDuration.newDuration(numerator: 1, denominator: 12);
var quarterNoteTriplet = TWDuration.newDuration(numerator: 2, denominator: 12);

The value of a TWDuration can be calculated as a decimal value:

(
    var duration = TWDuration.newDuration(numerator: 1, denominator: 8);
    duration.decimal.postln; // 0.125
)

As a clock value, which can be used with SuperCollider's Task object:

(
    var duration = TWDuration.newDuration(numerator: 1, denominator: 8);
    duration.clock.postln; // 0.5
)

Or as time in seconds or milliseconds. Time calculations are based on a reference tempo, which defaults to 60 beats per minute. You can change the reference tempo globally for all instances of TWDuration:

(
    var duration1 = TWDuration.newDuration(numerator: 1, denominator: 8);
    duration1.seconds.postln; // 0.5
    duration1.milliseconds.postln; // 500
)

TWDuration.setReferenceTempo(120.0);

(
    var duration2 = TWDuration.newDuration(numerator: 1, denominator: 8);
    duration2.seconds.postln; // 0.25
    duration2.milliseconds.postln; // 250
)

TWNote

TWNote describes a note as a combination of a pitch (an instance of TWPitch) and a duration (an instance of TWDuration). TWNote objects also have a boolean property named isRest that indicates whether or not the note should be treated as a rest. (It is the responsibility of the code that interprets the note to handle the rest properly.)

(
    var note = TWNote.newNote;
    note.pitch.postln; // a TWPitch
    note.duration.postln; // a TWDuration
    note.isRest.postln; // false
)

TWNote has several constructors:

// Verbose constructor:

(
    var note1 = TWNote.newNote(
        pitch: TWPitch.newPitch(
            pitchClass: 9,
            octave: 5),
        duration: TWDuration.newDuration(
            numerator: 1,
            denominator: 4),
        isRest: false);

    note1.pitch.pitchClass.postln; // 9
    note1.pitch.octave.postln; // 5
    note1.pitch.midiNoteNumber.postln; // 69
    note1.pitch.frequency.postln; // 440
    note1.duration.decimal.postln; // 0.25
    note1.isRest.postln; // false
)

// Convenience constructors:

(
    var note2 = TWNote.newNoteWithPitchClass(
        pitchClass: 9,
        octave: 5,
        durationNumerator: 1,
        durationDenominator: 4);

    note2.pitch.pitchClass.postln; // 9
    note2.pitch.octave.postln; // 5
    note2.pitch.midiNoteNumber.postln; // 69
    note2.pitch.frequency.postln; // 440
    note2.duration.decimal.postln; // 0.25
    note2.isRest.postln; // false
)

(
    var note3 = TWNote.newNoteWithMIDINoteNumber(
        noteNumber: 69,
        durationNumerator: 1,
        durationDenominator: 4);

    note3.pitch.pitchClass.postln; // 9
    note3.pitch.octave.postln; // 5
    note3.pitch.midiNoteNumber.postln; // 69
    note3.pitch.frequency.postln; // 440
    note3.duration.decimal.postln; // 0.25
    note3.isRest.postln; // false
)

(
    var rest = TWNote.newRest(
        durationNumerator: 1,
        durationDenominator: 4);

    rest.duration.decimal.postln; // 0.25
    rest.isRest.postln; // true
)

Sequencing TWNote Objects With Tasks

SuperCollider's Task object provides a convenient and powerful way to sequence events that works well with TWNote. You can build simple sequence with a list of TWNote objects:

(
    // Create a global clock using the TWDuration reference tempo:
    ~clock = TempoClock.new(TWDuration.referenceTempo / 60.0);

    var sequence = Task({[
        TWNote.newNoteWithPitchClass(pitchClass: 0, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 4, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 5, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 11, octave: 4, durationNumerator: 1, durationDenominator: 4)
        ].do({
            // The .do function receives the note object as its first argument:
            arg note;
            // Print the note's frequency:
            note.pitch.frequency.postln;
            // Wait for the duration of the note's clock value:
            note.duration.clock.wait;
        });
    }, ~clock);

    sequence.play;
)

Now add a simple SynthDef to play each note in the sequence:

// Boot the local server:
Server.default = s = Server.local.boot;

(
    SynthDef(\osc, {
        arg frequency = 440.0, duration = 1.0;
        var attack = 0.05;
        var decay = 0.1;
        var hold = duration - (attack + decay);
        var osc = SinOsc.ar(
            freq: frequency,
            mul: EnvGen.kr(
                envelope: Env(
                    levels: [0.000001, 0.1, 0.1, 0.000001],
                    times: [attack, hold, decay],
                    curve: \exponential
                ),
                doneAction: 2)
        );
        Out.ar([0, 1], osc);
    }).load(s);
)

(
    // Create a global clock using the TWDuration reference tempo:
    ~clock = TempoClock.new(TWDuration.referenceTempo / 60.0);

    var sequence = Task({[
        TWNote.newNoteWithPitchClass(pitchClass: 0, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 4, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 5, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 11, octave: 4, durationNumerator: 1, durationDenominator: 4)
        ].do({
            // The .do function receives the note object as its first argument:
            arg note;
            // Use the time in seconds for the synth duration:
            Synth(\osc, [frequency: note.pitch.frequency, duration: note.duration.seconds]);
            // Wait for the duration of the note's clock value:
            note.duration.clock.wait;
        });
    }, ~clock);

    sequence.play;
)

Polyphonic sequences can be created by combining multiple Task objects. Notice how rests are handled in sequence2:

// Boot the local server:
Server.default = s = Server.local.boot;

(
    SynthDef(\osc, {
        arg frequency = 440.0, duration = 1.0;
        var attack = 0.05;
        var decay = 0.1;
        var hold = duration - (attack + decay);
        var osc = SinOsc.ar(
            freq: frequency,
            mul: EnvGen.kr(
                envelope: Env(
                    levels: [0.000001, 0.1, 0.1, 0.000001],
                    times: [attack, hold, decay],
                    curve: \exponential
                ),
                doneAction: 2)
        );
        Out.ar([0, 1], osc);
    }).load(s);
)

(
    // Create a global clock using the TWDuration reference tempo:
    ~clock = TempoClock.new(TWDuration.referenceTempo / 60.0);

    var sequence1 = Task({[
        TWNote.newNoteWithPitchClass(pitchClass: 0, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 4, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 5, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 11, octave: 4, durationNumerator: 1, durationDenominator: 4)
        ].do({
            // The .do function receives the note object as its first argument:
            arg note;
            // Use the time in seconds for the synth duration:
            Synth(\osc, [frequency: note.pitch.frequency, duration: note.duration.seconds]);
            // Wait for the duration of the note's clock value:
            note.duration.clock.wait;
        });
    }, ~clock);

    var sequence2 = Task({[
        TWNote.newRest(durationNumerator: 2, durationDenominator: 12),
        TWNote.newNoteWithPitchClass(pitchClass: 0, octave: 6, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 11, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 9, octave: 5, durationNumerator: 1, durationDenominator: 4),
        TWNote.newNoteWithPitchClass(pitchClass: 7, octave: 5, durationNumerator: 1, durationDenominator: 12)
        ].do({
            arg note;
            // Do not play the synth during rests, but do wait out the duration:
            if (note.isRest.not, {
                Synth(\osc, [frequency: note.pitch.frequency, duration: note.duration.seconds]);
            });
            note.duration.clock.wait;
        });
    }, ~clock);

    var polySequence = Task({
        sequence1.play;
        sequence2.play;
    }, ~clock);

    polySequence.play;
)

Be sure to check out the full documentation for TWNote, TWPitch and TWDuration. Documentation and help for the SuperCollider language is available at doc.sccode.org.