diff --git a/Gemfile.lock b/Gemfile.lock index 01d8a46..7e07e6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,7 @@ PATH remote: . specs: rcoremidi (0.0.1) + concurrent-ruby listen thor @@ -27,6 +28,7 @@ GEM childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) coderay (1.1.2) + concurrent-ruby (1.0.5) contracts (0.16.0) cucumber (3.0.0) builder (>= 2.1.2) @@ -47,9 +49,10 @@ GEM diff-lcs (1.3) ffi (1.9.18) gherkin (4.1.3) - listen (3.0.8) + listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) method_source (0.9.0) multi_json (1.12.2) multi_test (0.1.2) @@ -62,7 +65,7 @@ GEM rake (12.3.0) rake-compiler (1.0.4) rake - rb-fsevent (0.10.2) + rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) rdoc (4.3.0) @@ -82,6 +85,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.7.0) rspec-support (3.7.0) + ruby_dep (1.5.0) thor (0.20.0) PLATFORMS @@ -99,4 +103,4 @@ DEPENDENCIES rspec-its BUNDLED WITH - 1.16.0 + 1.16.1 diff --git a/ext/rcoremidi/client.c b/ext/rcoremidi/client.c index f0b2edb..4ff7e5a 100644 --- a/ext/rcoremidi/client.c +++ b/ext/rcoremidi/client.c @@ -3,13 +3,13 @@ static ByteCount max_packet_list_size = 65536; static unsigned long mspm = 60000000; static ByteCount note_on_packet_size = 2; static VALUE cb_thread; - - +static int midi_beat_start = 0; +static int last_midi_beat = 0; pthread_mutex_t g_callback_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t g_callback_cond = PTHREAD_COND_INITIALIZER; callback_t *g_callback_queue = NULL; -/* http://www.burgestrand.se//articles/asynchronous-callbacks-in-ruby-c-extensions/ */ +/* http://www.burgestrand.se/articles/asynchronous-callbacks-in-ruby-c-extensions/ */ void g_callback_queue_push(callback_t *callback) { callback->next = g_callback_queue; @@ -51,6 +51,10 @@ static void stop_waiting_for_callback_signal(void *w) pthread_mutex_unlock(&g_callback_mutex); } +static VALUE handle_callback(VALUE midi_clock, VALUE client, int argc, VALUE* argv) { + rb_funcall(client, rb_intern("on_tick"), 1, midi_clock); + return Qnil; +} static VALUE boot_callback_event_thread(void * data) { callback_waiting_t waiting = { @@ -61,16 +65,12 @@ static VALUE boot_callback_event_thread(void * data) { rb_thread_call_without_gvl(wait_for_callback_signal, &waiting, stop_waiting_for_callback_signal, &waiting); if (waiting.callback) { - RCoremidiNode *clientNode = (RCoremidiNode *)waiting.callback->data; - pthread_mutex_lock(&waiting.callback->mutex); - rb_funcall( - clientNode->rb_client_obj, - rb_intern("on_tick"), - 1, - UINT2NUM(clientNode->transport->bar) - ); - /* printf ("TRANSPORT: %d \n", clientNode->transport->tick_count); */ - pthread_mutex_unlock(&waiting.callback->mutex); + callback_t *cb = (callback_t*)waiting.callback; + RCoremidiNode *clientNode = (RCoremidiNode *)cb->data; + VALUE pool = rb_iv_get(clientNode->rb_client_obj, "@pool"); + VALUE *block_args = &clientNode->rb_client_obj; + VALUE midi_beat = UINT2NUM(clientNode->transport->tick_count); + rb_block_call(pool, rb_intern("post"), 1, &midi_beat, handle_callback, clientNode->rb_client_obj); } } return Qnil; @@ -82,8 +82,6 @@ static void midi_node_free(void *ptr) RCoremidiNode *tmp = ptr; if(tmp) { // May be (also) use MIDICLientDispose() from OSX API? - // Anyway is this usefull or should i just need to - // free the struct RCoremidiNode. Any just to make sure i free all free(tmp->client); free(tmp->transport); free(tmp->name); @@ -115,10 +113,7 @@ RCoremidiNode * client_get_data(VALUE self) { static RCoreMidiTransport * reset_transport(RCoreMidiTransport * transport) { transport->tick_count = 0; - transport->bar = 1; - transport->quarter = 0; - transport->eigth = 0; - transport->sixteinth = 0; + transport->state = kMIDIStop; return transport; } @@ -137,7 +132,6 @@ VALUE client_alloc(VALUE klass) clientNode->transport = transport_alloc(); clientNode->in = malloc(sizeof(MIDIPortRef)); clientNode->out = malloc(sizeof(MIDIPortRef)); - clientNode->out = malloc(sizeof(MIDIPortRef)); clientNode->callback = malloc(sizeof(callback_t)); pthread_mutex_init(&clientNode->callback->mutex, NULL); pthread_cond_init(&clientNode->callback->cond, NULL); @@ -175,10 +169,12 @@ static void notifyProc(const MIDINotification *notification, void *refCon) } } +static int calculate_current_tick(lsb, msb) { + int midi_beat_start = (msb << 7) | lsb; + return midi_beat_start * 6; +} static void MidiReadProc(const MIDIPacketList *pktlist, void *refCon, void *connRefCon) { - - pthread_mutex_lock(&g_callback_mutex); MIDIPacket *packet = (MIDIPacket *)pktlist->packet; unsigned int j; int i; @@ -192,45 +188,33 @@ static void MidiReadProc(const MIDIPacketList *pktlist, void *refCon, void *conn switch(packet->data[i]) { case kMIDIStart: - /* static MIDITimeStamp timestamp = 0; */ + transport->state = kMIDIStart; transport->current_timestamp = mach_absolute_time(); - clientNode->callback->data = (void *)clientNode; - - - g_callback_queue_push(clientNode->callback); - + transport->tick_count = -1; + break; + case kMIDIContinue: + transport->tick_count = last_midi_beat - 1; break; case kMIDIStop: - printf("Stoping Client...\n"); + transport->state = kMIDIStop; break; case kMIDITick: transport->tick_count++; - printf("%d\n", transport->tick_count); - if ((transport->tick_count % 96) == 0) { - clientNode->callback->data = (void *)clientNode; - - g_callback_queue_push(clientNode->callback); - - transport->bar++; - } - // quarter - if ((transport->tick_count % 24) == 0) { - transport->quarter++; - } - // eigth - if ((transport->tick_count % 12) == 0) { - transport->eigth++; - } - // sixteinth - if ((transport->tick_count % 8) == 0) { - transport->sixteinth++; - } + printf("C: tick_count %d\n", transport->tick_count); + pthread_mutex_lock(&g_callback_mutex); + clientNode->callback->data = (void *)clientNode; + g_callback_queue_push(clientNode->callback); + pthread_mutex_unlock(&g_callback_mutex); + pthread_cond_signal(&g_callback_cond); break; case kMIDISongPositionPointer: - reset_transport(transport); + last_midi_beat = calculate_current_tick(packet->data[i+1], packet->data[i+2]); + /* packet = MIDIPacketNext(packet); */ break; + default: + printf("DEFAULT packet : %04x\n", packet->data[i]); } @@ -245,8 +229,6 @@ static void MidiReadProc(const MIDIPacketList *pktlist, void *refCon, void *conn packet = MIDIPacketNext(packet); } - pthread_mutex_unlock(&g_callback_mutex); - pthread_cond_signal(&g_callback_cond); } VALUE connect_to(VALUE self, VALUE source) @@ -428,6 +410,7 @@ VALUE client_init(VALUE self, VALUE name) } rb_iv_set(self, "@name", name); + rb_iv_set(self, "@started", Qfalse); clientNode->rb_client_obj = self; @@ -435,7 +418,8 @@ VALUE client_init(VALUE self, VALUE name) cb_thread = rb_thread_create(boot_callback_event_thread, NULL); rb_funcall(cb_thread, rb_intern("abort_on_exception="), 1, Qtrue); rb_iv_set(self, "@cb_thread", cb_thread); - + rb_iv_set(self, "@midi_clock", UINT2NUM(0)); + rb_iv_set(self, "@pool", rb_funcall(rb_cCachedThreadPool, new_intern, 0, Qnil)); return self; } diff --git a/ext/rcoremidi/extconf.rb b/ext/rcoremidi/extconf.rb index 7f4ac0b..20a6dff 100644 --- a/ext/rcoremidi/extconf.rb +++ b/ext/rcoremidi/extconf.rb @@ -2,7 +2,7 @@ $CFLAGS = "-Wno-error=shorten-64-to-32 -g" $CPPFLAGS += " -g " -$CPPFLAGS += "-I/Users/yann/.rbenv/versions/2.1.1/include/ruby-2.1.0 -I/System/Library/Frameworks/CoreMIDI.framework/Headers " +$CPPFLAGS += "-I/System/Library/Frameworks/CoreMIDI.framework/Headers " $LDFLAGS += " -F/System/Library/Frameworks -framework CoreMIDI -framework CoreFoundation -framework CoreAudio -framework Carbon" dir_config("rcoremidi") create_makefile("rcoremidi") diff --git a/ext/rcoremidi/rcoremidi.c b/ext/rcoremidi/rcoremidi.c index bf0a52e..65a66ec 100644 --- a/ext/rcoremidi/rcoremidi.c +++ b/ext/rcoremidi/rcoremidi.c @@ -1,5 +1,6 @@ #include "rcoremidi.h" +VALUE rb_cCachedThreadPool; VALUE rb_mRCOREMIDI; VALUE rb_cMIDIObject; VALUE rb_cDevice; @@ -20,7 +21,6 @@ ID empty_intern; ID lock_intern; ID length_intern; ID pack_intern; -ID core_midi_cb_thread_intern; ID on_intern; ID off_intern; ID on_timestamp_intern; @@ -69,7 +69,6 @@ Init_rcoremidi() devices_intern = rb_intern("@@devices"); empty_intern = rb_intern("empty?"); length_intern = rb_intern("length"); - core_midi_cb_thread_intern = rb_intern("@@core_midi_cb_thread"); on_intern = rb_intern("on"); off_intern = rb_intern("off"); on_timestamp_intern = rb_intern("on_timestamp"); @@ -130,5 +129,13 @@ Init_rcoremidi() rb_define_method(rb_cClient, "send_packets", send_packets, 2); rb_define_attr(rb_cClient, "name", 1, 1); rb_define_attr(rb_cClient, "is_connected", 1, 1); + rb_define_attr(rb_cClient, "midi_in", 1, 1); + rb_define_attr(rb_cClient, "midi_out", 1, 1); + rb_require("concurrent"); + + /* TODO: fix as per the last comment here: https://stackoverflow.com/questions/6442920/how-do-i-extend-my-ruby-class-with-a-c-extension */ + + VALUE rb_cConcurrent = rb_const_get(rb_cObject, rb_intern("Concurrent")); + rb_cCachedThreadPool = rb_const_get(rb_cConcurrent, rb_intern("CachedThreadPool")); } diff --git a/ext/rcoremidi/rcoremidi.h b/ext/rcoremidi/rcoremidi.h index f68477b..24a0c88 100644 --- a/ext/rcoremidi/rcoremidi.h +++ b/ext/rcoremidi/rcoremidi.h @@ -1,6 +1,6 @@ #include -#include -#include +#include "ruby.h" +#include "ruby/thread.h" #include #include #include @@ -10,6 +10,9 @@ #include "client.h" #include "midi_object.h" +extern VALUE rb_cCachedThreadPool; + + extern VALUE rb_cConectionManager; extern VALUE rb_cDevice; @@ -35,18 +38,18 @@ extern const rb_data_type_t midi_endpoint_data_t; extern const rb_data_type_t midi_object_data_t; typedef struct callback_t { -pthread_mutex_t mutex; -pthread_cond_t cond; + pthread_mutex_t mutex; + pthread_cond_t cond; -struct callback_t *next; -void *data; -bool handled; + struct callback_t *next; + void *data; } callback_t; extern pthread_mutex_t g_callback_mutex; extern pthread_cond_t g_callback_cond; extern callback_t *g_callback_queue; +// callback queue typedef struct callback_waiting_t { callback_t *callback; bool abort; diff --git a/lib/rcoremidi.rb b/lib/rcoremidi.rb index dd0a2c4..dda97e9 100644 --- a/lib/rcoremidi.rb +++ b/lib/rcoremidi.rb @@ -1,5 +1,6 @@ require 'securerandom' require 'rcoremidi.bundle' +require 'rcoremidi/app_pathname' require 'rcoremidi/device' require 'rcoremidi/entity' require 'rcoremidi/source' diff --git a/lib/rcoremidi/application.rb b/lib/rcoremidi/application.rb index 1bbd29e..7aafe00 100644 --- a/lib/rcoremidi/application.rb +++ b/lib/rcoremidi/application.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'yaml' require 'rcoremidi/app_pathname' require 'rcoremidi/client' @@ -5,9 +7,7 @@ require 'listen' module RCoreMidi - - class Configuration - + class Configuration # :nodoc: attr_accessor :bpm, :logger def initialize @@ -16,20 +16,18 @@ def initialize end end - class Application + class Application # :nodoc: attr_reader :root, :connections - def self.config @@config ||= Configuration.new end def initialize(app_path) self.root = AppPathname.new(app_path).expand_path - raise "Could not find application root" unless root.valid? + raise 'Could not find application root' unless root.valid? end def run(daemon_mode = false) - load_connections! load_clips! @@ -37,20 +35,19 @@ def run(daemon_mode = false) if daemon_mode Process.daemon - Process.setproctitle('arcx:live %s' % root) + Process.setproctitle(format('arcx:live %s', root)) write_pid_file redirect_output - client.midi_in = midi_in client.midi_out = midi_out client.create_live config.logger = Logger.new(root.log_file) - trap 'SIGINT', Proc.new { + trap 'SIGINT', proc { client.dispose - puts "quiting" + puts 'quiting' exit(0) } @@ -62,9 +59,9 @@ def run(daemon_mode = false) config.logger = Logger.new(STDOUT) - trap 'SIGINT', Proc.new { + trap 'SIGINT', proc { client.dispose - puts "quiting" + puts 'quiting' exit(0) } @@ -74,9 +71,9 @@ def run(daemon_mode = false) def stop if root.pid_file.exist? - Process.kill("QUIT", Integer(root.pid_file.read)) + Process.kill('QUIT', Integer(root.pid_file.read)) else - config.logger.info("No Arcx application seems to be running") + config.logger.info('No Arcx application seems to be running') end end @@ -89,10 +86,11 @@ def config end private + attr_writer :root, :connections def listeners - @listeners ||= Listen.to(root.clips_dir, root.instruments_dir) do |modified, added, removed| + @listeners ||= Listen.to(root.clips_dir, root.instruments_dir) do |modified, added, _removed| (modified + added).flat_map.each do |file| load file end @@ -101,14 +99,14 @@ def listeners def wait_for_start listeners.start - config.logger.info "Arcx live successfully started" + config.logger.info 'Arcx live successfully started' sleep end def redirect_output FileUtils.mkdir_p root.log_file.dirname # FileUtils.touch root.log_file - root.log_file.chmod(0644) + root.log_file.chmod(0o644) $stderr.reopen(root.log_file, 'a') $stdout.reopen($stderr) $stdout.sync = $stderr.sync = true @@ -119,7 +117,7 @@ def write_pid_file check_pid else root.pid_file.open(::File::CREAT | ::File::EXCL | ::File::WRONLY) do |f| - f.write("#{Process.pid}") + f.write(Process.pid.to_s) end at_exit do root.pid_file.delete if root.pid_file.exist? @@ -168,7 +166,7 @@ def connection end def load_connections! - self.connections = YAML.load(root.connections_file.read) + self.connections = YAML.safe_load(root.connections_file.read) end def load_clips! diff --git a/lib/rcoremidi/client.rb b/lib/rcoremidi/client.rb index b4c2172..a66c911 100644 --- a/lib/rcoremidi/client.rb +++ b/lib/rcoremidi/client.rb @@ -1,24 +1,31 @@ -module RCoreMidi - class Client +# frozen_string_literal: true - attr_accessor :midi_in, :midi_out +module RCoreMidi + class Client # :nodoc: - def on_tick(bar) - to_send = live.generate_beats(bar).flatten.compact + def on_tick(m) + # return unless bar_start?(midi_beat_clock) + to_send = live.beats_for(m) + ap "m: #{m}" send_packets(midi_out, to_send) end def create_live connect! self.live = Live.new + live.load end private + attr_accessor :live def connect! connect_to midi_in end + def pool + @pool ||= Concurrent::CachedThreadPool.new + end end end diff --git a/lib/rcoremidi/clip.rb b/lib/rcoremidi/clip.rb index a1efb0c..6e55004 100644 --- a/lib/rcoremidi/clip.rb +++ b/lib/rcoremidi/clip.rb @@ -1,20 +1,25 @@ +# frozen_string_literal: true + require 'observer' require 'rcoremidi/notifier' module RCoreMidi - - class Clip - include RCoreMidi::Registrable, Observable + class Clip # :nodoc: + include Observable + include RCoreMidi::Registrable attr_reader :rythm_sequences, :name, :notifier - def initialize(name, &block) - self.name = name + def initialize(name) + self.name = name self.rythm_sequences = [] end - def note(pitch, beat_resolution, probabilities) - self.rythm_sequences << RythmSequence.new(pitch, beat_resolution, probabilities) + def note(pitch, rhythm_pattern) + rhythm_pattern = RythmSequence.new(pitch, rhythm_pattern) + rhythm_pattern.generate.each do |note, i| + beats[i] << note if note + end end def load(&block) @@ -24,13 +29,17 @@ def load(&block) notify_observers(self) end + def beats + @notes ||= Array.new(96) { [] } + end + private + attr_accessor :generator, :block attr_writer :rythm_sequences, :name, :notifier def log(msg) RCoreMidi::Application.config.logger.info msg end - end end diff --git a/lib/rcoremidi/daemonize.rb b/lib/rcoremidi/daemonize.rb index 8a5e0ef..39a60fd 100644 --- a/lib/rcoremidi/daemonize.rb +++ b/lib/rcoremidi/daemonize.rb @@ -1,52 +1,45 @@ -module Rcoremidi - module Daemonize +# frozen_string_literal: true +module Rcoremidi + module Daemonize # :nodoc: def daemonize exit if fork Process.setsid exit if fork yield set_process_title - Dir.chdir "/" + Dir.chdir '/' end def write_pid - unless root.pid_file.exist? - begin - root.pid_file.expand_path.open(::File::CREAT | ::File::EXCL | ::File::WRONLY){|f| f.write("#{Process.pid}") } - at_exit do - if root.pid_file.expand_path.exist? - root.pid_file.expand_path.delete - end - end - rescue Errno::EEXIST - check_pid - retry - end - end + return if root.pid_file.exist? + root.pid_file.expand_path.open(::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(Process.pid.to_s) } + at_exit { destroy_pid_file } + rescue Errno::EEXIST + check_pid + retry end def redirect_output FileUtils.touch root.log_file - root.log_file.chmod(0644) + root.log_file.chmod(0o644) $stderr.reopen(root.log_file, 'a') $stdout.reopen($stderr) $stdout.sync = $stderr.sync = true end def set_process_title - Process.setproctitle('arcx live:%d - in %s' % [config.bpm, root]) + Process.setproctitle(format('arcx live:%d - in %s', config.bpm, root)) end def check_pid - if root.pid_file.exist? - case pid_status - when :running, :not_owned - config.logger.info "A server is already running. Check #{root.pid_file}" - exit(1) - when :dead - root.pid_file.delete - end + return unless root.pid_file.exist? + case pid_status + when :running, :not_owned + config.logger.info "A server is already running. Check #{root.pid_file}" + exit(1) + when :dead + root.pid_file.delete end end @@ -54,7 +47,7 @@ def pid_status return :exited unless root.pid_file.exist? pid = root.pid_file.read.to_i return :dead if pid == 0 - Process.kill(0, pid) # check process status + Process.kill(0, pid) # check process status :running rescue Errno::ESRCH :dead @@ -63,11 +56,16 @@ def pid_status end def trap_signals - trap 'QUIT', Proc.new { - listeners.stop - client.dispose - exit(0) - } + trap('QUIT', proc { + listeners.stop + client.dispose + exit(0) + }) + end + + def destroy_pid_file + return unless root.pid_file.expand_path.exist? + root.pid_file.expand_path.delete end end end diff --git a/lib/rcoremidi/duration_calculator.rb b/lib/rcoremidi/duration_calculator.rb index 9646139..99e2c66 100644 --- a/lib/rcoremidi/duration_calculator.rb +++ b/lib/rcoremidi/duration_calculator.rb @@ -13,9 +13,10 @@ def initialize(bpm, ppqn = 96) # seixteenth note = 240 ticks def timestamps_for(note_index, resolution = 16) # A probabilities array of length 16 means - # our current resolution is an eigthth note - # 48 ticks is our current resolution - on = (mpt * 6 * note_index).round + # our current resolution is an 16th note + + # on = (mpt * 6 * note_index).round + on = 0 [on, (on + default_note_off_offset).round] end diff --git a/lib/rcoremidi/instrument.rb b/lib/rcoremidi/instrument.rb index 83a0731..926799c 100644 --- a/lib/rcoremidi/instrument.rb +++ b/lib/rcoremidi/instrument.rb @@ -1,63 +1,58 @@ +# frozen_string_literal: true + require 'rcoremidi/track' require 'observer' module RCoreMidi - - class Instrument - + class Instrument # :nodoc: include RCoreMidi::Registrable attr_reader :name + def initialize(name, channel, &block) self.name = name self.channel = channel self.block = block end - def generate_bar(bar) - track.generate(bar) + def generate_beat(current_tick) + track.bars[current_tick] end + def log(msg) RCoreMidi::Application.config.logger.info msg end - - - def play(bar, clip_name, enable_probability = false) - clp = clip(clip_name) - clp.add_observer(self) - if bar.is_a? Range - bar.each do |bar_index| - track.play(bar_index, clp, enable_probability) - end - track.reset_at = bar.max - else - track.play(bar, clp, enable_probability) - end + def play(bar, clip_name) + clip = find_clip(clip_name) + clip.add_observer(self) + track.play(bar, clip, channel) end def load instance_eval(&block) end - def update(clip) + def update(_clip) track.reset load end + def bar(number) + track.bars[number] + end + private attr_accessor :channel, :file, :block attr_writer :tracks, :name def track - @track ||= Track.new(channel) + @track ||= Track.new end - def clip(name) - raise ArgumentError.new("Clip #{name} not found.") unless c = Clip[name] - c + def find_clip(name) + raise ArgumentError, "Clip #{name} not found." unless clip = Clip[name] + clip end - end - end diff --git a/lib/rcoremidi/live.rb b/lib/rcoremidi/live.rb index cd2cae3..00a5f32 100644 --- a/lib/rcoremidi/live.rb +++ b/lib/rcoremidi/live.rb @@ -1,15 +1,30 @@ +# frozen_string_literal: true + require 'rcoremidi/duration_calculator' module RCoreMidi - - class Live + class Live # :nodoc: attr_reader :instruments - def generate_beats(bar) - Instrument.all.flat_map do |instrument| - instrument.generate_bar(bar) + PPQN = 24 + + def beats_for(current_tick) + puts "beats[#{current_tick}] = #{beats[current_tick]}" + beats[current_tick] + end + + def load(current_bar = 1) + beats.each_with_index do |beat, i| + Instrument.all.each do |instrument| + instrument_beats = instrument.bar(current_bar) + beats[i].concat(instrument_beats[i]) unless instrument_beats[i].empty? + end end + ap beats end + def beats + @beats ||= Array.new(96) { [] } + end end end diff --git a/lib/rcoremidi/note.rb b/lib/rcoremidi/note.rb index c8a3da7..e7f1219 100644 --- a/lib/rcoremidi/note.rb +++ b/lib/rcoremidi/note.rb @@ -2,8 +2,8 @@ module RCoreMidi class Note - attr_accessor :note, :velocity, :channel, :status_byte, :on_timestamp, :off_timestamp - private :note=, :velocity=, :channel=, :status_byte=, :on_timestamp= + attr_accessor :note, :velocity, :channel, :status_byte, :on_timestamp, :off_timestamp, :channel + private :note=, :velocity=, :status_byte=, :on_timestamp= NOTE_ON = 0x90 NOTE_OFF = 0x80 diff --git a/lib/rcoremidi/registrable.rb b/lib/rcoremidi/registrable.rb index dd707f9..a48eb80 100644 --- a/lib/rcoremidi/registrable.rb +++ b/lib/rcoremidi/registrable.rb @@ -14,7 +14,6 @@ def self.included(klass) end klass.define_singleton_method :register do |*args, &block| - # return self[args.first].load && self[args.first] if self[args.first] klass.instance_variable_get(:@registry)[args.first] ||= new(*args, &block) klass.instance_variable_get(:@registry)[args.first].load(&block) end diff --git a/lib/rcoremidi/rythm_sequence.rb b/lib/rcoremidi/rythm_sequence.rb index 399d5fb..d4fe28c 100644 --- a/lib/rcoremidi/rythm_sequence.rb +++ b/lib/rcoremidi/rythm_sequence.rb @@ -1,24 +1,30 @@ +# frozen_string_literal: true + require 'rcoremidi/probability_generator' module RCoreMidi + class RythmSequence # :nodoc: + + PULSE_PER_BAR = 96 - class RythmSequence - def initialize(pitch, beat_resolution, probabilities) - self.pitch = pitch - self.beat_resolution = beat_resolution - self.probabilities = probabilities + def initialize(pitch, probabilities, rhythm = probabilities.size) + self.pitch = pitch + self.delta = PULSE_PER_BAR / rhythm + self.probabilities = probabilities end - def generate(enable_probability, channel) + def generate probabilities.map.with_index do |probability, i| - next unless probability_generator[enable_probability].play?(probability) + next [nil, i] if probability.zero? + # probability_generator[enable_probability].play?(probability) - Note.new(pitch, *duration_calculator.timestamps_for(i), channel) + [Note.new(pitch, *duration_calculator.timestamps_for(i), nil), delta * i] end end private - attr_accessor :pitch, :probabilities, :beat_resolution + + attr_accessor :pitch, :probabilities, :delta def probability_generator @probability_generator ||= { @@ -31,5 +37,4 @@ def duration_calculator @duration_calculator ||= DurationCalculator.new(RCoreMidi::Application.config.bpm) end end - end diff --git a/lib/rcoremidi/track.rb b/lib/rcoremidi/track.rb index ef19c13..a1de3ea 100644 --- a/lib/rcoremidi/track.rb +++ b/lib/rcoremidi/track.rb @@ -1,43 +1,32 @@ +# frozen_string_literal: true + require 'rcoremidi/notifier' module RCoreMidi - - class Track - + class Track # :nodoc: attr_accessor :reset_at, :clips, :channel - def initialize(channel) - self.reset_at = 0 - self.channel = channel - end - - def generate(bar) - if bar == reset_at - return @clips = {} && [] + def play(bar, clip, channel = 0) + if bars[bar].nil? + clip.beats.each { |notes| notes.each { |note| note.channel = channel } } + bars[bar] = clip.beats + else + bars[bar].each_with_index do |existing_notes, i| + clip.beats[i].each { |note| note.channel = channel } + existing_notes |= clip.beats[i] + end end - - clip, enable_probability = bars[bar] - - return [] unless clip - clip.rythm_sequences.map do |rythm_sequence| - rythm_sequence.generate(enable_probability, channel).compact - end.inject(:+) end - def play(bar, clip, enable_probability) - bars[bar] = [clip, enable_probability] + def bars + @bars ||= {} end def reset @bars = {} end - private - attr_accessor :notifiers - - def bars - @bars ||= {} - end + attr_accessor :notifiers end end diff --git a/rcoremidi.gemspec b/rcoremidi.gemspec index b509d4b..e8febb8 100644 --- a/rcoremidi.gemspec +++ b/rcoremidi.gemspec @@ -23,6 +23,7 @@ the osx CoreMidi Framework} s.add_dependency 'thor' s.add_dependency 'listen' + s.add_dependency 'concurrent-ruby' s.add_development_dependency 'rdoc' s.add_development_dependency 'rspec' s.add_development_dependency 'rspec-its' diff --git a/spec/fixtures/clips/piano.rb b/spec/fixtures/clips/piano.rb new file mode 100644 index 0000000..bd05371 --- /dev/null +++ b/spec/fixtures/clips/piano.rb @@ -0,0 +1,5 @@ +RCoreMidi::Clip.register :piano do + note 'E5', [1, 0, 0] + note 'F#5', [0, 1, 0] + note 'G#5', [0, 0, 1] +end diff --git a/spec/fixtures/instruments/piano.rb b/spec/fixtures/instruments/piano.rb new file mode 100644 index 0000000..c21a553 --- /dev/null +++ b/spec/fixtures/instruments/piano.rb @@ -0,0 +1,3 @@ +RCoreMidi::Instrument.register :piano, 1 do + play 1, :piano +end diff --git a/spec/rcoremidi/clip_spec.rb b/spec/rcoremidi/clip_spec.rb index 3cd4208..fb606db 100644 --- a/spec/rcoremidi/clip_spec.rb +++ b/spec/rcoremidi/clip_spec.rb @@ -13,16 +13,35 @@ it_behaves_like 'registrable', [:clip_name] describe '#note' do - let(:rythm_sequence) { instance_double(RCoreMidi::RythmSequence) } - let(:pitch) { 'E5' } - let(:notes) { [1,0,0,0] * 4 } - let(:beat_resolution) { 16 } + let(:e5) { 'E5' } + let(:d5) { 'D5' } + let(:e5_rhythm) { [1,0,1,0] * 4 } + let(:d5_rhythm) { [1,1,1] * 4 } + let(:ppqn) { 24 } it 'parses RythmSequence' do - expect(RCoreMidi::RythmSequence).to receive(:new).with(pitch, beat_resolution, notes).and_return(rythm_sequence) - subject.note(pitch, beat_resolution, notes) + subject.note(e5, e5_rhythm) + subject.note(d5, d5_rhythm) - expect(subject.rythm_sequences).to eq([rythm_sequence]) + expect(subject.beats[0]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.beats[8]).to include(RCoreMidi::Note.new('D5')) + expect(subject.beats[12]).to include(RCoreMidi::Note.new('E5')) + expect(subject.beats[16]).to include(RCoreMidi::Note.new('D5')) + + expect(subject.beats[0 + ppqn]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.beats[8 + ppqn]).to include(RCoreMidi::Note.new('D5')) + expect(subject.beats[12 + ppqn]).to include(RCoreMidi::Note.new('E5')) + expect(subject.beats[16 + ppqn]).to include(RCoreMidi::Note.new('D5')) + + expect(subject.beats[0 + ppqn * 2]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.beats[8 + ppqn * 2]).to include(RCoreMidi::Note.new('D5')) + expect(subject.beats[12 + ppqn * 2]).to include(RCoreMidi::Note.new('E5')) + expect(subject.beats[16 + ppqn * 2]).to include(RCoreMidi::Note.new('D5')) + + expect(subject.beats[0 + ppqn * 3]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.beats[8 + ppqn * 3]).to include(RCoreMidi::Note.new('D5')) + expect(subject.beats[12 + ppqn * 3]).to include(RCoreMidi::Note.new('E5')) + expect(subject.beats[16 + ppqn * 3]).to include(RCoreMidi::Note.new('D5')) end end end diff --git a/spec/rcoremidi/live_spec.rb b/spec/rcoremidi/live_spec.rb index 74284b6..fc39920 100644 --- a/spec/rcoremidi/live_spec.rb +++ b/spec/rcoremidi/live_spec.rb @@ -6,9 +6,13 @@ let(:clip_dir) { File.join('spec', 'fixtures', 'clips') } let(:instruments_dir) { File.join('spec', 'fixtures', 'instruments') } - subject { described_class.new(bpm, clip_dir, instruments_dir) } + # subject { described_class.new(bpm, clip_dir, instruments_dir) } - describe '#load_clips' do + before do + Dir["#{clip_dir}/**/*.rb"].each { |f| load f } + Dir["#{instruments_dir}/**/*.rb"].each { |f| load f } + end + describe '#load_clip' do it 'loads clips from file' do subject expect(RCoreMidi::Clip[:drum]).to be_instance_of(RCoreMidi::Clip) @@ -22,15 +26,27 @@ end end - describe '#generate_beats' do - let(:duration_calculator) { RCoreMidi::DurationCalculator.new(120) } - it "return notes" do - expect(subject.generate_beats(1)).to eq([ - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(0)), - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(4)), - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(8)), - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(12)), - ]) + describe '#load' do + + it "loads the given bar" do + subject.load + ap subject.beats + # expect(subject.bars).to eq({}) + # RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(0)), + # RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(4)), + # RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(8)), + # RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(12)), + end + end + + describe '#sequence!' do + let(:sequencer) { RCoreMidi::Sequencer } + before do + expect(RCoreMidi::Sequencer).to receive(:new).and_return(sequencer) + end + + it 'prepares the first bar sequence' do + subject.sequence! end end end diff --git a/spec/rcoremidi/track_spec.rb b/spec/rcoremidi/track_spec.rb index 1bf158b..875b139 100644 --- a/spec/rcoremidi/track_spec.rb +++ b/spec/rcoremidi/track_spec.rb @@ -2,48 +2,47 @@ RSpec.describe RCoreMidi::Track do - let(:clip) do - RCoreMidi::Clip.new(:drum) do - note 'E5', [1, 0, 0, 0] * 4 + before do + RCoreMidi::Clip.register(clip_name) do + note 'E5', [1,0,1,0] * 4 + note 'D5', [1,1,1] * 4 end end - let(:bar) { 1 } - let(:enable_probability) { false } - let(:clips) { {} } + let(:ppqn) { 24 } + let(:clip_name) { :drum } + let(:clip) { RCoreMidi::Clip[clip_name] } describe '#play' do - - it "stores what clip to play for a given bar and configuration" do - expect(subject).to receive(:clips).and_return(clips) - - subject.play bar, clip, enable_probability - - expect(clips).to eq(bar => [clip, enable_probability]) - end - - end - - describe '#generate' do - - let(:duration_calculator) { RCoreMidi::DurationCalculator.new(120) } - - before do - subject.play(1, clip, enable_probability) - end - - context 'with no probability generator disabled' do - it "generates notes" do - expect(subject.generate(1, duration_calculator)).to eq( - [ - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(0)), - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(4)), - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(8)), - RCoreMidi::Note.new('E5', *duration_calculator.timestamps_for(12)), - ] - ) + context 'when given a single bar' do + let(:bar) { 1 } + + context 'when there are not notes yet' do + it "add clip notes to given bar" do + subject.play bar, clip + + expect(subject.bars[bar]).to eq(clip.beats) + expect(subject.bars[bar][0]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][8]).to include(RCoreMidi::Note.new('D5')) + expect(subject.bars[bar][12]).to include(RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][16]).to include(RCoreMidi::Note.new('D5')) + + expect(subject.bars[bar][0 + ppqn]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][8 + ppqn]).to include(RCoreMidi::Note.new('D5')) + expect(subject.bars[bar][12 + ppqn]).to include(RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][16 + ppqn]).to include(RCoreMidi::Note.new('D5')) + + expect(subject.bars[bar][0 + ppqn * 2]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][8 + ppqn * 2]).to include(RCoreMidi::Note.new('D5')) + expect(subject.bars[bar][12 + ppqn * 2]).to include(RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][16 + ppqn * 2]).to include(RCoreMidi::Note.new('D5')) + + expect(subject.bars[bar][0 + ppqn * 3]).to include(RCoreMidi::Note.new('D5'), RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][8 + ppqn * 3]).to include(RCoreMidi::Note.new('D5')) + expect(subject.bars[bar][12 + ppqn * 3]).to include(RCoreMidi::Note.new('E5')) + expect(subject.bars[bar][16 + ppqn * 3]).to include(RCoreMidi::Note.new('D5')) + end end end end - end