Lunar Trajectories

Example script (scamp): examples/Composition & form/Larger-scale form/evanstein_lunar_trajectories.py

Download: evanstein_lunar_trajectories.py

Interactive piano script for the first movement of “Lunar Trajectories”. The notes list below is a list of all the notes played by the middle arpeggio part in the first movement of the Moonlight Sonata, in order. If a pitch is in that list, then when it is depressed, the piano reacts by playing whichever pitches follow that pitch the first time it occurs in the list. If a pitch is not in the list, then instead we look for the same pitch class but in a different octave, and follow it up by the notes that follow that pitch (transposed back up or down by however many octaves). Every pitch class appears in the first movement, so we don’t have the issue of searching for a pitch class that doesn’t occur.

Topics: Composition & form › Larger-scale form

from scamp import *


# the input and output device can be the same or different
# for instance, you might want to take midi in from the keyboard,
# but send any note playback messages to a softsynth like pianoteq
MIDI_INPUT_DEVICE = "E-MU"
MIDI_OUTPUT_DEVICE = "E-MU"

# --------------------------------------- Setup -------------------------------------------
# Here we pre-process the list of notes, so that we know, for every pitch that the performer
# might play, where in that list of notes it first occurs, and what(if any) transposition
# we need to apply. We also set up the rate at which notes playback throughout the keyboard


# create a mapping from midi pitch to the rate of note playback (rate is in notes/second)
gesture_rate_by_pitch = {
    p: 3 * 2 ** ((p - 60) / 12)
    for p in range(21, 109)
}


# a list of all the pitches from the middle arpeggio part
# in the first movement of the Moonlight Sonata
notes = [56, 61, 64, 56, 61, 64, 56, 61, 64, 56, 61, 64, 56, 61, 64, 56, 61, 64, 56, 61, 64,
         56, 61, 64, 57, 61, 64, 57, 61, 64, 57, 62, 66, 57, 62, 66, 56, 60, 66, 56, 61, 64,
         56, 61, 63, 54, 60, 63, 52, 56, 61, 56, 61, 64, 56, 61, 64, 56, 61, 64, 56, 63, 66,
         56, 63, 66, 56, 63, 66, 56, 63, 66, 56, 61, 64, 56, 61, 64, 57, 61, 66, 57, 61, 66,
         56, 59, 64, 56, 59, 64, 57, 59, 63, 57, 59, 63, 56, 59, 64, 56, 59, 64, 56, 59, 64,
         56, 59, 64, 55, 59, 64, 55, 59, 64, 55, 59, 64, 55, 59, 64, 55, 59, 65, 55, 59, 65,
         55, 59, 65, 55, 59, 65, 55, 60, 64, 55, 59, 64, 55, 61, 64, 54, 61, 64, 54, 59, 62,
         54, 59, 62, 55, 59, 61, 52, 59, 61, 54, 59, 62, 54, 59, 62, 54, 58, 61, 54, 58, 61,
         59, 62, 66, 59, 62, 66, 59, 63, 66, 59, 63, 66, 59, 64, 67, 59, 64, 67, 59, 64, 67,
         59, 64, 67, 59, 63, 66, 59, 63, 66, 59, 63, 66, 59, 63, 66, 59, 64, 67, 59, 64, 67,
         59, 64, 67, 59, 64, 67, 59, 63, 66, 59, 63, 66, 59, 62, 65, 59, 62, 65, 59, 61, 68,
         59, 61, 68, 57, 61, 66, 57, 61, 66, 55, 59, 62, 55, 59, 62, 54, 57, 63, 54, 57, 63,
         49, 54, 57, 49, 54, 57, 49, 54, 56, 49, 53, 56, 54, 57, 61, 57, 61, 66, 61, 66, 69,
         61, 66, 69, 61, 68, 71, 61, 68, 71, 61, 68, 71, 61, 68, 71, 61, 66, 69, 61, 66, 69,
         60, 66, 69, 61, 66, 69, 63, 66, 68, 63, 66, 68, 63, 66, 68, 63, 66, 68, 64, 68, 73,
         64, 68, 73, 63, 66, 69, 61, 64, 70, 72, 60, 63, 68, 60, 63, 69, 60, 63, 66, 60, 63,
         60, 63, 56, 60, 63, 57, 60, 63, 54, 60, 63, 52, 64, 68, 73, 64, 68, 76, 64, 68, 73,
         64, 68, 52, 56, 61, 52, 56, 64, 52, 56, 61, 52, 56, 51, 57, 54, 60, 57, 63, 60, 66,
         63, 69, 66, 72, 52, 61, 56, 64, 61, 68, 64, 73, 68, 76, 73, 68, 61, 67, 64, 70, 67,
         73, 70, 76, 73, 79, 76, 82, 66, 72, 69, 75, 72, 78, 75, 81, 78, 84, 81, 87, 84, 78,
         81, 75, 78, 72, 75, 69, 72, 66, 69, 63, 66, 60, 63, 57, 60, 54, 57, 51, 54, 49, 54,
         57, 48, 54, 56, 57, 56, 54, 51, 54, 57, 49, 54, 57, 48, 54, 56, 57, 56, 54, 50, 54,
         57, 49, 54, 57, 48, 54, 56, 57, 56, 54, 49, 52, 61, 49, 52, 61, 51, 57, 61, 51, 57,
         61, 51, 56, 60, 51, 54, 60, 52, 56, 61, 56, 61, 64, 56, 61, 64, 56, 61, 64, 56, 63,
         66, 56, 63, 66, 56, 63, 66, 56, 63, 66, 56, 61, 64, 56, 61, 64, 57, 61, 66, 57, 61,
         66, 56, 59, 64, 56, 59, 64, 57, 59, 63, 57, 59, 63, 56, 59, 64, 59, 64, 68, 59, 64,
         68, 59, 64, 68, 59, 66, 69, 59, 66, 69, 59, 66, 69, 59, 66, 69, 59, 64, 68, 59, 64,
         68, 60, 66, 68, 61, 64, 68, 63, 66, 68, 63, 66, 68, 64, 68, 73, 64, 68, 73, 62, 66,
         69, 62, 66, 69, 60, 66, 68, 60, 66, 68, 61, 64, 68, 61, 64, 68, 61, 65, 68, 61, 65,
         68, 61, 66, 69, 61, 66, 69, 61, 66, 69, 61, 66, 69, 61, 65, 68, 61, 65, 68, 61, 65,
         68, 61, 65, 68, 61, 66, 69, 61, 66, 69, 61, 66, 69, 61, 66, 69, 61, 65, 68, 61, 65,
         68, 61, 66, 69, 61, 66, 69, 59, 66, 69, 59, 66, 69, 59, 66, 69, 59, 64, 68, 57, 64,
         68, 57, 63, 66, 56, 63, 66, 56, 61, 64, 54, 61, 63, 54, 61, 63, 56, 61, 63, 57, 61,
         63, 56, 61, 64, 56, 61, 64, 54, 60, 63, 54, 60, 63, 52, 56, 61, 56, 61, 64, 56, 61,
         64, 56, 61, 64, 56, 63, 66, 56, 63, 66, 56, 63, 66, 56, 63, 66, 56, 64, 61, 68, 64,
         73, 68, 76, 73, 80, 76, 73, 72, 75, 69, 72, 66, 69, 63, 66, 57, 60, 56, 54, 52, 61,
         64, 61, 68, 64, 73, 68, 76, 73, 80, 76, 73, 72, 75, 69, 72, 66, 69, 63, 66, 57, 60,
         56, 54, 52, 56, 61, 64, 61, 56, 49, 52, 56, 61, 56, 52, 44, 49, 52, 56, 52, 49, 44,
         49, 44, 40, 44, 40, 37]


