:orphan: Barlicity ========= *Example script* (scamp_extensions): `examples/Composition & form/Larger-scale form/evanstein_barlicity.py `__ *Download:* :download:`evanstein_barlicity.py ` **Requires the** ``scamp_extensions`` **package** (``pip install scamp_extensions``). A large interactive piece built on harmonicity and indispensability (barlicity extension), with multidimensional-scaling visualization in Qt. This was the initial script for the piece, which ultimately became the notated work for piano and electronics that you can view here: https://www.youtube.com/watch?v=xMpET9KKOrw *Topics:* :doc:`Interactivity & visualization › Visualization `, :doc:`Composition & form › Larger-scale form ` .. raw:: html .. code-block:: python from scamp import * import sys from sklearn import manifold import numpy as np from scamp_extensions.composers.barlicity import harmonicity, _gaussian_discount, get_indispensability_array from scamp_extensions.pitch.utilities import midi_to_hertz, hertz_to_midi import random from PyQt5 import QtCore, QtWidgets, Qt import math from fractions import Fraction ################################################################################################################## # Setting up some of the guiding variables ################################################################################################################## SCAN_PERIOD, WIDTH_AVERAGE, WIDTH_VARIATION, WIDTH_VARIATION_PERIOD, WIDTH_START_PHASE = \ 160, 10.0, 5.0, 20, 3*math.pi / 2 PIANO_TEMPO = 140 HARPSICHORD_TEMPO = 840 piano_distillation_timeline = Envelope((0, 1, 0), (120, 120)) ################################################################################################################## # Set up the scale pitches ################################################################################################################## root_frequency = midi_to_hertz(31) rationalized_bark_intervals = [Fraction(1, 1), Fraction(2, 1), Fraction(3, 1), Fraction(4, 1), Fraction(5, 1), Fraction(6, 1), Fraction(64, 9), Fraction(25, 3), Fraction(28, 3), Fraction(32, 3), Fraction(12, 1), Fraction(64, 5), Fraction(128, 9), Fraction(63, 4), Fraction(50, 3), Fraction(18, 1), Fraction(20, 1), Fraction(64, 3), Fraction(45, 2), Fraction(24, 1)] piano_scale = [hertz_to_midi(root_frequency * ratio) for ratio in rationalized_bark_intervals] harpsichord_scale = [x - 24 for x in piano_scale[11:]] + [x - 12 for x in piano_scale[11:]] + piano_scale[11:] ################################################################################################################## # Do the multidimensional scaling ################################################################################################################## mds = manifold.MDS(n_components=2, dissimilarity="precomputed", random_state=152) harmonic_distances = np.array([ [abs(1/harmonicity((top / bottom).numerator, (top / bottom).denominator)) for bottom in rationalized_bark_intervals] for top in rationalized_bark_intervals ]) mds_points = mds.fit(harmonic_distances).embedding_ # scale those mds_points into the range 0-1000 in x and y coordinates for drawing point_range = min(x[0] for x in mds_points), max(x[0] for x in mds_points), \ min(x[1] for x in mds_points), max(x[1] for x in mds_points) scale_factor = 1000 / max(point_range[1] - point_range[0], point_range[3] - point_range[2]) mds_points *= scale_factor mds_points += 500 ################################################################################################################## # Main QT Class ################################################################################################################## class Barlicity(QtWidgets.QMainWindow): def __init__(self, points): super(Barlicity, self).__init__() # set up the scene scene = QtWidgets.QGraphicsScene(self) view = QtWidgets.QGraphicsView(scene) self.setCentralWidget(view) window_size = int(QtWidgets.QDesktopWidget().availableGeometry(self).size().height() * 0.8) self.resize(Qt.QSize(window_size, window_size)) view.setSceneRect(QtCore.QRectF(0, 0, 1000, 1000)) view.scale(window_size/1100, window_size/1100) # create and position the scanner circle self.circle = QtWidgets.QGraphicsEllipseItem(QtCore.QRectF(-50, -50, 100, 100)) self.circle.setOpacity(0.6) self.circle.setBrush(Qt.QColor(0, 100, 255)) # these two variables are set from the scamp threads, and then they are read during the "do_frame" method (which # operates in Qt land) and used to actually position the circle. GUI actions can only be on the main QT thread. self.circle_center = (500, 500) self.circle_width = scale_factor * (WIDTH_AVERAGE + WIDTH_VARIATION * math.sin(WIDTH_START_PHASE)) # Having defined them, since we're on the Qt thread right now, we position the circle self.circle.setPos(*self.circle_center) self.circle.setRect(-self.circle_width / 2, -self.circle_width / 2, self.circle_width, self.circle_width) scene.addItem(self.circle) # the scanner moves around between the different points; this keeps track of where it is self.scanner_index = 0 self.set_scanner_position(0, 0) view.setMouseTracking(True) def mme(evt): self.start() view.mousePressEvent = mme # create all of the points self.points_graphics = [] for i, point in enumerate(points): self.points_graphics.append( scene.addEllipse(Qt.QRectF(point[0] - 5, point[1] - 5, 10, 10), brush=Qt.QColor(0, 0, 0)) ) text = scene.addText(str(i)) text.setPos(point[0] - 30 * len(str(i)) / 2, point[1]) self.contained_points = [] # this checks which points are currently in the scanner and puts their indices in self.contained_points self.check_contained_points() # set up the Qt repaint to happen every 10 milliseconds by calling self.do_frame self.repaint_timer = QtCore.QTimer(self) self.repaint_timer.timeout.connect(self.do_frame) self.repaint_timer.start(10) self.repaint() # These scamp objects are defined when the window is shown self.session = self.piano = self.harpsichord = None def showEvent(self, a0): # when the window is shown, we set up scamp super().showEvent(a0) def start(self): self.session = Session().run_as_server() self.harpsichord = self.session.new_part("harpsichord") self.piano = self.session.new_part("piano") self.session.fork(self.run_scanner, name="SCANNER_CLOCK") piano_clock = self.session.fork(self.piano_part, initial_tempo=PIANO_TEMPO, name="PIANO_CLOCK") self.session.fork(self.harpsichord_part, initial_tempo=HARPSICHORD_TEMPO, name="PIANO_CLOCK") self.session.start_transcribing(clock=piano_clock) def closeEvent(self, a0): # when the window is closed, we stop transcribing and create a score super().closeEvent(a0) # the session only exists if the user clicked to start it if self.session is not None: self.session.kill() # self.session.stop_transcribing().to_score( # time_signature="3/4", title="Barlicity (raw)", composer="Marc Evanstein").show_xml() def piano_part(self): piano_indispensabilities = get_indispensability_array(((3, 2), 3, 2), normalize=True) last_piano_note_pool = None self.piano.send_midi_cc(64, 1.0) while True: which_beat = int(round((get_beat() * 2) % len(piano_indispensabilities))) this_indispensability = piano_indispensabilities[which_beat] note_pool = [piano_scale[i] for i in self.contained_points] if last_piano_note_pool != note_pool: self.piano.send_midi_cc(64, 0.0) self.piano.send_midi_cc(64, 1.0) last_piano_note_pool = note_pool syncopation_prob = 1 - ((self.circle_width / scale_factor - WIDTH_AVERAGE) / (2 * WIDTH_VARIATION) + 0.5) piano_distillation_factor = piano_distillation_timeline.value_at(get_time()) # spensability is either indispensability or dispensability, depending on whether it's syncopated spensability = this_indispensability if random.random() < syncopation_prob else 1 - this_indispensability if len(note_pool) > 0 and spensability >= piano_distillation_factor: # distillation allows only the most "spensible" beats to play, and to play with bigger chords # at distillation_factor 0, the number of notes should be 1, # at distillation_factor 1 and spensibility 1, it should by all the notes num_notes = int(1 + round(piano_distillation_factor * spensability * (len(note_pool) - 1))) first_note_index = int(0.999 * spensability * (len(note_pool) - num_notes + 1)) volume = 0.4 + 0.5 * this_indispensability self.piano.play_chord(note_pool[first_note_index: first_note_index + num_notes], volume, 0.5) else: wait(0.5) def harpsichord_part(self): harpsichord_indispensabilities = get_indispensability_array((2, 3, (3, 2)), normalize=True) running = False scale_index = None direction = None octave_transposition = 0 while True: runniness = 0.02 + 0.98 * piano_distillation_timeline.value_at(get_time()) which_beat = int(round((get_beat()) % len(harpsichord_indispensabilities))) this_indispensability = harpsichord_indispensabilities[which_beat] if not running: # if not running, a high runniness and a high indispensibility will tend to start a run if random.random() < runniness * this_indispensability: running = True octave_transposition = Barlicity.get_an_octave_transposition(get_time()) # play the first note "on the beat" scale_index = random.randrange(len(harpsichord_scale) // 3) + len(harpsichord_scale) // 3 direction = random.choice([-1, 1]) self.harpsichord.play_note(harpsichord_scale[scale_index] + octave_transposition, 0.9, 1.0) else: wait(1) else: # play the last note "on the beat" scale_index += direction self.harpsichord.play_note(harpsichord_scale[scale_index] + octave_transposition, 0.9, 1.0) if random.random() * 1.5 < this_indispensability or \ scale_index == 0 or scale_index == len(harpsichord_scale) - 1: # change run direction if on an important beat or at the top or bottom direction *= -1 # if running, a low runniness and a high indispensibility will tend to stop a run if random.random() < (1 - runniness) * this_indispensability: running = False @staticmethod def get_an_octave_transposition(time_passed): most_octaves_transposed = min(int((time_passed / 140.0) * 2), 2) transposition = random.choice([-1, 1]) * random.choice( [max(most_octaves_transposed - 1, 0), most_octaves_transposed]) return transposition * 12 def set_scanner_position(self, t, dt): self.circle_width = scale_factor * ( WIDTH_AVERAGE + WIDTH_VARIATION * math.sin(2 * math.pi * t / WIDTH_VARIATION_PERIOD + WIDTH_START_PHASE) ) total_multiplier = 0 new_location = np.array([0.0, 0.0]) for i, point in enumerate(mds_points): index_distance = min((i - self.scanner_index) % len(mds_points), (self.scanner_index - i) % len(mds_points)) index_discount = _gaussian_discount(index_distance, 0, 0.45) total_multiplier += index_discount new_location[0] += point[0] * index_discount new_location[1] += point[1] * index_discount new_location /= total_multiplier scanner_location = new_location self.circle_center = scanner_location self.scanner_index = (self.scanner_index + dt * len(mds_points) / SCAN_PERIOD) % len(mds_points) def run_scanner(self): while True: wait(0.05) self.set_scanner_position(self.session.time, 0.05) def do_frame(self): self.circle.setPos(*self.circle_center) self.circle.setRect(-self.circle_width/2, -self.circle_width/2, self.circle_width, self.circle_width) self.check_contained_points() super().repaint() def check_contained_points(self): contained_points = [] for i, point in enumerate(self.points_graphics): if self.circle.collidesWithItem(point): contained_points.append(i) self.contained_points = contained_points app = QtWidgets.QApplication(sys.argv) w = Barlicity(mds_points) w.show() app.exec_()