def adjust_sequence_times(sequence, delta_time): # pylint: disable-msg=no-member retimed_sequence = NoteSequence() retimed_sequence.CopyFrom(sequence) for note in retimed_sequence.notes: note.start_time += delta_time note.end_time += delta_time retimed_sequence.total_time += delta_time return retimed_sequence
def generate(bundle_name: str, sequence_generator, generator_id: str, primer_filename: str = None, qpm: float = DEFAULT_QUARTERS_PER_MINUTE, total_length_steps: int = 64, temperature: float = 1.0, beam_size: int = 1, branch_factor: int = 1, steps_per_iteration: int = 1) -> NoteSequence: """Generates and returns a new sequence given the sequence generator. Uses the bundle name to download the bundle in the "bundles" directory if it doesn't already exist, then uses the sequence generator and the generator id to get the generator. Parameters can be provided for the generation phase. The MIDI and plot files are written to disk in the "output" folder, with the filename pattern "<generator_name>_<generator_id>_<date_time>" with "mid" or "html" as extension respectively. :param bundle_name: The bundle name to be downloaded and generated with. :param sequence_generator: The sequence generator module, which is the python module in the corresponding models subfolder. :param generator_id: The id of the generator configuration, this is the model's configuration. :param primer_filename: The filename for the primer, which will be taken from the "primers" directory. If left empty, and empty note sequence will be used. :param qpm: The QPM for the generated sequence. If a primer is provided, the primer QPM will be used and this parameter ignored. :param total_length_steps: The total length of the sequence, which contains the added length of the primer and the generated sequence together. This value need to be bigger than the primer length in bars. :param temperature: The temperature value for the generation algorithm, lesser than 1 is less random (closer to the primer), bigger than 1 is more random :param beam_size: The beam size for the generation algorithm, a bigger branch size means the generation algorithm will generate more sequence each iteration, meaning a less random sequence at the cost of more time. :param branch_factor: The branch factor for the generation algorithm, a bigger branch factor means the generation algorithm will keep more sequence candidates at each iteration, meaning a less random sequence at the cost of more time. :param steps_per_iteration: The number of steps the generation algorithm generates at each iteration, a bigger steps per iteration meaning there are less iterations in total because more steps gets generated each time. :returns The generated NoteSequence """ # Downloads the bundle from the magenta website, a bundle (.mag file) is a # trained model that is used by magenta mm.notebook_utils.download_bundle(bundle_name, "bundles") bundle = mm.sequence_generator_bundle.read_bundle_file( os.path.join("bundles", bundle_name)) # Initialize the generator from the generator id, this need to fit the # bundle we downloaded before, and choose the model's configuration. generator_map = sequence_generator.get_generator_map() generator = generator_map[generator_id](checkpoint=None, bundle=bundle) generator.initialize() # Gets the primer sequence that is fed into the model for the generator, # which will generate a sequence based on this one. # If no primer sequence is given, the primer sequence is initialized # to an empty note sequence if primer_filename: primer_sequence = mm.midi_io.midi_file_to_note_sequence( os.path.join("primers", primer_filename)) else: primer_sequence = NoteSequence() # Gets the QPM from the primer sequence. If it wasn't provided, take the # parameters that defaults to Magenta's default if primer_sequence.tempos: if len(primer_sequence.tempos) > 1: raise Exception("No support for multiple tempos") qpm = primer_sequence.tempos[0].qpm # Calculates the seconds per 1 step, which changes depending on the QPM value # (steps per quarter in generators are mostly 4) seconds_per_step = 60.0 / qpm / getattr(generator, "steps_per_quarter", 4) # Calculates the primer sequence length in steps and time by taking the # total time (which is the end of the last note) and finding the next step # start time. primer_sequence_length_steps = math.ceil(primer_sequence.total_time / seconds_per_step) primer_sequence_length_time = primer_sequence_length_steps * seconds_per_step # Calculates the start and the end of the primer sequence. # We add a negative delta to the end, because if we don't some generators # won't start the generation right at the beginning of the bar, they will # start at the next step, meaning we'll have a small gap between the primer # and the generated sequence. primer_end_adjust = (0.00001 if primer_sequence_length_time > 0 else 0) primer_start_time = 0 primer_end_time = (primer_start_time + primer_sequence_length_time - primer_end_adjust) # Calculates the generation time by taking the total time and substracting # the primer time. The resulting generation time needs to be bigger than zero. generation_length_steps = total_length_steps - primer_sequence_length_steps if generation_length_steps <= 0: raise Exception("Total length in steps too small " + "(" + str(total_length_steps) + ")" + ", needs to be at least one bar bigger than primer " + "(" + str(primer_sequence_length_steps) + ")") generation_length_time = generation_length_steps * seconds_per_step # Calculates the generate start and end time, the start time will contain # the previously added negative delta from the primer end time. # We remove the generation end time delta to end the generation # on the last bar. generation_start_time = primer_end_time generation_end_time = (generation_start_time + generation_length_time + primer_end_adjust) # Showtime print(f"Primer time: [{primer_start_time}, {primer_end_time}]") print(f"Generation time: [{generation_start_time}, {generation_end_time}]") # Pass the given parameters, the generator options are common for all models generator_options = GeneratorOptions() generator_options.args['temperature'].float_value = temperature generator_options.args['beam_size'].int_value = beam_size generator_options.args['branch_factor'].int_value = branch_factor generator_options.args['steps_per_iteration'].int_value = steps_per_iteration generator_options.generate_sections.add( start_time=generation_start_time, end_time=generation_end_time) # Generates the sequence, add add the time signature # back to the generated sequence sequence = generator.generate(primer_sequence, generator_options) # Writes the resulting midi file to the output directory date_and_time = time.strftime('%Y-%m-%d_%H%M%S') generator_name = str(generator.__class__).split(".")[2] midi_filename = "%s_%s_%s.mid" % (generator_name, generator_id, date_and_time) midi_path = os.path.join("output", midi_filename) mm.midi_io.note_sequence_to_midi_file(sequence, midi_path) print(f"Generated midi file: {os.path.abspath(midi_path)}") # Writes the resulting plot file to the output directory date_and_time = time.strftime('%Y-%m-%d_%H%M%S') generator_name = str(generator.__class__).split(".")[2] plot_filename = "%s_%s_%s.html" % (generator_name, generator_id, date_and_time) plot_path = os.path.join("output", plot_filename) pretty_midi = mm.midi_io.note_sequence_to_pretty_midi(sequence) plotter = Plotter() plotter.save(pretty_midi, plot_path) print(f"Generated plot file: {os.path.abspath(plot_path)}") return sequence
def run(self): start_time = time() self._captor = self._midi_hub.start_capture(self._qpm, start_time) if not self._clock_signal and self._metronome_channel is not None: self._midi_hub.start_metronome(self._qpm, start_time, channel=self._metronome_channel) # Register callbacks if self._end_call_signal is not None: self._captor.register_callback(self._end_call_callback, signal=self._end_call_signal) if self._panic_signal is not None: self._captor.register_callback(self._panic_callback, signal=self._panic_signal) if self._mutate_signal is not None: self._captor.register_callback(self._mutate_callback, signal=self._mutate_signal) # Keep track of the end of the previous tick time. last_tick_time = time() # Keep track of the duration of a listen state. listen_ticks = 0 # Start with an empty response sequence. response_sequence = NoteSequence() response_start_time = 0 response_duration = 0 # Get handles for all required players player_melody = self._midi_hub.start_playback(response_sequence, playback_channel=1, allow_updates=True) player_bass = self._midi_hub.start_playback(response_sequence, playback_channel=2, allow_updates=True) player_chords = self._midi_hub.start_playback(response_sequence, playback_channel=3, allow_updates=True) player_drums = self._midi_hub.start_playback(response_sequence, playback_channel=9, allow_updates=True) # Song structure data part_in_song = 0 # index to STRUCTURE list bars_played = 0 # absolute number of bars played bars_played_for_part = 0 # number of bars played for the current part total_bars = self.STRUCTURE.duration(bars=True) part_duration = self.STRUCTURE[part_in_song].duration(bars=True) # Enter loop at each clock tick. for captured_sequence in self._captor.iterate( signal=self._clock_signal, period=self._tick_duration): if self._stop_signal.is_set(): break if self._panic.is_set(): response_sequence = NoteSequence() player_melody.update_sequence(response_sequence) player_bass.update_sequence(response_sequence) player_chords.update_sequence(response_sequence) player_drums.update_sequence(response_sequence) self._panic.clear() tick_time = captured_sequence.total_time # Set to current QPM, since it might have changed. if not self._clock_signal and self._metronome_channel is not None: self._midi_hub.start_metronome(self._qpm, tick_time, channel=self._metronome_channel) captured_sequence.tempos[0].qpm = self._qpm tick_duration = tick_time - last_tick_time if bars_played_for_part > part_duration: part_in_song += 1 bars_played_for_part = 0 if part_in_song >= len(self.STRUCTURE): break start_of_part = bars_played_for_part == 0 part_duration = self.STRUCTURE[part_in_song].duration(bars=True) part = self.STRUCTURE[part_in_song] logging.info( "Current Part: {} ({}) | Bars (Part): {}/{} | Bars (Total): {}/{}" .format(part.name, part_in_song, bars_played_for_part, part_duration, bars_played, total_bars)) response_start_time = tick_time capture_start_time = self._captor.start_time response_duration = part_duration * tick_duration last_tick_time = tick_time bars_played += 1 bars_played_for_part += 1 # If we are not at the beginning of a new part, there's nothing to do. if not start_of_part: continue if self.MELODY_CACHE[part.name]: logging.info("Pulling melody sequence from cache") melody_sequence = self.MELODY_CACHE[part.name].sequence response_start_time = self.MELODY_CACHE[ part.name].response_start_time else: logging.info("Generating new melody sequence") melody_sequence = self._generate( 0, captured_sequence, capture_start_time, response_start_time, response_start_time + response_duration) self.MELODY_CACHE[part.name] = CacheItem( melody_sequence, capture_start_time) if self.BASS_CACHE[part.name]: logging.info("Pulling bass sequence from cache") bass_sequence = self.BASS_CACHE[part.name].sequence response_start_time = self.BASS_CACHE[ part.name].response_start_time else: logging.info("Generating new bass sequence") bass_sequence = self._generate( 1, captured_sequence, capture_start_time, response_start_time, response_start_time + response_duration) self.BASS_CACHE[part.name] = CacheItem(bass_sequence, capture_start_time) if self.DRUM_CACHE[part.name]: logging.info("Pulling drum sequence from cache") drum_sequence = self.DRUM_CACHE[part.name].sequence response_start_time = self.DRUM_CACHE[ part.name].response_start_time else: logging.info("Generating new drum sequence") drum_sequence = self._generate( 2, captured_sequence, capture_start_time, response_start_time, response_start_time + response_duration) self.DRUM_CACHE[part.name] = CacheItem(drum_sequence, capture_start_time) size = getsizeof(self.MELODY_CACHE) + getsizeof( self.BASS_CACHE) + getsizeof(self.DRUM_CACHE) logging.info("Cache Size: {}KB".format(size // 8)) chord_sequence, notes = NoteSequence(), [] chords = part.get_midi_chords() for i, chord in enumerate(chords): for note in generate_midi_chord(chord, i / 2, 0.5): notes.append(note) notes = [ Note(note.pitch, note.velocity, note.start + response_start_time, note.start + note.end + response_start_time) for note in notes ] add_track_to_sequence(chord_sequence, 0, notes) # If it took too long to generate, push the response to next tick. if (time() - response_start_time) >= tick_duration / 4: push_ticks = ((time() - response_start_time) // tick_duration + 1) response_start_time += push_ticks * tick_duration melody_sequence = adjust_sequence_times( melody_sequence, push_ticks * tick_duration) bass_sequence = adjust_sequence_times( bass_sequence, push_ticks * tick_duration) chord_sequence = adjust_sequence_times( chord_sequence, push_ticks * tick_duration) drum_sequence = adjust_sequence_times( drum_sequence, push_ticks * tick_duration) self.MELODY_CACHE[ part.name].response_start_time = response_start_time self.BASS_CACHE[ part.name].response_start_time = response_start_time self.DRUM_CACHE[ part.name].response_start_time = response_start_time logging.warning( "Response too late. Pushing back {} ticks.".format( push_ticks)) # Start response playback. Specify start_time to avoid stripping initial events due to generation lag. player_melody.update_sequence(melody_sequence, start_time=response_start_time) player_bass.update_sequence(bass_sequence, start_time=response_start_time) player_chords.update_sequence(chord_sequence, start_time=response_start_time) player_drums.update_sequence(drum_sequence, start_time=response_start_time) player_melody.stop() player_bass.stop() player_chords.stop() player_drums.stop()