# dictionary which will map each pitch played to the index within the arpeggio notes
pitch_to_note_index = {}
# dictionary which will map each pitch played to the extra transposition needed
pitch_to_gesture_transposition = {}


# this code populates the pitch_to_note_index and pitch_to_gesture_transposition dictionaries
# so that we know, for each pitch that might be played, where to find that in the notes list
# and what if any transposition to apply.
represented_notes = set(notes)
for pitch in range(21, 108):
    transpose = 0

    if pitch not in represented_notes:
        closest_of_same_pc = None
        min_distance = float("inf")

        for p in represented_notes:
            if p % 12 == pitch % 12 and abs(p - pitch) < min_distance:
                closest_of_same_pc = p
                min_distance = abs(p - pitch)

        transpose = pitch - closest_of_same_pc
        search_pitch = closest_of_same_pc
    else:
        search_pitch = pitch

    search_start = 0
    try:
        while notes.index(search_pitch, search_start) in pitch_to_note_index.values():
            search_start = notes.index(search_pitch, search_start) + 1
    except ValueError:
        search_start -= 1

    pitch_to_note_index[pitch] = notes.index(search_pitch, search_start)
    pitch_to_gesture_transposition[pitch] = transpose


# -------------------------------------- The main script ------------------------------------------

s = Session()

# uncomment these lines to print out the available MIDI output and input devices
# s.print_available_midi_input_devices()
# s.print_available_midi_output_devices()

# this instrument will playback the generated MIDI notes
piano = s.new_midi_part("piano", MIDI_OUTPUT_DEVICE, num_channels=1)


# used to keep track of which pitches are being held and therefore currently have gestures running
gestures_running = {}


def play_gesture(pitch, volume):
    # this is the gesture that is forked every time a note with the given pitch and volume is depressed
    gestures_running[pitch] = True
    # all notes play back for one "beat", but we turn up or down the speed of this gesture's clock
    # depending on which pitch was played
    current_clock().rate = gesture_rate_by_pitch[pitch]

    next_pitches = notes[pitch_to_note_index[pitch]:]
    wait(1)
    for p in next_pitches:
        if not gestures_running[pitch]:
            # if gestures_running[pitch] is now false, it means we lifted the key, so break out of the gesture
            break
        # play the next note of the gesture
        piano.play_note(p + pitch_to_gesture_transposition[pitch], volume, 1)
        # each note fades out relative to the previous one
        volume *= 0.9


def midi_callback(midi_message):
    # function that handles any incoming midi messages from the keyboard
    code, pitch, volume = midi_message
    if volume > 0 and code == 144:
        # note on message causes us to fork a new moonlight sonata gesture at the given pitch and volume
        fork(play_gesture, args=(pitch, volume / 127))
    elif volume == 0 and code == 144 or code == 143 or code == 128:
        # note off message (or note on message with 0 velocity, which is sometimes how it's implemented)
        # we use it to kill the gesture running for the note at the given pitch
        gestures_running[pitch] = False
    elif code == 176 and pitch == 64:
        # pedal change messages just get passed straight along to the MIDI_OUTPUT_DEVICE
        # (with some warping of the values)
        piano.send_midi_cc(64, (volume/127) ** 0.3)


# have the session forward any incoming midi messages
s.register_midi_listener(MIDI_INPUT_DEVICE, midi_callback)
# since everything is driven by MIDI input, we ask the session to wait forever
# so that the program doesn't just end immediate.y
s.wait_forever()