# 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])