From a2993502758437f063f18dfb10fe5cd95d519348 Mon Sep 17 00:00:00 2001 From: Paul Kinlan Date: Sat, 13 Jul 2013 15:52:41 +0100 Subject: [PATCH] #7 and #8 - The null sequencer is complete It now has a loop time lasting 4 seconds, with 250ms between each beat in a bar. I believe in musical parlance this is a 4/4 beat. --- Orchestra/sw/hub/config/app.yml | 2 +- .../hub/orchestra/null_sequencer/__init__.py | 16 ++++ .../sw/hub/orchestra/null_sequencer/max.py | 80 +++++++++++++++++ .../sw/hub/orchestra/null_sequencer/notes.py | 89 +++++++++++++++++++ 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 Orchestra/sw/hub/orchestra/null_sequencer/__init__.py create mode 100644 Orchestra/sw/hub/orchestra/null_sequencer/max.py create mode 100644 Orchestra/sw/hub/orchestra/null_sequencer/notes.py diff --git a/Orchestra/sw/hub/config/app.yml b/Orchestra/sw/hub/config/app.yml index b859809..13b6981 100644 --- a/Orchestra/sw/hub/config/app.yml +++ b/Orchestra/sw/hub/config/app.yml @@ -6,7 +6,7 @@ messenger: host: 127.0.0.1 osc: - sequencer: max_sequencer + sequencer: null_sequencer host: 127.0.0.1 tx_port: 7403 rx_port: 7406 diff --git a/Orchestra/sw/hub/orchestra/null_sequencer/__init__.py b/Orchestra/sw/hub/orchestra/null_sequencer/__init__.py new file mode 100644 index 0000000..a2db7a3 --- /dev/null +++ b/Orchestra/sw/hub/orchestra/null_sequencer/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2013 Google Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from max import * +from notes import * diff --git a/Orchestra/sw/hub/orchestra/null_sequencer/max.py b/Orchestra/sw/hub/orchestra/null_sequencer/max.py new file mode 100644 index 0000000..cf96965 --- /dev/null +++ b/Orchestra/sw/hub/orchestra/null_sequencer/max.py @@ -0,0 +1,80 @@ +# +# max.py: send OSC messages to be converted to MIDI messages for robots +# +# Copyright 2013 Google Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from twisted.application import internet, service +from txosc import osc, dispatch, async +import messenger +import notes +import time + +# This is a stub for users that do not have Max or are not running on mac. +# It needs to send a "/loop" OSC command every loop so that the client can sync. +# Normally this is done by Max, but we set up a timmer and do it manually here. + +# CLIENT +# +osc_send_address = None +client = None + +def send_instruments(): + """ + Send all instrument note positions to Max + """ + send("/instruments " + json.dumps(notes.instruments)) + +def send(message): + """ + Send message to Max. + OSC message is path + arguments in one string. + """ + global client + print "SEND: %s" % message + client.send(osc.Message(message), osc_send_address) + +def client_service(address): + """ + Twisted service for outbound OSC messages. + (Weirdly, Twisted has us call UDPServer() to register a client. [sic]) + """ + global client, osc_send_address + osc_send_address = address + client = async.DatagramClientProtocol() + return internet.UDPServer(0, client) + + +# SERVER +# + +def got_loop(): + """ + Max passed a timestamp from last loop start. + Get next n loop_times, send to WS. + """ + global client + loop_time = int(round(time.time() * 1000)) + notes.step_ms = 250 + loop_times = notes.next_loop_times(loop_time) + messenger.websocketclient.broadcast_loop_times(loop_times) + +def server_service(port): + """ + Create Twisted service for OSC server ('receiver'). + Callbacks for inbound messages must be registered here. + """ + + return internet.TimerService(4, got_loop) diff --git a/Orchestra/sw/hub/orchestra/null_sequencer/notes.py b/Orchestra/sw/hub/orchestra/null_sequencer/notes.py new file mode 100644 index 0000000..330f45e --- /dev/null +++ b/Orchestra/sw/hub/orchestra/null_sequencer/notes.py @@ -0,0 +1,89 @@ +# +# notes.py: heavy lifting of note matrices +# +# Copyright 2013 Google Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from time import time +import max + +NUM_STEPS = 16 + +# When we're determining the next_note_play_time for a note, +# factor in how long it'll be before this message hits the client. +# We don't know how long that'll actually be. Fudge it! +LATENCY_FUDGE_FACTOR_MS = 100 + +step_ms = 250 +instruments = [] +last_loop_time = 0 +loop_times = [] + +def set_instruments(all_instruments): + """ + setter + """ + global instruments + instruments = all_instruments + max.send_instruments() + +def next_loop_times(time): + """ + Rebuild upcoming loop_times list based on last loop time + """ + global last_loop_time, loop_times + last_loop_time = time + loop_times = [] + for i in range(10): + loop_times.append(last_loop_time + ((i + 1) * current_loop_ms())) + return loop_times + +def update_instrument(instrument_id, message_type, note): + """ + Update this note within the instrument (creating, moving, or deleting) + """ + global instruments + instrument = instruments[instrument_id] + + if message_type is 'change_note': + for index, instrument_note in enumerate(instrument): + if instrument_note['id'] == note['id']: + instrument[index] = note + elif message_type is 'add_note': + instrument.append(note) + elif message_type is 'remove_note': + for index, instrument_note in enumerate(instrument): + if instrument_note['id'] == note['id']: + instrument.pop(index) + + max.send_instruments() + +def next_note_play_time(note): + """ + DEPRECATED. Just returning current time now, not note time! + Determine next play time (epoch, ms) for this new note. + """ + next_play_time = None + if note.has_key('pos'): + next_play_time = last_loop_time + note['pos'] * step_ms + likely_client_position = int(time() * 1000) + LATENCY_FUDGE_FACTOR_MS + if next_play_time <= likely_client_position: + next_play_time += current_loop_ms() + return next_play_time + +def current_loop_ms(): + """ + Length of one loop based on current tempo + """ + return step_ms * NUM_STEPS