module Music module Performance class PartSequencer def initialize part, dynamics_sample_rate: 50, cents_per_step: 10 extractor = NoteSequenceExtractor.new(part.notes, cents_per_step) note_sequences = extractor.extract_sequences note_events = gather_note_events(note_sequences) dynamic_events = gather_dynamic_events(part.start_dynamic, part.dynamic_changes, dynamics_sample_rate) @events = (note_events + dynamic_events).sort_by {|x| x[0] } end def make_midi_track midi_sequence, part_name, channel, ppqn track = begin_track(midi_sequence, part_name, channel) prev_offset = 0 @events.each do |offset, event| if offset == prev_offset delta = 0 else delta = MidiUtil.delta(offset - prev_offset, ppqn) end track.events << case event when NoteOnEvent vel = MidiUtil.note_velocity(event.accented) MIDI::NoteOn.new(channel, event.notenum, vel, delta) when NoteOffEvent MIDI::NoteOff.new(channel, event.notenum, 127, delta) when VolumeExpressionEvent MIDI::Controller.new(channel, MIDI::CC_EXPRESSION_CONTROLLER, event.volume, delta) end prev_offset = offset end return track end private #def add_event events_hash, offset, event # if events_hash.has_key? offset # events_hash[offset].push event # else # events_hash[offset] = [ event ] # end #end def gather_note_events note_sequences note_events = [] note_sequences.each do |note_seq| pitches = note_seq.pitches.sort pitches.each_index do |i| offset, pitch = pitches[i] accented = false if note_seq.attacks.has_key?(offset) accented = note_seq.attacks[offset].accented? end note_num = MidiUtil.pitch_to_notenum(pitch) on_at = offset off_at = (i < (pitches.size - 1)) ? pitches[i+1][0] : note_seq.stop note_events.push [on_at, NoteOnEvent.new(note_num, accented)] note_events.push [off_at, NoteOffEvent.new(note_num)] end end return note_events end def gather_dynamic_events start_dyn, dyn_changes, sample_rate dynamic_events = [] dyn_comp = ValueComputer.new(start_dyn,dyn_changes) finish = 0 if dyn_changes.any? finish, change = dyn_changes.max if change.is_a? Music::Transcription::Change::Gradual finish += change.duration end end samples = dyn_comp.sample(0, finish, sample_rate) prev = nil samples.each_index do |i| sample = samples[i] unless sample == prev offset = Rational(i,sample_rate) volume = MidiUtil.dynamic_to_volume(sample) dynamic_events.push [offset, VolumeExpressionEvent.new(volume)] end prev = sample end return dynamic_events end def begin_track midi_sequence, part_name, channel # Track to hold part notes track = MIDI::Track.new(midi_sequence) # Name the track and instrument track.name = part_name.to_s track.instrument = MIDI::GM_PATCH_NAMES[0] # Add a volume controller event (optional). track.events << MIDI::Controller.new(channel, MIDI::CC_VOLUME, 127) # Change to particular instrument sound track.events << MIDI::ProgramChange.new(channel, 1, 0) return track end end end end