impsy 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
impsy/__init__.py ADDED
File without changes
impsy/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """imps.__main__: executed when imps directory is called as a script."""
2
+
3
+
4
+ from .impsy import main
5
+ main()
impsy/dataset.py ADDED
@@ -0,0 +1,72 @@
1
+ """impsy.dataset: functions for generating a dataset from .log files in the log directory."""
2
+
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import os
7
+ import click
8
+
9
+
10
+ def transform_log_to_sequence_example(logfile, dimension):
11
+ data_names = ['x'+str(i) for i in range(dimension-1)]
12
+ column_names = ['date', 'source'] + data_names
13
+ perf_df = pd.read_csv(logfile,
14
+ header=None, parse_dates=True,
15
+ index_col=0, names=column_names)
16
+ # Filter out RNN lines, just keep 'interface'
17
+ perf_df = perf_df[perf_df.source == 'interface']
18
+ # Process times.
19
+ perf_df['t'] = perf_df.index
20
+ perf_df.t = perf_df.t.diff()
21
+ perf_df.t = perf_df.t.dt.total_seconds()
22
+ perf_df = perf_df.dropna()
23
+ return np.array(perf_df[['t']+data_names])
24
+
25
+
26
+ @click.command(name="dataset")
27
+ @click.option("-D", "--dimension", type=int, default=2, help="The dimension of the data to model, must be >= 2.")
28
+ @click.option("-S", "--source", type=str, default="logs", help="The source directory to obtain .log files.")
29
+ def dataset(dimension: int, source: str):
30
+ """Generate a dataset from .log files in the log directory."""
31
+ # Load up the performances
32
+ log_location = f"{source}/"
33
+ log_file_ending = "-" + str(dimension) + "d-mdrnn.log"
34
+ log_arrays = []
35
+
36
+ for local_file in os.listdir(log_location):
37
+ if local_file.endswith(log_file_ending):
38
+ print("Processing:", local_file)
39
+ try:
40
+ log = transform_log_to_sequence_example(log_location + local_file,
41
+ dimension)
42
+ log_arrays.append(log)
43
+ except Exception:
44
+ print("Processing failed for", local_file)
45
+
46
+ # Save Performance Data in a compressed numpy file.
47
+ dataset_location = 'datasets/'
48
+ dataset_filename = 'training-dataset-' + str(dimension) + 'd.npz'
49
+
50
+ # Input format is:
51
+ # 0. 1. 2. ... n.
52
+ # dt x1 x2 ... xn
53
+
54
+ raw_perfs = []
55
+
56
+ acc = 0
57
+ time = 0
58
+ interactions = 0
59
+ for l in log_arrays:
60
+ acc += l.shape[0] * l.shape[1]
61
+ interactions += l.shape[0]
62
+ time += l.T[0].sum()
63
+ raw = l.astype('float32') # dt, x_1, ... , x_n
64
+ raw_perfs.append(raw)
65
+
66
+ print("total number of values:", acc)
67
+ print("total number of interactions:", interactions)
68
+ print("total time represented:", time)
69
+ print("total number of perfs in raw array:", len(raw_perfs))
70
+ raw_perfs = np.array(raw_perfs)
71
+ np.savez_compressed(dataset_location + dataset_filename, perfs=raw_perfs)
72
+ print("done saving:", dataset_location + dataset_filename)
impsy/impsy.py ADDED
@@ -0,0 +1,22 @@
1
+ """impsy.impsy: provides entry point main() to impsy."""
2
+
3
+
4
+ import click
5
+ from .dataset import dataset
6
+ from .train import train
7
+ from .interaction import run
8
+ from .tests import test_mdrnn
9
+
10
+ @click.group()
11
+ def cli():
12
+ pass
13
+
14
+
15
+ def main():
16
+ """The entry point function for IMPSY, this just passes through the interfaces for each command"""
17
+ cli.add_command(dataset)
18
+ cli.add_command(train)
19
+ cli.add_command(run)
20
+ cli.add_command(test_mdrnn)
21
+ # runs the command line interface
22
+ cli()
impsy/interaction.py ADDED
@@ -0,0 +1,323 @@
1
+ """impsy.interaction: Functions for using imps as an interactive music system. This server has OSC input and output."""
2
+
3
+ import logging
4
+ import time
5
+ import datetime
6
+ import numpy as np
7
+ import queue
8
+ import click
9
+ from pythonosc import dispatcher
10
+ from pythonosc import osc_server
11
+ from pythonosc import udp_client
12
+ from threading import Thread
13
+ from .utils import mdrnn_config
14
+
15
+
16
+ np.set_printoptions(precision=2)
17
+
18
+ # Interaction Modes:
19
+
20
+ # user, callresponse, filter, battle.
21
+ # user: no AI generation, but user interaction is passed on and logged.
22
+ # callresponse: AI responds after 'threshold' seconds, typical turn-based arrangement.
23
+ # filter: AI responds directly to every input, could be providing second part etc.
24
+ # battle: AI disconnected from human input, both running simultaneously.
25
+
26
+
27
+ # Global variables
28
+ # TODO: get rid of these, use a Class instead for data storage.
29
+ call_response_mode = False
30
+ user_to_rnn = False
31
+ rnn_to_rnn = False
32
+ rnn_to_sound = False
33
+ last_user_interaction_time = None
34
+ last_user_interaction_data = None
35
+
36
+
37
+ @click.command(name="run")
38
+ @click.option("--log/--no-log", default=True, help="Save input and output data to a log file.")
39
+ @click.option("--verbose/--no-verbose", default=True, help="Verbose mode, print prediction results.")
40
+ # Performance modes
41
+ @click.option("-O", "--mode", type=str, default="callresponse", help="Select interaction mode, one of: user, callresponse, filter, battle. user: no AI generation, callresponse: AI responds after 'threshold' seconds, filter: AI responds directly to every input, battle: AI disconnected from human input")
42
+ @click.option("-T", "--threshold", type=float, default=2.0, help="Seconds to wait before switching to response")
43
+ # MDRNN arguments.
44
+ @click.option("-D", "--dimension", type=int, default=2, help="The dimension of the data to model, must be >= 2.")
45
+ @click.option("-M", "--modelsize", default="s", help="The model size: xs, s, m, l, xl", type=str)
46
+ @click.option("-S", "--sigmatemp", type=float, default=0.01, help="The sigma temperature for sampling.")
47
+ @click.option("-P", "--pitemp", type=float, default=1, help="The pi temperature for sampling.")
48
+ # OSC addresses
49
+ @click.option("--clientip", type=str, default="localhost", help="The address of output device, default is 'localhost'.")
50
+ @click.option("--clientport", type=int, default=5000, help="The port the output device is listening on, default is 5000.")
51
+ @click.option("--serverip", type=str, default="localhost", help="The address of this server, default is 'localhost'.")
52
+ @click.option("--serverport", type=int, default=5001, help="The port this server should listen on, default is 5001.")
53
+ def run(log: bool, verbose: bool, mode: str, threshold: float, dimension: int, modelsize: str, sigmatemp: float, pitemp: float, clientip: str, clientport: int, serverip:str, serverport: int):
54
+ """Run IMPS predictive musical interaction system as a server with OSC input and output."""
55
+ global call_response_mode
56
+ global user_to_rnn
57
+ global rnn_to_rnn
58
+ global rnn_to_sound
59
+ global last_user_interaction_time
60
+ global last_user_interaction_data
61
+
62
+ # import tensorflow, do this now to make CLI more responsive.
63
+ print("Importing MDRNN.")
64
+ start_import = time.time()
65
+ import impsy.mdrnn as mdrnn
66
+ import tensorflow.compat.v1 as tf
67
+ print("Done. That took", time.time() - start_import, "seconds.")
68
+
69
+ model_config = mdrnn_config(modelsize)
70
+ mdrnn_units = model_config["units"]
71
+ mdrnn_layers = model_config["layers"]
72
+ mdrnn_mixes = model_config["mixes"]
73
+
74
+ # Interaction Loop Parameters
75
+ # All set to false before setting is chosen.
76
+ user_to_rnn = False
77
+ rnn_to_rnn = False
78
+ rnn_to_sound = False
79
+
80
+ # Interactive Mapping
81
+ if mode == "callresponse":
82
+ print("Entering call and response mode.")
83
+ # set initial conditions.
84
+ user_to_rnn = True
85
+ rnn_to_rnn = False
86
+ rnn_to_sound = False
87
+ elif mode == "filter":
88
+ print("Entering filter mode.")
89
+ user_to_rnn = True
90
+ rnn_to_rnn = False
91
+ rnn_to_sound = True
92
+ elif mode == "battle":
93
+ print("Entering battle royale mode.")
94
+ user_to_rnn = False
95
+ rnn_to_rnn = True
96
+ rnn_to_sound = True
97
+ elif mode == "user":
98
+ print("Entering user only mode.")
99
+ user_to_rnn = False
100
+ rnn_to_rnn = False
101
+ rnn_to_sound = False
102
+
103
+
104
+ def build_network(sess):
105
+ """Build the MDRNN."""
106
+ mdrnn.MODEL_DIR = "./models/"
107
+ tf.keras.backend.set_session(sess)
108
+ with compute_graph.as_default():
109
+ net = mdrnn.PredictiveMusicMDRNN(mode=mdrnn.NET_MODE_RUN,
110
+ dimension=dimension,
111
+ n_hidden_units=mdrnn_units,
112
+ n_mixtures=mdrnn_mixes,
113
+ layers=mdrnn_layers)
114
+ net.pi_temp = pitemp
115
+ net.sigma_temp = sigmatemp
116
+ print("MDRNN Loaded.")
117
+ return net
118
+
119
+
120
+ def handle_interface_message(address: str, *osc_arguments) -> None:
121
+ """Handler for OSC messages from the interface"""
122
+ global last_user_interaction_time
123
+ global last_user_interaction_data
124
+ if verbose:
125
+ # print out OSC message.
126
+ print("User:", ', '.join(["{0:0.2f}".format(abs(i)) for i in osc_arguments]))
127
+ int_input = osc_arguments
128
+ logger = logging.getLogger("impslogger")
129
+ logger.info("{1},interface,{0}".format(','.join(map(str, int_input)),
130
+ datetime.datetime.now().isoformat()))
131
+ dt = time.time() - last_user_interaction_time
132
+ last_user_interaction_time = time.time()
133
+ last_user_interaction_data = np.array([dt, *int_input])
134
+ assert len(last_user_interaction_data) == dimension, "Input is incorrect dimension, set dimension to %r" % len(last_user_interaction_data)
135
+ # These values are accessed by the RNN in the interaction loop function.
136
+ interface_input_queue.put_nowait(last_user_interaction_data)
137
+
138
+
139
+ def handle_temperature_message(address: str, *osc_arguments) -> None:
140
+ """Handler for temperature messages from the interface: format is ff [sigma temp, pi temp]"""
141
+ new_sigma_temp = osc_arguments[0]
142
+ new_pi_temp = osc_arguments[1]
143
+ if verbose:
144
+ print(f"Temperature -- Sigma: {new_sigma_temp}, Pi: {new_pi_temp}")
145
+ net.sigma_temp = new_sigma_temp
146
+ net.pi_temp = new_pi_temp
147
+
148
+
149
+ def handle_timescale_message(address: str, *osc_arguments) -> None:
150
+ """Handler for timescale messages: format is f [timescale]"""
151
+ new_timescale = osc_arguments[0]
152
+ if verbose:
153
+ print(f"Timescale: {new_timescale}")
154
+ # TODO: implement this on the prediction end.
155
+
156
+
157
+ def request_rnn_prediction(input_value):
158
+ """ Accesses a single prediction from the RNN. """
159
+ output_value = net.generate_touch(input_value)
160
+ return output_value
161
+
162
+
163
+ def make_prediction(sess, compute_graph):
164
+ """Interaction loop: reads input, makes predictions, outputs results."""
165
+ # Make predictions.
166
+
167
+ # First deal with user --> MDRNN prediction
168
+ if user_to_rnn and not interface_input_queue.empty():
169
+ item = interface_input_queue.get(block=True, timeout=None)
170
+ tf.keras.backend.set_session(sess)
171
+ with compute_graph.as_default():
172
+ rnn_output = request_rnn_prediction(item)
173
+ # if verbose:
174
+ # print("User->RNN:", ",".join(["{0:0.2f}".format(float(i)) for i in rnn_output]))
175
+ if rnn_to_sound:
176
+ rnn_output_buffer.put_nowait(rnn_output)
177
+ interface_input_queue.task_done()
178
+
179
+ # Now deal with MDRNN --> MDRNN prediction.
180
+ if rnn_to_rnn and rnn_output_buffer.empty() and not rnn_prediction_queue.empty():
181
+ item = rnn_prediction_queue.get(block=True, timeout=None)
182
+ tf.keras.backend.set_session(sess)
183
+ with compute_graph.as_default():
184
+ rnn_output = request_rnn_prediction(item)
185
+ if verbose:
186
+ print("RNN: ", ', '.join(["{0:0.2f}".format(abs(i)) for i in rnn_output[1:]]), 'dt:', "{0:0.2f}".format(abs(rnn_output[0])))
187
+ rnn_output_buffer.put_nowait(rnn_output) # put it in the playback queue.
188
+ rnn_prediction_queue.task_done()
189
+
190
+
191
+ def send_sound_command(command_args):
192
+ """Send a sound command back to the interface/synth"""
193
+ assert len(command_args)+1 == dimension, "Dimension not same as prediction size." # Todo more useful error.
194
+ osc_client.send_message(OUTPUT_MESSAGE_ADDRESS, command_args)
195
+
196
+
197
+ def playback_rnn_loop():
198
+ """Plays back RNN notes from its buffer queue."""
199
+ while True:
200
+ item = rnn_output_buffer.get(block=True, timeout=None) # Blocks until next item is available.
201
+ # print("processing an rnn command", time.time())
202
+ dt = item[0]
203
+ x_pred = np.minimum(np.maximum(item[1:], 0), 1)
204
+ dt = max(dt, 0.001) # stop accidental minus and zero dt.
205
+ time.sleep(dt) # wait until time to play the sound
206
+ # put last played in queue for prediction.
207
+ rnn_prediction_queue.put_nowait(np.concatenate([np.array([dt]), x_pred]))
208
+ if rnn_to_sound:
209
+ send_sound_command(x_pred)
210
+ # print("RNN Played:", x_pred, "at", dt)
211
+ logger = logging.getLogger("impslogger")
212
+ logger.info("{1},rnn,{0}".format(','.join(map(str, x_pred)),
213
+ datetime.datetime.now().isoformat()))
214
+ rnn_output_buffer.task_done()
215
+
216
+
217
+ def monitor_user_action():
218
+ """Handles changing action responsibility in Call-Response mode."""
219
+ global call_response_mode
220
+ global user_to_rnn
221
+ global rnn_to_rnn
222
+ global rnn_to_sound
223
+ # Check when the last user interaction was
224
+ dt = time.time() - last_user_interaction_time
225
+ if dt > threshold:
226
+ # switch to response modes.
227
+ user_to_rnn = False
228
+ rnn_to_rnn = True
229
+ rnn_to_sound = True
230
+ if call_response_mode == 'call':
231
+ print("switching to response.")
232
+ call_response_mode = 'response'
233
+ while not rnn_prediction_queue.empty():
234
+ # Make sure there's no inputs waiting to be predicted.
235
+ rnn_prediction_queue.get()
236
+ rnn_prediction_queue.task_done()
237
+ rnn_prediction_queue.put_nowait(last_user_interaction_data) # prime the RNN queue
238
+ else:
239
+ # switch to call mode.
240
+ user_to_rnn = True
241
+ rnn_to_rnn = False
242
+ rnn_to_sound = False
243
+ if call_response_mode == 'response':
244
+ print("switching to call.")
245
+ call_response_mode = 'call'
246
+ # Empty the RNN queues.
247
+ while not rnn_output_buffer.empty():
248
+ # Make sure there's no actions waiting to be synthesised.
249
+ rnn_output_buffer.get()
250
+ rnn_output_buffer.task_done()
251
+
252
+
253
+ # Logging
254
+ LOG_FILE = datetime.datetime.now().isoformat().replace(":", "-")[:19] + "-" + str(dimension) + "d" + "-mdrnn.log" # Log file name.
255
+ LOG_FILE = "logs/" + LOG_FILE
256
+ LOG_FORMAT = '%(message)s'
257
+
258
+ if log:
259
+ formatter = logging.Formatter(LOG_FORMAT)
260
+ handler = logging.FileHandler(LOG_FILE)
261
+ handler.setFormatter(formatter)
262
+ logger = logging.getLogger("impslogger")
263
+ logger.setLevel(logging.INFO)
264
+ logger.addHandler(handler)
265
+ print("Logging enabled:", LOG_FILE)
266
+ # Details for OSC output
267
+ INPUT_MESSAGE_ADDRESS = "/interface"
268
+ OUTPUT_MESSAGE_ADDRESS = "/prediction"
269
+ TEMPERATURE_MESSAGE_ADDRESS = "/temperature"
270
+ TIMESCALE_MESSAGE_ADDRESS = "/timescale"
271
+
272
+ # Set up runtime variables.
273
+ # ## Load the Model
274
+ compute_graph = tf.Graph()
275
+ with compute_graph.as_default():
276
+ sess = tf.Session()
277
+ net = build_network(sess)
278
+ interface_input_queue = queue.Queue()
279
+ rnn_prediction_queue = queue.Queue()
280
+ rnn_output_buffer = queue.Queue()
281
+ writing_queue = queue.Queue()
282
+ last_user_interaction_time = time.time()
283
+ last_user_interaction_data = mdrnn.random_sample(out_dim=dimension)
284
+ rnn_prediction_queue.put_nowait(mdrnn.random_sample(out_dim=dimension))
285
+ call_response_mode = 'call'
286
+
287
+ # Set up OSC client and server
288
+ osc_client = udp_client.SimpleUDPClient(clientip, clientport)
289
+ disp = dispatcher.Dispatcher()
290
+ disp.map(INPUT_MESSAGE_ADDRESS, handle_interface_message)
291
+ disp.map(TEMPERATURE_MESSAGE_ADDRESS, handle_temperature_message)
292
+ disp.map(TIMESCALE_MESSAGE_ADDRESS, handle_timescale_message)
293
+ server = osc_server.ThreadingOSCUDPServer((serverip, serverport), disp)
294
+
295
+ thread_running = True # TODO: is this line needed?
296
+
297
+ # Set up run loop.
298
+ print("Preparing MDRNN.")
299
+ tf.keras.backend.set_session(sess)
300
+ with compute_graph.as_default():
301
+ net.load_model() # try loading from default file location.
302
+ print("Preparting MDRNN thread.")
303
+ rnn_thread = Thread(target=playback_rnn_loop, name="rnn_player_thread", daemon=True)
304
+ print("Preparing Server thread.")
305
+ server_thread = Thread(target=server.serve_forever, name="server_thread", daemon=True)
306
+
307
+ try:
308
+ rnn_thread.start()
309
+ server_thread.start()
310
+ print("Prediction server started.")
311
+ print("Serving on {}".format(server.server_address))
312
+ while True:
313
+ make_prediction(sess, compute_graph)
314
+ if mode == "callresponse":
315
+ monitor_user_action()
316
+ except KeyboardInterrupt:
317
+ print("\nCtrl-C received... exiting.")
318
+ thread_running = False
319
+ rnn_thread.join(timeout=0.1)
320
+ server_thread.join(timeout=0.1)
321
+ pass
322
+ finally:
323
+ print("\nDone, shutting down.")
@@ -0,0 +1,267 @@
1
+ """
2
+ EMPI MDRNN Model.
3
+ Charles P. Martin, 2018
4
+ University of Oslo, Norway.
5
+ """
6
+ import numpy as np
7
+ import tensorflow.compat.v1 as tf
8
+ import mdn
9
+ import time
10
+
11
+ tf.logging.set_verbosity(tf.logging.INFO) # set logging.
12
+ NET_MODE_TRAIN = 'train'
13
+ NET_MODE_RUN = 'run'
14
+ MODEL_DIR = "./models/"
15
+ LOG_PATH = "./logs/"
16
+ SCALE_FACTOR = 10 # scales input and output from the model. Should be the same between training and inference.
17
+
18
+
19
+ # Functions for slicing up data
20
+ def slice_sequence_examples(sequence, num_steps, step_size=1):
21
+ """ Slices a sequence into examples of length
22
+ num_steps with step size step_size."""
23
+ xs = []
24
+ for i in range((len(sequence) - num_steps) // step_size + 1):
25
+ example = sequence[(i * step_size): (i * step_size) + num_steps]
26
+ xs.append(example)
27
+ return xs
28
+
29
+
30
+ def seq_to_overlapping_format(examples):
31
+ """Takes sequences of seq_len+1 and returns overlapping
32
+ sequences of seq_len."""
33
+ xs = []
34
+ ys = []
35
+ for ex in examples:
36
+ xs.append(ex[:-1])
37
+ ys.append(ex[1:])
38
+ return (xs, ys)
39
+
40
+
41
+ def seq_to_singleton_format(examples):
42
+ """Return the examples in seq to singleton format.
43
+ """
44
+ xs = []
45
+ ys = []
46
+ for ex in examples:
47
+ xs.append(ex[:-1])
48
+ ys.append(ex[-1])
49
+ return (xs, ys)
50
+
51
+
52
+ def build_model(seq_len=30, hidden_units=256, num_mixtures=5, layers=2,
53
+ out_dim=2, time_dist=True, inference=False, compile_model=True,
54
+ print_summary=True):
55
+ """Builds a EMPI MDRNN model for training or inference.
56
+
57
+ Keyword Arguments:
58
+ seq_len : sequence length to unroll
59
+ hidden_units : number of LSTM units in each layer
60
+ num_mixtures : number of mixture components (5-10 is good)
61
+ layers : number of layers (2 is good)
62
+ out_dim : number of dimensions for the model = number of degrees of freedom + 1 (time)
63
+ time_dist : time distributed or not (default True)
64
+ inference : inference network or training (default False)
65
+ compile_model : compiles the model (default True)
66
+ print_summary : print summary after creating mdoe (default True)
67
+ """
68
+ print("Building EMPI Model...")
69
+ # Set up training mode
70
+ stateful = False
71
+ # batch_shape = None
72
+ batch_size = None
73
+ # Set up inference mode.
74
+ if inference:
75
+ stateful = True
76
+ batch_size = 1
77
+ #batch_shape = (1, 1, out_dim)
78
+ inputs = tf.keras.layers.Input(shape=(seq_len, out_dim), name='inputs',
79
+ batch_size=batch_size)
80
+ # batch_shape=batch_shape)
81
+ lstm_in = inputs # starter input for lstm
82
+ for layer_i in range(layers):
83
+ ret_seq = True
84
+ if (layer_i == layers - 1) and not time_dist:
85
+ # return sequences false if last layer, and not time distributed.
86
+ ret_seq = False
87
+ lstm_out = tf.keras.layers.LSTM(hidden_units, name='lstm'+str(layer_i),
88
+ return_sequences=ret_seq,
89
+ stateful=stateful)(lstm_in)
90
+ lstm_in = lstm_out
91
+
92
+ mdn_layer = mdn.MDN(out_dim, num_mixtures, name='mdn_outputs')
93
+ if time_dist:
94
+ mdn_layer = tf.keras.layers.TimeDistributed(mdn_layer, name='td_mdn')
95
+ mdn_out = mdn_layer(lstm_out) # apply mdn
96
+ model = tf.keras.models.Model(inputs=inputs, outputs=mdn_out)
97
+
98
+ if compile_model:
99
+ loss_func = mdn.get_mixture_loss_func(out_dim, num_mixtures)
100
+ optimizer = tf.keras.optimizers.Adam()
101
+ model.compile(loss=loss_func, optimizer=optimizer)
102
+
103
+ model.summary()
104
+ return model
105
+
106
+
107
+ def load_inference_model(model_file="", layers=2, units=512, mixtures=5, predict_moving=False):
108
+ """Returns an IMPS model loaded from a file"""
109
+ # TODO: make this parse the name to get the hyperparameters.
110
+ decoder = decoder = build_model(seq_len=1, hidden_units=units, num_mixtures=mixtures, layers=layers, time_dist=False, inference=True, compile_model=False, print_summary=True, predict_moving=predict_moving)
111
+ decoder.load_weights(model_file)
112
+ return decoder
113
+
114
+
115
+ def random_sample(out_dim=2):
116
+ """ Generate a random sample in format (dt, x_1, ..., x_n), where dt is positive
117
+ and the x_i are between 0 and 1."""
118
+ output = np.random.rand(out_dim)
119
+ output[0] = (0.01 + (np.random.rand()-0.5)*0.005) # TODO: see if this dt heuristic should change
120
+ return output
121
+
122
+
123
+ def proc_generated_touch(x_input, out_dim=2):
124
+ """ Processes a generated touch in the format (dt, x)
125
+ such that dt > 0, and 0 <= x <= 1 """
126
+ dt = np.maximum(x_input[0], 0.000454) # TODO: see if the min value of dt shoud change.
127
+ x_output = np.minimum(np.maximum(x_input[1:], 0), 1)
128
+ return np.concatenate([np.array([dt]), x_output])
129
+
130
+
131
+ def generate_sample(model, n_mixtures, prev_sample, pi_temp=1.0, sigma_temp=0.0, out_dim=2):
132
+ """Generate one forward prediction from a previous sample in format
133
+ (dt, x_1,...,x_n). Pi and Sigma temperature are adjustable."""
134
+ params = model.predict(prev_sample.reshape(1, 1, out_dim) * SCALE_FACTOR)
135
+ new_sample = mdn.sample_from_output(params[0], out_dim, n_mixtures, temp=pi_temp, sigma_temp=sigma_temp) / SCALE_FACTOR
136
+ new_sample = new_sample.reshape(out_dim,)
137
+ return new_sample
138
+
139
+
140
+ def generate_performance(model, n_mixtures, first_sample, time_limit=None, steps_limit=1000, pi_temp=1.0, sigma_temp=0.0, out_dim=2):
141
+ """Generates a performance of (dt, x) pairs, up to a step_limit.
142
+ Time limit is not presently implemented.
143
+ """
144
+ time = 0
145
+ steps = 0
146
+ prev_sample = first_sample
147
+ print(prev_sample)
148
+ performance = [prev_sample.reshape((out_dim,))]
149
+ while (steps < steps_limit): # and time < time_limit
150
+ params = model.predict(prev_sample.reshape(1, 1, out_dim) * SCALE_FACTOR)
151
+ prev_sample = mdn.sample_from_output(params[0], out_dim, n_mixtures,
152
+ temp=pi_temp,
153
+ sigma_temp=sigma_temp)
154
+ prev_sample = prev_sample / SCALE_FACTOR
155
+ output_touch = prev_sample.reshape(out_dim,)
156
+ output_touch = proc_generated_touch(output_touch)
157
+ performance.append(output_touch.reshape((out_dim,)))
158
+ steps += 1
159
+ time += output_touch[0]
160
+ return np.array(performance)
161
+
162
+
163
+ class PredictiveMusicMDRNN(object):
164
+ """EMPI MDRNN object for convenience in the run script."""
165
+
166
+ def __init__(self, mode=NET_MODE_TRAIN, dimension=2, n_hidden_units=128, n_mixtures=5, batch_size=100, sequence_length=120, layers=2):
167
+ """Initialise the MDRNN model. Use mode='run' for evaluation graph and
168
+ mode='train' for training graph."""
169
+ # network parameters
170
+ self.dimension = dimension
171
+ self.mode = mode
172
+ self.n_hidden_units = n_hidden_units
173
+ self.n_rnn_layers = layers
174
+ self.n_mixtures = n_mixtures # number of mixtures
175
+ # Training parameters
176
+ self.batch_size = batch_size
177
+ self.sequence_length = sequence_length
178
+ self.val_split = 0.10
179
+ # Sampling hyperparameters
180
+ self.pi_temp = 1.5
181
+ self.sigma_temp = 0.01
182
+
183
+ if self.mode is NET_MODE_TRAIN:
184
+ self.model = build_model(seq_len=self.sequence_length,
185
+ hidden_units=self.n_hidden_units,
186
+ num_mixtures=self.n_mixtures,
187
+ layers=self.n_rnn_layers,
188
+ out_dim=self.dimension,
189
+ time_dist=True,
190
+ inference=False,
191
+ compile_model=True,
192
+ print_summary=True)
193
+ else:
194
+ self.model = build_model(seq_len=1,
195
+ hidden_units=self.n_hidden_units,
196
+ num_mixtures=self.n_mixtures,
197
+ layers=self.n_rnn_layers,
198
+ out_dim=self.dimension,
199
+ time_dist=False,
200
+ inference=True,
201
+ compile_model=False,
202
+ print_summary=True)
203
+
204
+ self.run_name = self.get_run_name()
205
+
206
+ def model_name(self):
207
+ """Returns the name of the present model for saving to disk"""
208
+ return "musicMDRNN" + "-dim" + str(self.dimension) + "-layers" + str(self.n_rnn_layers) + "-units" + str(self.n_hidden_units) + "-mixtures" + str(self.n_mixtures) + "-scale" + str(SCALE_FACTOR)
209
+
210
+ def load_model(self, model_file=None):
211
+ if model_file is None:
212
+ model_file = MODEL_DIR + self.model_name() + ".h5"
213
+ try:
214
+ self.model.load_weights(model_file)
215
+ except OSError as err:
216
+ print("OS error: {0}".format(err))
217
+ print("MDRNN could not be loaded from file:", model_file)
218
+ print("MDRNN is untrained.")
219
+
220
+ def get_run_name(self):
221
+ out = self.model_name() + "-"
222
+ out += time.strftime("%Y%m%d-%H%M%S")
223
+ return out
224
+
225
+ def train(self, X, y, num_epochs=10, saving=True):
226
+ """Train the network for the a number of epochs."""
227
+ # Setup callbacks
228
+ filepath = MODEL_DIR + self.model_name() + "-E{epoch:02d}-VL{val_loss:.2f}.hdf5"
229
+ checkpoint = tf.keras.callbacks.ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
230
+ terminateOnNaN = tf.keras.callbacks.TerminateOnNaN()
231
+ tboard = tf.keras.callbacks.TensorBoard(log_dir=LOG_PATH+self.run_name, histogram_freq=2, batch_size=32, write_graph=True, update_freq='epoch')
232
+ callbacks = [terminateOnNaN, tboard]
233
+ if saving:
234
+ callbacks.append(checkpoint)
235
+
236
+ # Do the data scaling in here.
237
+ X = np.array(X) * SCALE_FACTOR
238
+ y = np.array(y) * SCALE_FACTOR
239
+ print("Training corpus has shape:")
240
+ print("X:", X.shape)
241
+ print("y:", y.shape)
242
+
243
+ # Train
244
+ history = self.model.fit(X, y, batch_size=self.batch_size,
245
+ epochs=num_epochs,
246
+ validation_split=self.val_split,
247
+ callbacks=callbacks)
248
+ return history
249
+
250
+ def prepare_model_for_running(self):
251
+ """Reset RNN state."""
252
+ self.model.reset_states() # reset LSTM state.
253
+
254
+ def generate_touch(self, prev_sample):
255
+ # TODO - do something with the session.
256
+ output = generate_sample(self.model, self.n_mixtures, prev_sample,
257
+ pi_temp=self.pi_temp,
258
+ sigma_temp=self.sigma_temp,
259
+ out_dim=self.dimension)
260
+ return output
261
+
262
+ def generate_performance(self, first_sample, number):
263
+ return generate_performance(self.model, self.n_mixtures,
264
+ first_sample, time_limit=None,
265
+ steps_limit=number, pi_temp=self.pi_temp,
266
+ sigma_temp=self.sigma_temp,
267
+ out_dim=self.dimension)
@@ -0,0 +1,59 @@
1
+ """Manages Training Data for the Musical MDN and can generate fake datsets for testing."""
2
+ import numpy as np
3
+ import pandas as pd
4
+ import random
5
+
6
+
7
+ def batch_generator(seq_len, batch_size, dim, corpus):
8
+ """Returns a generator to cut up datasets into
9
+ batches of features and labels."""
10
+ # generator = batch_generator(SEQ_LEN, BATCH_SIZE, 3, corpus)
11
+ batch_X = np.zeros((batch_size, seq_len, dim))
12
+ batch_y = np.zeros((batch_size, dim))
13
+ while True:
14
+ for i in range(batch_size):
15
+ # choose random example
16
+ l = random.choice(corpus)
17
+ last_index = len(l) - seq_len - 1
18
+ start_index = np.random.randint(0, high=last_index)
19
+ batch_X[i] = l[start_index:start_index+seq_len]
20
+ batch_y[i] = l[start_index+1:start_index+seq_len+1] # .reshape(1,dim)
21
+ yield batch_X, batch_y
22
+
23
+
24
+ def generate_data():
25
+ """Generating some Slightly fuzzy sine wave data."""
26
+ NSAMPLE = 50000
27
+ print("Generating", str(NSAMPLE), "toy data samples.")
28
+ t_data = np.float32(np.array(range(NSAMPLE)) / 10.0)
29
+ t_interval = t_data[1] - t_data[0]
30
+ t_r_data = np.random.normal(0, t_interval / 20.0, size=NSAMPLE)
31
+ t_data = t_data + t_r_data
32
+ r_data = np.random.normal(size=NSAMPLE)
33
+ x_data = np.sin(t_data) * 1.0 + (r_data * 0.05)
34
+ df = pd.DataFrame({'t': t_data, 'x': x_data})
35
+ df.t = df.t.diff()
36
+ df.t = df.t.fillna(1e-4)
37
+ print(df.describe())
38
+ return np.array(df)
39
+
40
+
41
+ def generate_synthetic_3D_data():
42
+ """
43
+ Generates some slightly fuzzy sine wave data
44
+ in two dimensions (plus time).
45
+ """
46
+ NSAMPLE = 50000
47
+ print("Generating", str(NSAMPLE), "toy data samples.")
48
+ t_data = np.float32(np.array(range(NSAMPLE)) / 10.0)
49
+ t_interval = t_data[1] - t_data[0]
50
+ t_r_data = np.random.normal(0, t_interval / 20.0, size=NSAMPLE)
51
+ t_data = t_data + t_r_data
52
+ r_data = np.random.normal(size=NSAMPLE)
53
+ x_data = (np.sin(t_data) + (r_data / 10.0) + 1) / 2.0
54
+ y_data = (np.sin(t_data * 3.0) + (r_data / 10.0) + 1) / 2.0
55
+ df = pd.DataFrame({'a': x_data, 'b': y_data, 't': t_data})
56
+ df.t = df.t.diff()
57
+ df.t = df.t.fillna(1e-4)
58
+ print(df.describe())
59
+ return np.array(df)
@@ -0,0 +1,17 @@
1
+ from . import model
2
+ from . import sample_data
3
+
4
+
5
+ def train_epochs(num_epochs=1):
6
+ print("Training Mixture RNN for", num_epochs, "epochs")
7
+ net = model.MixtureRNN(mode=model.NET_MODE_TRAIN, n_hidden_units=128, n_mixtures=10, batch_size=100, sequence_length=120)
8
+ x_t_log = sample_data.generate_data()
9
+ loader = sample_data.SequenceDataLoader(num_steps=121, batch_size=100, corpus=x_t_log)
10
+ losses = net.train(loader, num_epochs, saving=True)
11
+ print("Training Done.")
12
+ print("Mean Losses per Batch:")
13
+ print(losses)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ train_epochs(30)
impsy/tests.py ADDED
@@ -0,0 +1,36 @@
1
+ import click
2
+ import time
3
+ from .utils import mdrnn_config
4
+
5
+
6
+ @click.command(name="test-mdrnn")
7
+ def test_mdrnn():
8
+ """This command simply loads the MDRNN to test that it works and how long it takes."""
9
+ # import tensorflow, do this now to make CLI more responsive.
10
+ print("Importing MDRNN.")
11
+ start_import = time.time()
12
+ import impsy.mdrnn as mdrnn
13
+ import tensorflow.compat.v1 as tf
14
+ print("Done. That took", time.time() - start_import, "seconds.")
15
+
16
+ model_config = mdrnn_config("s")
17
+
18
+ def build_network(sess, dimension, units, mixes, layers):
19
+ """Build the MDRNN."""
20
+ mdrnn.MODEL_DIR = "./models/"
21
+ tf.keras.backend.set_session(sess)
22
+ with compute_graph.as_default():
23
+ net = mdrnn.PredictiveMusicMDRNN(mode=mdrnn.NET_MODE_RUN,
24
+ dimension=dimension,
25
+ n_hidden_units=units,
26
+ n_mixtures=mixes,
27
+ layers=layers)
28
+ print("MDRNN Loaded.")
29
+ return net
30
+
31
+ start_build = time.time()
32
+ compute_graph = tf.Graph()
33
+ with compute_graph.as_default():
34
+ sess = tf.Session()
35
+ build_network(sess, 4, model_config["units"], model_config["mixes"], model_config["layers"])
36
+ print("Done. That took", time.time() - start_build, "seconds.")
impsy/train.py ADDED
@@ -0,0 +1,140 @@
1
+ """impsy.train: Functions for training an impsy mdrnn model."""
2
+
3
+
4
+ import random
5
+ import numpy as np
6
+ import os
7
+ import datetime
8
+ import click
9
+ from .utils import mdrnn_config
10
+
11
+
12
+ # Input and output to serial are bytes (0-255)
13
+ # Output to Pd is a float (0-1)
14
+
15
+
16
+ # Model Hyperparameters
17
+ SEQ_LEN = 50
18
+ SEQ_STEP = 1
19
+ TIME_DIST = True
20
+ # Training Hyperparameters:
21
+ VAL_SPLIT = 0.10
22
+ # Set random seed for reproducibility
23
+ SEED = 2345
24
+
25
+
26
+ @click.command(name="train")
27
+ @click.option("-D", "--dimension", type=int, default=2, help="The dimension of the data to model, must be >= 2.")
28
+ @click.option("-S", "--source", type=str, default="datasets", help="The source directory to obtain .npz dataset files.")
29
+ @click.option("-M", "--modelsize", default="s", help="The model size: xxs, xs, s, m, l, xl.", type=str)
30
+ @click.option("--earlystopping/--no-earlystopping", default=True, help="Use early stopping.")
31
+ @click.option("-P", "--patience", type=int, default=10, help="The number of epochs patience for early stopping.")
32
+ @click.option("-N", "--numepochs", type=int, default=100, help="The maximum number of epochs.")
33
+ @click.option("-B", "--batchsize", type=int, default=64, help="Batch size for training, default=64.")
34
+ def train(dimension: int, source: str, modelsize: str, earlystopping: bool, patience: int, numepochs: int, batchsize: int):
35
+ """Trains a predictive music interaction model."""
36
+
37
+ # Hack to get openMP working annoyingly.
38
+ os.environ['KMP_DUPLICATE_LIB_OK']='True' # TODO: is this necessary?
39
+ # Import IMPS MDRNN at this point so CLI is fast.
40
+ import impsy.mdrnn as mdrnn
41
+ from tensorflow.compat.v1 import keras
42
+ import tensorflow.compat.v1 as tf
43
+ # Set up environment.
44
+ # Only for GPU use:
45
+ os.environ["CUDA_VISIBLE_DEVICES"] = "0"
46
+ config = tf.ConfigProto()
47
+ config.gpu_options.allow_growth = True
48
+ sess = tf.Session(config=config)
49
+ tf.keras.backend.set_session(sess)
50
+
51
+ model_config = mdrnn_config(modelsize)
52
+ mdrnn_units = model_config["units"]
53
+ mdrnn_layers = model_config["layers"]
54
+ mdrnn_mixes = model_config["mixes"]
55
+
56
+ print("Model size:", modelsize)
57
+ print("Units:", mdrnn_units)
58
+ print("Layers:", mdrnn_layers)
59
+ print("Mixtures:", mdrnn_mixes)
60
+
61
+ random.seed(SEED)
62
+ np.random.seed(SEED)
63
+
64
+ # Load dataset
65
+ dataset_location = f'{source}/'
66
+ dataset_filename = f'training-dataset-{str(dimension)}d.npz'
67
+
68
+ with np.load(dataset_location + dataset_filename, allow_pickle=True) as loaded:
69
+ perfs = loaded['perfs']
70
+
71
+ print("Loaded perfs:", len(perfs))
72
+ print("Num touches:", np.sum([len(l) for l in perfs]))
73
+ corpus = perfs # might need to do some processing here...processing
74
+ # Restrict corpus to sequences longer than the corpus.
75
+ corpus = [l for l in corpus if len(l) > SEQ_LEN+1]
76
+ print("Corpus Examples:", len(corpus))
77
+ # Prepare training data as X and Y.
78
+ slices = []
79
+ for seq in corpus:
80
+ slices += mdrnn.slice_sequence_examples(seq,
81
+ SEQ_LEN+1,
82
+ step_size=SEQ_STEP)
83
+ X, y = mdrnn.seq_to_overlapping_format(slices)
84
+ X = np.array(X) * mdrnn.SCALE_FACTOR
85
+ y = np.array(y) * mdrnn.SCALE_FACTOR
86
+
87
+ print("Number of training examples:")
88
+ print("X:", X.shape)
89
+ print("y:", y.shape)
90
+
91
+ # Setup Training Model
92
+ model = mdrnn.build_model(seq_len=SEQ_LEN,
93
+ hidden_units=mdrnn_units,
94
+ num_mixtures=mdrnn_mixes,
95
+ layers=mdrnn_layers,
96
+ out_dim=dimension,
97
+ time_dist=TIME_DIST,
98
+ inference=False,
99
+ compile_model=True,
100
+ print_summary=True)
101
+
102
+ model_dir = "models/"
103
+ model_name = "musicMDRNN" + "-dim" + str(dimension) + "-layers" + str(mdrnn_layers) + "-units" + str(mdrnn_units) + "-mixtures" + str(mdrnn_mixes) + "-scale" + str(mdrnn.SCALE_FACTOR)
104
+ date_string = datetime.datetime.today().strftime('%Y%m%d-%H_%M_%S')
105
+
106
+ filepath = model_dir + model_name + "-E{epoch:02d}-VL{val_loss:.2f}.hdf5"
107
+ checkpoint = keras.callbacks.ModelCheckpoint(filepath,
108
+ monitor='val_loss',
109
+ verbose=1,
110
+ save_best_only=True,
111
+ mode='min')
112
+ terminateOnNaN = keras.callbacks.TerminateOnNaN()
113
+ early_stopping = keras.callbacks.EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=patience)
114
+ tboard = keras.callbacks.TensorBoard(log_dir='./logs/' + date_string + model_name,
115
+ histogram_freq=0,
116
+ write_graph=True,
117
+ update_freq='epoch')
118
+
119
+ callbacks = [checkpoint, terminateOnNaN, tboard]
120
+ if earlystopping:
121
+ print("Enabling Early Stopping.")
122
+ callbacks.append(early_stopping)
123
+ # Train
124
+ history = model.fit(X, y, batch_size=batchsize,
125
+ epochs=numepochs,
126
+ validation_split=VAL_SPLIT,
127
+ callbacks=callbacks)
128
+
129
+ # Save final Model
130
+ model.save_weights(model_dir + model_name + ".h5")
131
+
132
+ # ## Converting for tensorflow lite.
133
+ # # Convert the model.
134
+ # converter = tf.lite.TFLiteConverter.from_keras_model(model)
135
+ # tflite_model = converter.convert()
136
+ # tflite_model_name = f`{model_dir}{model_name}-lite.h5`
137
+ # with open(tflite_model_name, 'wb') as f:
138
+ # f.write(tflite_model)
139
+
140
+ print("Training done, bye.")
impsy/utils.py ADDED
@@ -0,0 +1,41 @@
1
+ SIZE_TO_PARAMETERS = {
2
+ 'xxs': {
3
+ "units": 16,
4
+ "mixes": 5,
5
+ "layers": 2,
6
+ },
7
+ 'xs': {
8
+ "units": 32,
9
+ "mixes": 5,
10
+ "layers": 2,
11
+ },
12
+ 's': {
13
+ "units": 64,
14
+ "mixes": 5,
15
+ "layers": 2
16
+ },
17
+ 'm': {
18
+ "units": 128,
19
+ "mixes": 5,
20
+ "layers": 2
21
+ },
22
+ 'l': {
23
+ "units": 256,
24
+ "mixes": 5,
25
+ "layers": 2
26
+ },
27
+ 'xl': {
28
+ "units": 512,
29
+ "mixes": 5,
30
+ "layers": 3
31
+ },
32
+ 'default': {
33
+ "units": 128,
34
+ "mixes": 5,
35
+ "layers": 2
36
+ }
37
+ }
38
+
39
+ def mdrnn_config(size: str):
40
+ """Get a config dictionary from a size string as used in the IMPS command line interface."""
41
+ return SIZE_TO_PARAMETERS[size]
@@ -0,0 +1,7 @@
1
+ Copyright 2019 Charles P Martin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.1
2
+ Name: impsy
3
+ Version: 0.5.0
4
+ Summary: IMPSY is the Interactive Musical Prediction SYstem, a tool for creative interactive intelligent musical instruments using a recurrent mixture density neural network.
5
+ License: MIT
6
+ Author: Charles Martin
7
+ Author-email: cpm@charlesmartin.au
8
+ Requires-Python: >=3.11,<3.12
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Requires-Dist: click (>=8.1.7,<9.0.0)
13
+ Requires-Dist: keras-mdn-layer (>=0.3.0,<0.4.0)
14
+ Requires-Dist: pandas (>=2.0.3,<3.0.0)
15
+ Requires-Dist: python-osc (>=1.8.3,<2.0.0)
16
+ Requires-Dist: tensorflow (==2.15.0) ; sys_platform == "linux"
17
+ Requires-Dist: tensorflow-macos (==2.15.0) ; sys_platform == "darwin"
18
+ Requires-Dist: tensorflow-probability (==0.23.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # IMPSY: The Interactive Musical Predictive System
22
+
23
+ ![MIT License](https://img.shields.io/github/license/cpmpercussion/keras-mdn-layer.svg?style=flat)
24
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.2580176.svg)](https://doi.org/10.5281/zenodo.2580176)
25
+
26
+ ![Predictive Musical Interaction](https://github.com/cpmpercussion/imps/raw/master/images/predictive_interaction.png)
27
+
28
+ IMPSY is a system for predicting musical control data in live performance. It uses a mixture density recurrent neural network (MDRNN) to observe control inputs over multiple time steps, predicting the next value of each step, and the time that expects the next value to occur. It provides an input and output interface over OSC and can work with musical interfaces with any number of real valued inputs (we've tried from 1-8). Several interactive paradigms are supported for call-response improvisation, as well as independent operation, and "filtering" of the performer's input. Whenever you use IMPSY, your input data is logged to build up a training corpus and a script is provided to train new versions of your model.
29
+
30
+ Here's a [demonstration video showing how IMPSY can be used with different musical interfaces:](https://www.youtube.com/embed/Kdmhrp2dfHw)
31
+
32
+ <!-- <iframe width="560" height="315" src="https://www.youtube.com/embed/Kdmhrp2dfHw" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> -->
33
+
34
+ ## Installation
35
+
36
+ IMPSY is written in Python with Keras and TensorFlow Probability, so it should work on any platform where Tensorflow can be installed. Python 3 is required and we use [Poetry](https://python-poetry.org) for managing dependencies. IMPSY currently relies on Python 3.11, TensorFlow 2.15.0, TensorFlow Probability 0.23.0, and keras-mdn-layer 0.3.0. You can see the dependencies in `pyproject.toml`.
37
+
38
+ To install IMPSY, first **ensure that you have a Python 3.11** installation available, then install [Poetry](https://python-poetry.org). The poetry install instructions vary depending on your preferences for a python setup this is likely to work on Linux, macOS or Windows (WSL):
39
+
40
+ curl -sSL https://install.python-poetry.org | python3 -
41
+
42
+ Then you should clone this repository or download it to your computer:
43
+
44
+ git clone https://github.com/cpmpercussion/impsy.git
45
+ cd impsy
46
+
47
+ Then you can install the dependencies using Poetry:
48
+
49
+ poetry install
50
+
51
+ Finally, you can test that IMPSY works:
52
+
53
+ poetry run ./start_impsy.py --help
54
+
55
+ ## How to use
56
+
57
+ There are four steps for using IMPSY. First, you'll need to setup your musical interface to send it OSC data and receive predictions the same way. Then you can log data, train the MDRNN, and make predictions using our provided scripts.
58
+
59
+ ### 1. Connect music interface and synthesis software
60
+
61
+ You'll need:
62
+
63
+ - A music interface that can output data as OSC.
64
+ - Some synthesiser software that can take OSC as input.
65
+
66
+ These could be the same piece of software or hardware!
67
+
68
+ You need to decide on the number of inputs (or dimension) for your predictive model. This is the number of continuous outputs from your interface plus one (for time). So for an interface with 8 faders, the dimension will be 9.
69
+
70
+ Now you need your music interface to send messages to IMPSY over OSC. The default address for IMPSY is: localhost:5001. The messages to IMPSY should have the OSC address `/interface`, and then a float between 0 and 1 for each continuous output on your interface, e.g.:
71
+
72
+ /interface 0 0.5 0.23 0.87 0.9 0.7 0.45 0.654
73
+
74
+ For an 8-dimensional interface.
75
+
76
+ Your synthesiser software or interface needs to listen for messages from the IMPSY system as well. These have the same format with the OSC address `/prediction`. You can interpret these as interactions predicted to occur right when the message is sent.
77
+
78
+ Here's an example diagram for our 8-controller example, the [xtouch mini controller](https://www.musictribe.com/Categories/Behringer/Computer-Audio/Desktop-Controllers/X-TOUCH-MINI/p/P0B3M).
79
+
80
+ ![Predictive Musical Interaction](https://github.com/cpmpercussion/imps/raw/master/images/IMPS_connection_example.png)
81
+
82
+ In this example we've used Pd to connect the xtouch mini to IMPSY and to synthesis sounds. Our Pd mapping patch takes data from the xtouch and sends `/interface` OSC messages to IMPSY, it also receives `/prediction` OSC message back from IMPSY whenever they occur. Of course, whenever the user performs with the controller, the mapping patch sends commands to the synthesiser patch to make sound. Whenever `/prediction` messages are received, these also trigger changes in the synth patch, and we also send MIDI messages back to the xtouch controller to update its lights so that the performer knows what IMPSY is predicting.
83
+
84
+ So what happens if IMPSY and the performer play at the same time? In this example, it doesn't make sense for both to control the synthesiser at the same time, so we set IMPSY to run in "call and response" mode, so that it only makes predictions when the human has stopped performing. We could also set up our mapping patch to use prediction messages for a different synth and use one of the simultaneous performance modes of IMPS.
85
+
86
+ ### 2. Log some training data
87
+
88
+ You use the `run` command to log training data. If your interface has N inputs the dimension is N+1:
89
+
90
+ poetry run ./start_impsy run --dimension (N+1) --log
91
+
92
+ This command creates files in the `logs` directory with data like this:
93
+
94
+ 2019-01-17T12:37:38.109979,interface,0.3359375,0.296875,0.5078125
95
+ 2019-01-17T12:37:38.137938,interface,0.359375,0.296875,0.53125
96
+ 2019-01-17T12:37:38.160842,interface,0.375,0.3046875,0.1953125
97
+
98
+ These CSV files have the format: timestamp, source of message (interface or rnn), x_1, x_2, ..., x_N.
99
+
100
+ You can log training data without using the RNN with the `mode` option to select "user" if you like, or use a partially trained RNN and then collect more data.
101
+
102
+ poetry run ./start_impsy run --dimension (N+1) --log -mode user
103
+
104
+ Every time you use IMPS' "run" command, a new log file is created so that you can build up a significant dataset!
105
+
106
+ ### 3. Train an MDRNN
107
+
108
+ There's two steps for training: Generate a dataset file, and train the predictive model.
109
+
110
+ Use the `dataset` command:
111
+
112
+ poetry run ./start_impsy dataset --dimension (N+1)
113
+
114
+ This command collates all logs of dimension N+1 from the logs directory and saves the data in a compressed `.npz` file in the datasets directory. It will also print out some information about your dataset, in particular the total number of individual interactions. To have a useful dataset, it's good to start with more than 10,000 individual interactions but YMMV.
115
+
116
+ To train the model, use the `train` command---this can take a while on a normal computer, so be prepared to let your computer sit and think for a few hours! You'll have to decide what _size_ model to try to train: `xs`, `s`, `m`, `l`, `xl`. The size refers to the number of LSTM units in each layer of your model and roughly corresponds to "learning capacity" at a cost of slower training and predictions.
117
+ It's a good idea to start with an `xs` or `s` model, and the larger models are more relevant for quite large datasets (e.g., >1M individual interactions).
118
+
119
+ poetry run ./start_impsy train --dimension (N+1) --modelsize s
120
+
121
+ It's a good idea to use the `earlystopping` option to stop training after the model stops improving for 10 epochs.
122
+
123
+ ### 4. Perform with your predictive model
124
+
125
+ Now that you have a trained model, you can run this command to start making predictions:
126
+
127
+ poetry run ./start_impsy run --dimension (N+1) --modelsize xs --log
128
+
129
+ The `--log` switch logs all of your interactions as well as predictions for later re-training. (The dataset generator filters out RNN records so that you only train on human sourced data).
130
+
131
+ PS: all the IMPSY commands respond to the `--help` switch to show command line options. If there's something not documented or working, it would be great if you add an issue above to let me know.
132
+
133
+ ## More about Mixture Density Recurrent Neural Networks
134
+
135
+ IMPSY uses a mixture density recurrent neural network MDRNN to make predictions. This machine learning architecture is set up to predict the next in a sequence of multi-valued elements. The recurrent neural network uses LSTM units to remember information about past inputs and use this to help make decisions. The mixture density model at the end of the network allows continuous multi-valued elements to be sampled from a rich probability distribution.
136
+
137
+ The network is illustrated here---every time IMPSY receives an interaction message from your interface, it is sent to thorugh the LSTM layers to produce the parameters of a Gaussian mixture model. The predicted next interaction is sampled from this probability model.
138
+
139
+ ![A Musical MDRNN](https://github.com/cpmpercussion/imps/raw/master/images/mdn_diagram.png)
140
+
141
+ The MDRNN is written in Keras and uses the [keras-mdn-layer](https://github.com/cpmpercussion/keras-mdn-layer) package. There's more info and tutorials about MDNs on [that github repo](https://github.com/cpmpercussion/keras-mdn-layer).
142
+
@@ -0,0 +1,15 @@
1
+ impsy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ impsy/__main__.py,sha256=zpRvI5LqD_MKQM20J7YFi0RWsoB9YrF6g_vOzPAv-qY,105
3
+ impsy/dataset.py,sha256=_d6cVFPiPfB6XmlnwoUOeXvi2X-WTLr8wP3Hw5EYqSM,2639
4
+ impsy/impsy.py,sha256=iGPym9IjH2uXUC1ehOzvwUIH6PuaWnfEvZDtWCx_j_E,499
5
+ impsy/interaction.py,sha256=W7xyr6SzxxBGPAdYYCzwRfXHn59e52sRLqOb8WQiLZE,14005
6
+ impsy/mdrnn/__init__.py,sha256=qB4Oq_gL75o7RW5OfWOjwl31nBFrd3rvlDCZ6CtBlMg,11147
7
+ impsy/mdrnn/sample_data.py,sha256=dOE0JwhAxaiCCkkaszIx4TvWx9FkpI8UoXRRhML8MMw,2201
8
+ impsy/mdrnn/test_model.py,sha256=ngrxhKeMdijt2MXmPRcFyNAEk31tyJyfzjUDcvMyhPI,587
9
+ impsy/tests.py,sha256=Mkqhrrq5eDvuEleDqNLSh8gu1hsArvzPx2vr335_oCs,1328
10
+ impsy/train.py,sha256=UPqzt2d8aXZ7RwJG18tGrIbwuizs9pWDdMuoXpH4wQw,5717
11
+ impsy/utils.py,sha256=oaPL_WSTd_w2mxYG4CcrRvcbilTugZ6AYOBCIxVXgcg,682
12
+ impsy-0.5.0.dist-info/LICENSE.md,sha256=jnbna6p-Yx2oDGjcXqA6StZU1XFF9KkvUo8QIOYjZe0,1055
13
+ impsy-0.5.0.dist-info/METADATA,sha256=frYTmNpTsObI8Op9c-Ielrx-son9erws6nqKil-whSQ,9995
14
+ impsy-0.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
+ impsy-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any