# play_song.py
#
# Simplistic method to play a song (=sequence of sessions, each of certain pattern length)
# on a novation circuit by python script. I use it on Linux, may work on other OS.
#
# Background: The circuit lacks a song mode / session chaining (WTF!?).
# One either records sessions into a DAW or just plays a sequence of them by
# manual switching.
#
# This script plays sessions in an order and length given.
#
# Before starting the script:
# 1. Set your values below for bpm, portname and sessions.
# 2. Manually press play on the circuit (this script does not start/stop the device).
#
# Update 1:
#
# After updating the firmware, now its different: If an immediate session change
# is applied, the device seems to continue in the new session at the relative position
# in a pattern it left in the previous session. To work around it
# (this is kind of absurd but so is the presumed bug):
# 1. Set your values below for bpm, portname and sessions.
# 2. Select a session on the device which is NOT the first one in your song.
# 3. Stop the device.
# 4. Start the script.
# 5. Press play on the device within some tenth of seconds after step 4.
# To complicate the matter, if you are too fast in pressing play the script wont "fire".
#
# Released to public domain.
#
# Oliver Benedens, 16.7.2017
#
import mido
import rtmidi
import time
# SET TO YOUR VALUES BELOW
# assumption: bpm constant throughout song
bpm = 120
# to get the name of the port on your system:
# mido.get_output_names()
# On my system I got: [u'Circuit:Circuit MIDI 1 24:0', u'Midi Through:Midi Through Port-0 14:0']
portname = 'Circuit:Circuit MIDI 1 24:0'
# the order of sessions to play.
# Each session is tuple (S,L).
# S: session number 0..31
# L: pattern length to play (>=1)
# Below example plays session 30 for a length of 8 patterns, then
# session 29 for a length of 8 patterns, then two times
# session 28 for 8 patterns, then session 28 for 2 patterns and
# finally session 30 for 8 patterns
sessions=[[30,8],[29,8],[28,8],[28,8],[28,2],[30,8]]
# SET TO YOUR VALUES ABOVE
# we schedule a new session some time before the old sessions ends.
# Do not set it to zero/a low value as we need to compensate for
# timing inaccuracies and latencies.
scheduleSessionBeforeTimeInS = 0.5 # in seconds
outport = mido.open_output(portname)
def pattern_length_to_seconds(length):
seconds = length / (bpm/240.)
return seconds
def set_session_scheduled(sessionNumber):
print "set session (scheduled)",sessionNumber
msg = mido.Message('program_change', channel=15, program=64+sessionNumber)
outport.send(msg)
return
def set_session_immediate(sessionNumber):
print "set session (immediate)", sessionNumber
msg = mido.Message('program_change', channel=15, program=sessionNumber)
outport.send(msg)
return
# wait until given time
def wait_until_time(future):
now = time.time()
print "wait",future-now, "seconds"
while time.time() < future:
pass
return
# start the first session immediately, the following sessions play scheduled
print "starting song..."
startTimeInS = time.time()
set_session_immediate(sessions[0][0])
futureTimeInS = startTimeInS + (pattern_length_to_seconds(sessions[0][1]) - scheduleSessionBeforeTimeInS)
numSessions = len(sessions)
for i in range(1,numSessions):
# wait until its time to schedule the next session
wait_until_time(futureTimeInS)
set_session_scheduled(sessions[i][0])
futureTimeInS += pattern_length_to_seconds(sessions[i][1])