44100 BUFFER_SIZE = 128 require_relative "groovebox" require_relative "drum_rack" require_relative "synthesizer" require_relative "note" require_relative "vca" require_relative "step" require_relative "presets/bass" require_relative "presets/kick" require_relative "presets/snare" require_relative "presets/hihat_closed" require_relative "presets/piano" require_relative "sidechain" class Sequencer attr_reader :tracks, :steps_per_track DEFAULT_NOTES = ["C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",] NOTE_TO_MIDI = {} DEFAULT_NOTES.each_with_index do |note_name, idx| NOTE_TO_MIDI[note_name] = 48 + idx end def initialize(groovebox = nil, mid_file_path = nil) @groovebox = groovebox @current_position = 0 @current_track = 0 @current_voice = 0 # ݱࡏฤूதͷ෦ʢϙϦϑΥχʔͷΠϯσοΫεʣ @steps_per_track = 32 @tracks = [] @playing = false @bpm = 120 initialize_tracks if mid_file_path puts "Loading MIDI file: #{mid_file_path}" load_midi_file(mid_file_path) end end def initialize_tracks return if @groovebox.nil? @instruments = @groovebox.instruments @instruments.each_with_index do |instrument, idx| track_name = "Track #{idx}" if instrument.respond_to?(:pad_notes) # DrumRackͷ߹ैདྷ௨Γ instrument.pad_notes.sort.each do |pad_note| track = Array.new(@steps_per_track) { Step.new } @tracks << { name: "Drum #{pad_note}", instrument_index: idx, midi_note: pad_note, steps: track, polyphony: 1, # υϥϜৗʹ୯Ի voices: [0], # ෦ΠϯσοΫε } end else # γϯηαΠβʔͷ߹ϙϦϑΥχʔରԠ polyphony = @groovebox.get_polyphony(idx) voices = (0...polyphony).to_a # ֤෦ʢϙϦϑΥχʔʣʹରԠ͢ΔεςοϓྻΛੜ poly_steps = voices.map { Array.new(@steps_per_track) { Step.new } } @tracks << { name: track_name, instrument_index: idx, midi_note: nil, steps: poly_steps, polyphony: polyphony, voices: voices, } end end if @tracks.empty? @tracks = [ { name: "Default Track", instrument_index: 0, midi_note: nil, steps: [Array.new(@steps_per_track) { Step.new }], polyphony: 1, voices: [0], }, ] end end def load_midi_file(midi_file_path) seq = MIDI::Sequence.new File.open(midi_file_path, 'rb') do |file| seq.read(file) end puts "Loaded #{seq.tracks.size} tracks" # τϥοΫͷॳظԽʢϙϦϑΥχʔରԠʣ @tracks.each do |track_info| if track_info[:midi_note] # υϥϜτϥοΫͷ߹ track_info[:steps].each do |step| step.active = false step.note = nil step.velocity = nil end else # γϯηαΠβʔτϥοΫͷ߹ʢྻͷྻʣ track_info[:steps].each do |voice_steps| voice_steps.each do |step| step.active = false step.note = nil step.velocity = nil end end end end # Find BPM from tempo events seq.tracks.each do |track| tempo_events = track.events.select { |e| e.kind_of?(MIDI::Tempo) } if tempo_events.any? tempo_event = tempo_events.first @bpm = 60_000_000 / tempo_event.tempo break end end # See if we have any drum tracks to map to synth_tracks = @tracks.select { |t| t[:midi_note].nil? } drum_tracks = @tracks.select { |t| t[:midi_note] } # Skip the first track (usually just tempo/timing info) seq.tracks.each_with_index do |midi_track, track_index| next if track_index == 0 puts "MIDI Track #{track_index}: #{midi_track.name}" # Find all note-on events with velocity > 0 note_on_events = midi_track.events.select do |event| Sequencer is HARD