from dataclasses import dataclass import hashlib import math import numpy as np import torch import logging import itertools # TODO using a rest at the start of inference might not align with training data def midi2wavtool(midi, split_on_desc_anchor=False): res = [] for i, note in enumerate(midi): if note.get("descriptionAnchor", False) and split_on_desc_anchor: return {"notes": res}, midi2wavtool(midi[i:]) wnote = { "pitch": note["note"], "start": note["onBeat"], "end": note["offBeat"], "velocity": note.get("onVelocity", 127) / 127, "lifted": True, } if "dists" in note: wnote["dists"] = note["dists"] res.append(wnote) return ({"notes": res},) if split_on_desc_anchor else {"notes": res} def filter_wavtool_nulls(xs): return ( x for x in xs if x["start"] is not None and x["end"] is not None and x["pitch"] is not None ) def wavtool2midi(wavtool): return [ { "onBeat": note["start"], "offBeat": note["end"], "onVelocity": int(note["velocity"] * 127), "offVelocity": int(note["velocity"] * 127), "sourceTrack": 0, "note": note["pitch"], } for note in sorted( filter_wavtool_nulls(wavtool["notes"]), key=lambda note: (note["start"], note["pitch"]), ) ] def wavtoolm2midi(wavtool): return [ { "onBeat": note["start"], "offBeat": note["end"], "onVelocity": note["velocity"], "note": note["pitch"], } for note in sorted(wavtool, key=lambda note: (note["start"], note["pitch"])) ] def wavtoolmm2midi(wavtool): return [ { "onBeat": note["start"], "offBeat": note["end"], "onVelocity": int(note["velocity"] * 127), "note": int(note["pitch"]), } for note in sorted(wavtool, key=lambda note: (note["start"], note["pitch"])) ] def quantized_midi_to_indexable_bytes(quantized_midi): def int2varint(x): res = [] while x >= 0x80: res.append((x & 0x7F) | 0x80) x >>= 7 res.append(x) return res if len(quantized_midi) == 0: return b"" res = [] t = quantized_midi[0][2] group = [] for note in quantized_midi: t_next = note[2] if t_next > t: for note2 in sorted(group, key=lambda x: x[0]): res.extend(int2varint(max(0, note2[0]) + 3)) res.extend(int2varint(max(0, note2[3] - t) + 3)) group.clear() res.append(1) res.extend(int2varint(t_next - t + 3)) t = t_next group.append(note) res.append(2) # separator return bytes(res) def rec_sum(x): if isinstance(x, (int, float)): return x if isinstance(x, dict): return sum(rec_sum(v) for v in x.values()) return sum(rec_sum(v) for v in x) def normalize_dist(dist): def rec_div(x, d): if isinstance(x, (int, float)): return x / d if isinstance(x, dict): return {k: rec_div(v, d) for k, v in x.items()} return [rec_div(v, d) for v in x] s = max(rec_sum(dist.values()), 1e-8) return rec_div(dist, s) @dataclass class Symbol: index: int @dataclass class Control(Symbol): name: str def __repr__(self): return "{" + self.name + "}" @dataclass class Rest(Symbol): def __repr__(self): return "{R}" @dataclass class Pitch(Symbol): value: int velocity: int def __repr__(self): # convert midi note number self.value to string of the form {note}{octave}, e.g. C4 if self.value >= 1000: pitch_str = "drum" else: pitch_str = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", ][self.value % 12] + str(self.value // 12) return "{" + f"{pitch_str} {self.value} ({self.velocity})" + "}" @dataclass class Duration(Symbol): value: int # in quantize_divisions fraction: str # in fractional beats, e.g. 1/4 def __repr__(self): return "{L" + self.fraction + "}" @dataclass class Polyphony(Symbol): value: int def __repr__(self): return "{P" + str(self.value) + "}" @dataclass class Key(Symbol): value: int def __repr__(self): return "{K" + str(self.value) + "}" class VocabError(ValueError): pass def int_or_none(x): if x is None: return None return int(x) class Vocab: @classmethod def from_config(cls, config): quantize = int(config["quantize_divisions"]) return cls( int(config["pitch_min"]), int(config["pitch_max"]), int_or_none(config["drum_min"]), int_or_none(config["drum_max"]), int(config["velocity_min"]), int(config["velocity_max"]), int(config["velocity_quantize"]), int(config["length_min"]), int(config["length_max"]), int(config["polyphony_max"]), int(config["embed_length_max"]), int(config["embed_polyphony_max"]), quantize, ) def __init__( self, pitch_min, pitch_max, drum_min, drum_max, velocity_min, velocity_max, velocity_quantize, duration_min, # in quantize_divisions duration_max, # in quantize_divisions polyphony_max, embed_length_max, # in quantize_divisions embed_polyphony_max, quantize_divisions, ): super().__init__() self.pitch_min = pitch_min self.pitch_max = pitch_max self.drum_min = drum_min self.drum_max = drum_max self.velocity_min = velocity_min self.velocity_max = velocity_max self.velocity_quantize = velocity_quantize self.duration_min = duration_min self.duration_max = duration_max self.polyphony_max = polyphony_max self.embed_length_max = embed_length_max self.embed_polyphony_max = embed_polyphony_max self.quantize_divisions = quantize_divisions assert (self.drum_min is None) == (self.drum_max is None) if self.drum_min is not None: assert self.drum_min <= self.drum_max assert self.pitch_min <= self.pitch_max assert self.velocity_min <= self.velocity_max assert self.duration_min <= self.duration_max self.pad = Control(0, "pad") self.rest = Rest(1) self.begin = Control(2, "begin") self.end = Control(3, "end") self.new_track = Control(4, "new_track") self.description_anchor = Control(5, "description_anchor") self.symbols = [ self.pad, self.rest, self.begin, self.end, self.new_track, self.description_anchor, ] self.control_greatest_index = len(self.symbols) - 1 self.pitches = dict() self.pitch_least_index = len(self.symbols) for i in itertools.chain( range(pitch_min, pitch_max + 1), range(1000 + drum_min, 1000 + drum_max + 1) if drum_min is not None else (), ): pitch_vels = dict() for j in range(velocity_min, velocity_max + 1, velocity_quantize): pitch = Pitch(len(self.symbols), i, j) self.symbols.append(pitch) pitch_vels[j] = pitch if velocity_max % velocity_quantize != 0: pitch = Pitch(len(self.symbols), i, velocity_max) self.symbols.append(pitch) pitch_vels[velocity_max] = pitch self.pitches[i] = pitch_vels self.pitch_greatest_index = len(self.symbols) - 1 self.durations = dict() self.durations_least_index = len(self.symbols) for i in range(duration_min, duration_max + 1): gcd = math.gcd(i, quantize_divisions) duration = Duration( len(self.symbols), i, str(i // gcd) + ( ("/" + str(quantize_divisions // gcd)) if gcd != quantize_divisions else "" ), ) self.symbols.append(duration) self.durations[i] = duration self.durations_greatest_index = len(self.symbols) - 1 self.polyphony = dict() self.polyphony_least_index = len(self.symbols) for i in range(2, polyphony_max + 1): polyphony = Polyphony(len(self.symbols), i) self.symbols.append(polyphony) self.polyphony[i] = polyphony self.polyphony_greatest_index = len(self.symbols) - 1 self.keys = dict() self.keys_least_index = len(self.symbols) for i in range(24): key = Key(len(self.symbols), i) self.symbols.append(key) self.keys[i] = key self.keys_greatest_index = len(self.symbols) - 1 self.N = len(self.symbols) def is_duration(self, idx): return self.durations_least_index <= idx <= self.durations_greatest_index def is_rest(self, idx): return idx == self.rest.index def is_begin(self, idx): return idx == self.begin.index def is_new_track(self, idx): return idx == self.new_track.index def is_description_anchor(self, idx): return idx == self.description_anchor.index def is_pitch(self, idx): return self.pitch_least_index <= idx <= self.pitch_greatest_index def quantize_velocity(self, velocity): return min( self.velocity_max, max( self.velocity_min, int(self.velocity_quantize * round(velocity / self.velocity_quantize)), ), ) def get_pitch(self, midi_pitch, midi_velocity): return self.pitches[midi_pitch][self.quantize_velocity(midi_velocity)] def is_polyphony(self, idx): return self.polyphony_least_index <= idx <= self.polyphony_greatest_index def is_key(self, idx): return self.keys_least_index <= idx <= self.keys_greatest_index def get_tensor_subbeat(self, x, next=False): offset = 4 if next else 0 return ( x[..., offset + 1] * 4 * self.quantize_divisions + x[..., offset + 2] * self.quantize_divisions + x[..., offset + 3] ) def embedding_time_wrapped(self, x): x = x.numpy() if isinstance(x, torch.Tensor) else x return np.any( x[..., 1] * 4 * self.quantize_divisions + x[..., 2] * self.quantize_divisions + x[..., 3] > x[..., 5] * 4 * self.quantize_divisions + x[..., 6] * self.quantize_divisions + x[..., 7] ).item() def subbeat_to_tensor(self, subbeat, tensor=None): bar = (subbeat % self.embed_length_max) // (4 * self.quantize_divisions) beat = (subbeat // self.quantize_divisions) % 4 tick = subbeat % self.quantize_divisions if tensor is None: return torch.tensor([bar, beat, tick], dtype=torch.long) tensor[0] = bar tensor[1] = beat tensor[2] = tick return tensor def fast_estimate_length(self, example, accompany=None, include_header=True): if accompany is None: accompany_len = 0 else: accompany_len = ( self.fast_estimate_length(accompany, include_header=False) + 1 ) example = self.quantize_midi(example) count = 4 if include_header else 1 if len(example) == 0: return count + accompany_len time = example[0][2] polyphony = 0 for note in example: if note[0] > 1000: if note[0] - 1000 < self.drum_min or note[0] - 1000 > self.drum_max: raise VocabError( f"fast_estimate_length: drum {note[0]} out of vocab range" ) else: if note[0] < self.pitch_min or note[0] > self.pitch_max: raise VocabError( f"fast_estimate_length: pitch {note[0]} out of vocab range" ) if note[3] > self.embed_length_max: raise VocabError( f"fast_estimate_length: note end {note[3]} out of embed range" ) if note[3] - note[2] > self.duration_max: raise VocabError( f"fast_estimate_length: duration {note[3] - note[2]} out of vocab range" ) if note[2] > time: count += 3 if polyphony > 1 else 2 polyphony = 0 time = note[2] polyphony += 1 if polyphony > self.polyphony_max: raise VocabError( f"fast_estimate_length: polyphony {polyphony} out of vocab range" ) count += 2 return count + accompany_len # quantizes and removes any overlapping or zero-length notes # when strict is True, throws if: # any pitch is out of vocab range # any length is greater than vocab range # when allow_anchor is True, output may include description-anchored notes # returns [[pitch, velocity, qstart, qend, anchor], ...] def quantize_midi(self, notes, strict=True, allow_anchor=False): def quantize(x): return round(x * self.quantize_divisions) active_notes = dict() # pitch -> obj quantized_notes = [] # [pitch, velocity, qstart, qend] time = 0 for note in sorted(notes, key=lambda x: (x["onBeat"], -x["offBeat"])): pitch = note["note"] velocity = note["onVelocity"] qstart = quantize(note["onBeat"]) qlength = quantize(note["offBeat"] - note["onBeat"]) anchor = allow_anchor and note.get("descriptionAnchor", False) if qstart < 0 or qlength < 0: raise VocabError("negative qstart or qlength") # special rule for drums: round length 0 up to 1 if ( self.drum_min is not None and pitch >= 1000 + self.drum_min and pitch <= 1000 + self.drum_max and qlength == 0 ): qlength = 1 if ( strict and self.velocity_min != self.velocity_max and (velocity < self.velocity_min or velocity > self.velocity_max) ): raise VocabError(f"velocity {velocity} out of vocab range") velocity = self.quantize_velocity(velocity) if not ( (pitch >= self.pitch_min and pitch <= self.pitch_max) or ( self.drum_min is not None and pitch >= 1000 + self.drum_min and pitch <= 1000 + self.drum_max ) ): if strict: raise VocabError(f"pitch {pitch} out of vocab range") continue if qlength > self.duration_max: if strict: raise VocabError(f"duration {qlength} out of vocab range") continue if qlength <= 0: continue if qstart > time: time = qstart for active_pitch, rec in list(active_notes.items()): if rec[3] <= time: del active_notes[active_pitch] if pitch in active_notes: if active_notes[pitch][2] == qstart: active_notes[pitch][1] = velocity active_notes[pitch][3] = qstart + qlength continue active_notes[pitch][3] = qstart del active_notes[pitch] rec = [pitch, velocity, qstart, qstart + qlength, anchor] quantized_notes.append(rec) active_notes[pitch] = rec if len(notes) > 0 and len(quantized_notes) == 0: logging.debug("quantization removed all notes") return quantized_notes # midi -> tensor[j] def midi_to_symbols(self, notes, strict=True, allow_anchor=False): quantized_notes = self.quantize_midi( notes, strict=strict, allow_anchor=allow_anchor ) if len(quantized_notes) == 0: return None res = [] time = 0 group = [] def flush(): nonlocal group sorted_group = list(sorted(group, key=lambda x: x[0])) wrote_anchor = False if len(sorted_group) > 1: if strict and len(sorted_group) > self.polyphony_max: raise VocabError( f"polyphony {len(sorted_group)} out of vocab range, group is: {sorted_group}" ) if sorted_group[0][4]: res.append(self.description_anchor.index) wrote_anchor = True sorted_group = sorted_group[: self.polyphony_max] res.append(self.polyphony[len(sorted_group)].index) for note in sorted_group: if not wrote_anchor and note[4]: if len(sorted_group) > 1: logging.warning( "description anchor not first in polyphony group" ) res.append(self.description_anchor.index) res.append(self.pitches[note[0]][note[1]].index) res.append(self.durations[note[3] - note[2]].index) group.clear() for note in quantized_notes: qstart = note[2] if qstart > time: flush() rest_duration = qstart - time if rest_duration > self.duration_max: raise VocabError( f"rest duration {rest_duration} out of vocab range" ) res.append(self.rest.index) res.append(self.durations[rest_duration].index) time = qstart group.append(note) flush() return torch.tensor(res, dtype=torch.long) # midi -> tensor[j, (symbol, *context)] # key: optional key signature 0..23 (major then minor keys) # accompany: optional accompaniment midi, will be prepended to symbolc representation def midi_to_tensor( self, notes, key=None, accompany=None, rest_end=False, strict=False, place_anchor=True, ): out = self.append_symbols(None, self.header_symbols(key=key)) if accompany is not None: accompany_symbols = self.midi_to_symbols(accompany, strict=strict) if accompany_symbols is not None: out = self.append_symbols( out, accompany_symbols.unsqueeze(0), strict=strict, ) symbols = self.midi_to_symbols(notes, strict=strict, allow_anchor=True) out = self.append_symbols( out, torch.tensor([[self.new_track.index]], dtype=torch.long), ) # fix next subbeat and polyphony of new_track symbol self.subbeat_to_tensor(0, tensor=out[0, -1, 5:8]) out[0, -1, 8] = 0 # argument to torch.cat below symconcat = [] # append description anchor if needed if place_anchor: already_anchored = symbols is not None and torch.any( symbols == self.description_anchor.index ) if not already_anchored: symconcat.append( torch.tensor([self.description_anchor.index], dtype=torch.long) ) if symbols is not None: symconcat.append(symbols) if ( rest_end and symbols is not None and symbols.shape[0] >= 2 and self.is_pitch(symbols[-2].item()) and self.is_duration(symbols[-1].item()) ): symconcat.append(torch.tensor([self.rest.index], dtype=torch.long)) if not rest_end: symconcat.append(torch.tensor([self.end.index], dtype=torch.long)) if len(symconcat) == 0: return out[0] return self.append_symbols( out, torch.cat(symconcat, dim=-1).unsqueeze(0), strict=strict )[0] def header_symbols(self, key=None): header = [self.begin.index] if key is not None: header.append(self.keys[key].index) return torch.tensor(header, dtype=torch.long).unsqueeze(0) # tensor[j, (symbol, *context)] -> midi, key def tensor_to_midi( self, x, start_subbeat=0, strict=True, return_accompany=False, dists=None ): # read header if there is one start_idx = 0 key = None while start_idx < x.shape[0] and not ( self.is_pitch(x[start_idx, 0]) or self.is_polyphony(x[start_idx, 0]) or self.is_rest(x[start_idx, 0]) or self.is_description_anchor(x[start_idx, 0]) ): if self.is_key(x[start_idx, 0]): assert not strict or key is None, "multiple keys" key = self.symbols[x[start_idx, 0].item()].value start_idx += 1 assert not strict or start_idx > 0, "no header" if start_idx > 0: assert not strict or ( x[0, 0].item() == self.begin.index ), "header does not begin with begin symbol" if start_idx == x.shape[0]: return [], key res = [] embed_time = self.get_tensor_subbeat(x[start_idx]) time = start_subbeat expected_polyphony = 0 got_polyphony = 0 active_note_ends = dict() # pitch -> end subbeat desc_anchor_next = False desc_anchor_count = 0 last_dists_obj = None for i in range(start_idx, x.shape[0]): sym = x[i, 0].item() lastsym = x[i - 1, 0].item() if i > 0 else -1 if sym == self.pad.index: continue assert not strict or embed_time == self.get_tensor_subbeat( x[i] ), f"current embed time mismatch at {i}" assert ( not strict or len(active_note_ends) == x[i, 4].item() ), f"current embed polyphony mismatch at {i}" if self.is_duration(sym): duration = self.symbols[sym].value if dists is not None and i > x.shape[0] - dists.shape[0]: duration_dist = { d.value / self.quantize_divisions: dists[ i - x.shape[0] + dists.shape[0], d.index ].item() for d in self.durations.values() } normalize_dist(duration_dist) else: duration_dist = None if self.is_rest(lastsym): time += duration embed_time += duration assert ( not strict or expected_polyphony == got_polyphony ), f"expected polyphony mismatch at {i}" got_polyphony = 0 expected_polyphony = 1 for pitch, end in list(active_note_ends.items()): if end <= time: del active_note_ends[pitch] if last_dists_obj is not None and duration_dist is not None: last_dists_obj["restDuration"] = duration_dist elif self.is_pitch(lastsym): pitch = self.symbols[lastsym] note_obj = { "note": pitch.value, "onVelocity": pitch.velocity, "onBeat": time / self.quantize_divisions, "offBeat": (time + duration) / self.quantize_divisions, } if desc_anchor_next: desc_anchor_next = False note_obj["descriptionAnchor"] = True if dists is not None and i > x.shape[0] - dists.shape[0]: pitch_dist = { p: { value.velocity: dists[ i - x.shape[0] + dists.shape[0] - 1, value.index ].item() for value in v.values() } for p, v in self.pitches.items() } pitch_dist["rest"] = dists[ i - x.shape[0] + dists.shape[0] - 1, self.rest.index ].item() normalize_dist(pitch_dist) dists_obj = {} dists_obj["pitch"] = pitch_dist dists_obj["duration"] = duration_dist note_obj["dists"] = dists_obj last_dists_obj = dists_obj res.append(note_obj) got_polyphony += 1 if expected_polyphony == 0: expected_polyphony = 1 # tracks can start with a note or rest active_note_ends[pitch.value] = max( active_note_ends.get(pitch.value, 0), time + duration ) elif strict: raise ValueError("unexpected duration symbol") elif self.is_polyphony(sym): expected_polyphony = self.symbols[sym].value elif self.is_new_track(sym): assert not strict or expected_polyphony == got_polyphony, "polyphony" if return_accompany: return res, key res = [] embed_time = self.get_tensor_subbeat(x[i], next=True) time = start_subbeat expected_polyphony = 0 got_polyphony = 0 desc_anchor_next = False active_note_ends.clear() elif self.is_description_anchor(sym): desc_anchor_next = True desc_anchor_count += 1 assert ( not strict or desc_anchor_count == 1 ), "multiple description anchors" assert not strict or embed_time == self.get_tensor_subbeat( x[i], next=True ), f"next embed time mismatch at {i}" assert ( not strict or len(active_note_ends) == x[i, 8].item() ), f"next embed polyphony mismatch at {i}" return res, key def tensor_to_midi_using_embeds(self, x): res = [] tracks = [] desc_anchor_next = False for i in range(x.shape[0]): sym = x[i, 0].item() lastsym = x[i - 1, 0].item() if i > 0 else -1 if self.is_new_track(sym): tracks.append(res) res = [] desc_anchor_next = False if self.is_duration(sym) and self.is_pitch(lastsym): duration = self.symbols[sym].value lastsymobj = self.symbols[lastsym] time = self.get_tensor_subbeat(x[i]).item() noteobj = { "note": lastsymobj.value, "onVelocity": lastsymobj.velocity, "onBeat": time / self.quantize_divisions, "offBeat": (time + duration) / self.quantize_divisions, } if desc_anchor_next: desc_anchor_next = False noteobj["descriptionAnchor"] = True res.append(noteobj) elif self.is_description_anchor(sym): desc_anchor_next = True if len(res) > 0: tracks.append(res) return tracks # context = (cur_bar, cur_beat, cur_subbeat, cur_polyphony, next_bar, next_beat, next_subbeat, next_polyphony) # ys: tensor[batch, seq, (symbol, *context)] or None # sym: tensor[batch, j]) # inserting_subbeats: tensor[batch] # returns tensor[batch, seq + j, (symbol, *context)] # if seq_len is not None, the operation is in-place and seq_len is the length of the original sequence def append_symbols( self, ys, syms, inserting_subbeats=None, inserting_polyphonys=None, strict=True, seq_len=None, ): # fast indexing syms = syms.numpy() if isinstance(syms, torch.Tensor) else syms assert len(syms.shape) == 2, "syms must be tensor[batch, sym]" bsz, new = syms.shape if ys is None: assert seq_len is None old = 0 else: old = ys.shape[1] if seq_len is None else seq_len symcon_len = 9 if seq_len is None: new_ys_torch = torch.zeros(bsz, old + new, symcon_len, dtype=torch.long) else: assert ys.shape[1] >= old + new new_ys_torch = ys new_ys = new_ys_torch.numpy() if isinstance(new_ys_torch, torch.Tensor) else ys if ys is not None: ys = ys.numpy() if isinstance(ys, torch.Tensor) else ys assert ( len(ys.shape) == 3 and ys.shape[0] == bsz and ys.shape[-1] == symcon_len ), "ys must be tensor[batch, seq, (symbol, *context)]" if inserting_subbeats is None: inserting_subbeats = self.get_tensor_subbeat( ys[:, old - 1], next=True ).copy() if inserting_polyphonys is None: inserting_polyphonys = ys[:, old - 1, 8].copy() if seq_len is None: new_ys[:, :old] = ys new_ys[:, old : old + syms.shape[1], 0] = syms if inserting_subbeats is None: inserting_subbeats = np.zeros(bsz, dtype=np.int64) if inserting_polyphonys is None: inserting_polyphonys = np.zeros(bsz, dtype=np.int64) for i in range(bsz): # search backwards to find end times of currently active notes note_end_times = [] if inserting_polyphonys[i] > 0: p = inserting_polyphonys[i].item() t = inserting_subbeats[i].item() for j in range(2, old + 1): sym = ys[i, old - j, 0].item() nextsym = ys[i, old - j + 1, 0].item() if self.is_pitch(sym) and self.is_duration(nextsym): start_subbeat = self.get_tensor_subbeat(ys[i, old - j]).item() end_subbeat = start_subbeat + self.symbols[nextsym].value if end_subbeat > t: note_end_times.append(end_subbeat) p -= 1 if p == 0: break note_end_times.sort() # insert new symbols for j in range(new): sym = syms[i, j].item() lastsym = new_ys[i, old + j - 1, 0].item() if old + j > 0 else -1 self.subbeat_to_tensor( inserting_subbeats[i], tensor=new_ys[i, old + j, 1:4] ) new_ys[i, old + j, 4] = min( inserting_polyphonys[i], self.embed_polyphony_max ) if self.is_duration(sym): duration = self.symbols[sym].value if self.is_rest(lastsym): # increment inserting_subbeats inserting_subbeats[i] += duration if inserting_subbeats[i] >= self.embed_length_max: if strict: raise VocabError( f"subbeat {inserting_subbeats[i]} out of embedding range" ) else: inserting_subbeats[i] %= self.embed_length_max # update insert polyphony while ( len(note_end_times) > 0 and note_end_times[0] <= inserting_subbeats[i] ): note_end_times.pop(0) inserting_polyphonys[i] -= 1 elif self.is_pitch(lastsym): note_end_times.append(inserting_subbeats[i].item() + duration) note_end_times.sort() inserting_polyphonys[i] += 1 if inserting_polyphonys[i] > self.embed_polyphony_max: if strict: raise VocabError( f"polyphony {inserting_polyphonys[i]} out of embedding range" ) self.subbeat_to_tensor( inserting_subbeats[i], tensor=new_ys[i, old + j, 5:8] ) new_ys[i, old + j, 8] = min( inserting_polyphonys[i], self.embed_polyphony_max ) return new_ys_torch # x: tensor[seq, (symbol, *context)] or tensor[seq] def repr_tensor(self, x): quantize_digits = int(math.ceil(math.log10(self.quantize_divisions))) bar_digits = int( math.ceil( math.log10(self.embed_length_max // (4 * self.quantize_divisions)) ) ) poly_digits = int(math.ceil(math.log10(self.embed_polyphony_max))) def repr_time(t): return f"{t[0]:0{bar_digits}}.{t[1]}.{t[2]:0{quantize_digits}}" res = [] for i in range(x.shape[0]): if x.ndim > 1: f = "*" if isinstance(self.symbols[x[i, 0].item()], Control) else " " res.append( f"{f}{repr_time(x[i, 1:4])} {repr_time(x[i, 5:8])} p{x[i, 4].item():0{poly_digits}} pn{x[i, 8].item():0{poly_digits}} {self.symbols[x[i, 0].item()]}" ) else: res.append(str(self.symbols[x[i].item()])) return "\n".join(res) # x: tensor[seq, (symbol, *context)] -> bytes def hash_tensor(self, x): return hashlib.sha256(x.flatten().numpy().tobytes()).digest()[:16]