Skip to content
This repository was archived by the owner on Nov 23, 2023. It is now read-only.

Commit 6d8783e

Browse files
iipyper, example structure
1 parent 2896db4 commit 6d8783e

12 files changed

Lines changed: 620 additions & 160 deletions

File tree

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ dependencies:
1616
- pip
1717
- pip:
1818
- mido
19+
- python-rtmidi
1920
- python-osc

examples/iipyper/iipyper-test.scd

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
(
2+
MIDIIn.connectAll;
3+
b = NetAddr.new("127.0.0.1", 9999);
4+
Server.default.options.inDevice_("Built-in Microph");
5+
Server.default.options.outDevice_("Built-in Output");
6+
// Server.default.options.inDevice_("mic-buds");
7+
// Server.default.options.outDevice_("mic-buds");
8+
// s.boot;
9+
)
10+
11+
thisProcess.openUDPPort(57121);
12+
13+
14+
(
15+
// MIDI from controller
16+
MIDIdef.noteOn(\input, {
17+
arg val, num, chan, src;
18+
t = Process.elapsedTime;
19+
[\input, num, val, chan, src].postln;
20+
b.sendMsg("/test", num, val);
21+
b.sendMsg("/keyword_example", \arg1, num, \arg2, val);
22+
b.sendMsg("/math/add", num, val);
23+
b.sendMsg("/math/mul", num, val);
24+
});
25+
26+
// OSC return from python
27+
OSCdef(\return, {
28+
arg msg, time, addr, recvPort;
29+
[\return, Process.elapsedTime-t, msg].postln;
30+
31+
}, "/test", nil);
32+
33+
OSCdef.newMatching(\add, {
34+
arg msg, time, addr, recvPort;
35+
[\add, Process.elapsedTime-t, msg].postln;
36+
}, "/math/add", nil);
37+
38+
OSCdef.newMatching(\mul, {
39+
arg msg, time, addr, recvPort;
40+
[\mul, Process.elapsedTime-t, msg].postln;
41+
}, "/math/mul", nil);
42+
43+
OSCdef(\default_send_test, {
44+
arg msg, time, addr, recvPort;
45+
[\default_send_test, msg].postln;
46+
}, "/default_send_test", nil);
47+
48+
OSCdef(\other_send_test, {
49+
arg msg, time, addr, recvPort;
50+
[\other_send_test, msg].postln;
51+
}, "/other_send_test", nil);
52+
)
53+
54+
OSCFunc.trace(false)
55+
56+
// nil.postln
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from iipyper import OSC, MIDI, repeat, run
2+
3+
def main(osc_host='127.0.0.1', osc_port=9999, loop_time=1, loop_msg='hello'):
4+
# loop API:
5+
# use the @repeat decorator to define functions which run every n seconds
6+
@repeat(loop_time)
7+
def _():
8+
print(f'looping every {loop_time} sec: "{loop_msg}"')
9+
10+
@repeat(1.5)
11+
def _():
12+
print('looping every 1.5 sec')
13+
14+
# midi = MIDI()
15+
# # MIDI API:
16+
# # midi.note_on, midi.cc, etc
17+
18+
# # use as a decorator to make a midi handler
19+
# @midi.handle.note_on
20+
# def _(pitch, velocity, channel):
21+
# print(pitch, velocity, channel)
22+
23+
# # above is kind of an abuse of python syntax, could do this instead:
24+
# # (which would allow filtering on port, even note)
25+
# @midi.handle.note_on(pitch=range(1,128), channel=0, port='')
26+
# def _(pitch, velocity, channel):
27+
# print(pitch, velocity, channel)
28+
29+
# use as a function to send MIDI
30+
31+
# @repeat(1)
32+
# def step():
33+
# midi.note_on(pitch=60, velocity=100, channel=0)
34+
# midi.cc(number=0, value=127, channel=1)
35+
36+
37+
# make an OSC object
38+
osc = OSC(osc_host, osc_port)
39+
# # OSC API:
40+
# # osc.args, osc.kwargs
41+
42+
# # use as a decorator to make an osc handler
43+
44+
# # positional arguments
45+
# # osc route is taken from function name
46+
@osc.args
47+
def test(address, x, y): # route is /test
48+
print(address, x, y)
49+
# if a value (besides None) is returned,
50+
# it will be sent back as an OSC message to the sender
51+
# with the route given as the first element:
52+
return '/test', x-y, x+y
53+
54+
# named arguments as key, value pairs in OSC
55+
# e.g. an OSC message ["/keyword_example", "arg2", 3]
56+
# would print "/keyword_example 0 3 99"
57+
@osc.kwargs
58+
def keyword_example(address, arg1=0, arg2=1, arg3=99):
59+
print(address, arg1, arg2, arg3)
60+
# no return value: does not send OSC back
61+
62+
# can also give the route explictly to the decorator,
63+
# supporting wildcards
64+
@osc.args('/math/*')
65+
def _(address, a, b):
66+
print(address, a, b)
67+
op = address.split('/')[-1]
68+
if op=='add':
69+
return address, a + b
70+
if op=='mul':
71+
return address, a * b
72+
73+
# OSC clients can be created explcitly and given names:
74+
osc.create_client('supercollider', port=57120) # uses same host as server
75+
# but clients will also be created automatically when possible
76+
77+
# # to send osc:
78+
osc.send('/default_send_test', 0) # send to default (first created) client
79+
80+
# # send with client in the route:
81+
osc.send('127.0.0.1:57120/other_send_test', 1)
82+
83+
# # send to named client:
84+
osc.create_client('supercollider2', port=57121)
85+
osc.send('/other_send_test', 2, client='supercollider2')
86+
# # alternate send syntax:
87+
osc('supercollider2', '/other_send_test', 3)
88+
89+
# it may be possible to have async/threaded option for both OSC and MIDI?
90+
# MIDI is threaded and OSC is async
91+
# but MIDI could be enqueued and handled in the loop,
92+
# OSC could launch the threading server?
93+
94+
if __name__=='__main__':
95+
run(main)
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
(
2+
~gui = false;
23
MIDIIn.connectAll;
34
b = NetAddr.new("127.0.0.1", 9999);
4-
// Server.default.options.inDevice_("Built-in Microph");
5-
// Server.default.options.outDevice_("Built-in Output");
6-
Server.default.options.inDevice_("mic-buds");
7-
Server.default.options.outDevice_("mic-buds");
5+
Server.default.options.inDevice_("Built-in Microph");
6+
Server.default.options.outDevice_("Built-in Output");
7+
// Server.default.options.inDevice_("mic-buds");
8+
// Server.default.options.outDevice_("mic-buds");
89
s.boot;
9-
k = MIDIKeyboard.new(bounds: Rect(0, 0, 500, 100), octaves:11, startnote:0)
10+
~gui.if{
11+
k = MIDIKeyboard.new(bounds: Rect(0, 0, 500, 100), octaves:11, startnote:0)
12+
};
1013
)
1114

