Skip to content
This repository has been archived by the owner on Apr 25, 2018. It is now read-only.

Commit

Permalink
#7 and #8 - The null sequencer is complete
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
PaulKinlan committed Jul 13, 2013
1 parent bc9e3f7 commit a299350
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Orchestra/sw/hub/config/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions Orchestra/sw/hub/orchestra/null_sequencer/__init__.py
Original file line number Diff line number Diff line change
@@ -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 *
80 changes: 80 additions & 0 deletions Orchestra/sw/hub/orchestra/null_sequencer/max.py
Original file line number Diff line number Diff line change
@@ -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)
89 changes: 89 additions & 0 deletions Orchestra/sw/hub/orchestra/null_sequencer/notes.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a299350

Please sign in to comment.