1215
(
@@ -42,7 +45,7 @@ b.sendMsg("/predictor/reset");
4245
MIDIdef.noteOn(\input, {
4346
arg val, num, chan, src;
4447
var t2 = Process.elapsedTime;
45-
var dt = t2-t; //time since last note
48+
var dt = t2-(t?t2); //time since last note
4649

4750
// cancel any pending predictions
4851
SystemClock.clear;
@@ -87,15 +90,18 @@ OSCdef(\return, {
8790
b.sendMsg("/predictor/reset");
8891
//release the last note
8992
y.release(1.0);
93+
// unset time so next note will have dt=0
94+
t = nil;
9095
\reset.postln
9196
}{
92-
// cancel any pending predictions (there shouldn't be any, but might
97+
// cancel any pending predictions
98+
// (there shouldn't be any, but might
9399
// be if there was a lot of fast MIDI input)
94100
SystemClock.clear;
95101
// feed model its own prediction as input
96102
b.sendMsg("/predictor/predict", num, dt);
97103
// release the previous note
98-
(dt<1e-2).if{
104+
(dt<3e-2).if{
99105
// if the time delay is very small, slowly release to play a chord
100106
y.release(1.0)
101107
}{
@@ -109,8 +115,10 @@ OSCdef(\return, {
109115
// mark the time of current note
110116
t = Process.elapsedTime;
111117
// crudely draw note on piano GUI
112-
AppClock.sched(0,{k.keyDown(num)});
113-
AppClock.sched(0.2,{k.keyUp(num)});
118+
~gui.if{
119+
AppClock.sched(0,{k.keyDown(num)});
120+
AppClock.sched(0.2,{k.keyUp(num)});
121+
}
114122
}
115123
})};
116124

examples/notepredictor/server.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Authors:
3+
Victor Shepardson
4+
Jack Armitage
5+
Intelligent Instruments Lab 2022
6+
"""
7+
# TODO: convert to iipyper
8+
9+
# import asyncio
10+
# import fire
11+
from notepredictor import NotePredictor
12+
# from osc import OSC
13+
14+
from iipyper import OSC, run
15+
16+
# def predictor_handler(address, *args):
17+
# """
18+
# Handle OSC messages to Predictor
19+
# """
20+
# address = address.split("/")
21+
22+
# if(address[2] == "load"):
23+
# print(f"/load {args}")
24+
# global predictor
25+
# predictor = NotePredictor.from_checkpoint(*args)
26+
# predictor.eval()
27+
28+
# elif(address[2] == "predict"):
29+
# print(f"/predict {args}")
30+
# r = predictor.predict(*args)
31+
# msg = (r['pitch'], r['time'])
32+
# # print(msg)
33+
# osc.send_message('/prediction', msg)
34+
35+
# elif(address[2] == "reset"):
36+
# print(f"/reset {args}")
37+
# predictor.reset(*args)
38+
39+
# else:
40+
# print(f"PitchPredictor: Unrecognised OSC {address} with {args}")
41+
42+
# async def loop():
43+
# """
44+
# Separate async loop.
45+
# """
46+
# i = 0
47+
# while True:
48+
# i += 1
49+
# # osc.send_message("/hello", i)
50+
# await asyncio.sleep(1)
51+
52+
# async def init_main():
53+
# await osc.create_server(asyncio.get_event_loop())
54+
# osc.add_handler("/predictor/*", predictor_handler)
55+
# osc.create_client()
56+
57+
# await loop()
58+
59+
# osc.close_server()
60+
61+
# def main(ip="127.0.0.1", send=57120, receive=9999, checkpoint=None):
62+
def main(host="127.0.0.1", port=9999, checkpoint=None):
63+
# global osc, predictor
64+
65+
# osc = OSC(ip, send, receive)
66+
osc = OSC(host, port)
67+
68+
predictor = None
69+
if checkpoint is not None:
70+
predictor = NotePredictor.from_checkpoint(checkpoint)
71+
predictor.eval()
72+
73+
# asyncio.run(init_main())
74+
75+
@osc.args('/predictor/*')
76+
def predictor_handler(address, *args):
77+
"""
78+
Handle OSC messages to Predictor
79+
"""
80+
address = address.split("/")
81+
cmd = address[2]
82+
83+
if cmd=="load":
84+
print(f"/load {args}")
85+
# global predictor
86+
predictor = NotePredictor.from_checkpoint(*args)
87+
predictor.eval()
88+
89+
elif cmd=="predict":
90+
print(f"/predict {args}")
91+
r = predictor.predict(*args)
92+
return '/prediction', r['pitch'], r['time']
93+
94+
elif cmd=="reset":
95+
print(f"/reset {args}")
96+
predictor.reset(*args)
97+
98+
else:
99+
print(f"PitchPredictor: Unrecognised OSC {address} with {args}")
100+
101+
if __name__=='__main__':
102+
# fire.Fire(main)
103+
run(main)

iipyper/iipyper/__init__.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import asyncio
2+
3+
import fire
4+
5+
from .midi import *
6+
from .osc import *
7+
8+
_loop_fns = []
9+
# decorator to make a function loop
10+
def repeat(time):
11+
# close the decorator over time argument
12+
def decorator(f):
13+
# define the coroutine
14+
async def g():
15+
# call `f` every `time` seconds
16+
while True:
17+
f()
18+
await asyncio.sleep(time)
19+
# track the coroutine in a global list
20+
_loop_fns.append(g)
21+
22+
return decorator
23+
24+
async def _run_async():
25+
# start OSC server
26+
for osc in OSC.instances:
27+
await osc.create_server(asyncio.get_event_loop())
28+
# osc.create_client()
29+
30+
# start loop tasks
31+
if len(_loop_fns):
32+
for f in _loop_fns:
33+
asyncio.create_task(f())
34+
# else:
35+
while True:
36+
await asyncio.sleep(1)
37+
38+
# clean up
39+
# for osc in OSC.instances:
40+
# osc.close_server()
41+
42+
def run(main):
43+
fire.Fire(main)
44+
asyncio.run(_run_async())

0 commit comments

Comments
 (0)