midi-topaz 0.1.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3f0c8761ea2448cd3a28a29b15a1c8779e002be9
4
- data.tar.gz: 03f8b39884bb6cc77cabed0d27f93eb553a08978
3
+ metadata.gz: 0ce370c533a7d6bf26395da93632c35985e939c3
4
+ data.tar.gz: 2d225a7fd71f4fe2390278fd63c1c50e4ac3e3b1
5
5
  SHA512:
6
- metadata.gz: 884034e8940159b9997770a4ab7ac2e9eb1aa598aec6d1b58bcb14315f0077d25d7aa4ff5438c87a899e0c72ebd22e674dd3e0b2aa91497865b8c3bfd2b7f2d2
7
- data.tar.gz: b2e526421f8f9450fd0d36d46ec685eb4c3918dedd0637b7312eeeb1dc8bb0ac68c13fc83d70efc3d344bd4368ea0fff984a1b6ae92b6abd42066a5f566dfb51
6
+ metadata.gz: 563c0fcec28ebd826488239e42b95ebab118b1d35f13bf50e590f2d44adff102da9a434c63990f370cbb9b1df20fa3ec8969a9269119a7b860e653c0ba31e4e2
7
+ data.tar.gz: 8205a2ec2232efb86edb38d2ec0280cd4c69516ad6334e861fc8160ca03b19367551dc3fa3bc40c335b3121c1cd29814c022fe132d550b88cab964a9b123cc2f
data/README.md CHANGED
@@ -36,7 +36,7 @@ sequencer = Sequencer.new
36
36
  The simplest application of Topaz is to create a clock to step that sequencer at a given rate. Using timing generated internally by your computer, the passed in block will be called repeatedly at 130 BPM
37
37
 
38
38
  ```ruby
39
- @tempo = Topaz::Tempo.new(130) { sequencer.step }
39
+ @tempo = Topaz::Clock.new(130) { sequencer.step }
40
40
  ```
41
41
 
42
42
  You may also use another MIDI device to generate timing and control the tempo. The unimidi input to which that device is connected can be passed to the Tempo constructor
@@ -44,7 +44,7 @@ You may also use another MIDI device to generate timing and control the tempo.
44
44
  ```ruby
45
45
  @input = UniMIDI::Input.first.open # an midi input
46
46
 
47
- @tempo = Topaz::Tempo.new(@input) { sequencer.step }
47
+ @tempo = Topaz::Clock.new(@input) { sequencer.step }
48
48
  ```
49
49
 
50
50
  Topaz can also act as a master clock. If a MIDI output is passed to Topaz, MIDI start, stop and clock signals will automatically be sent to that output at the appropriate time
@@ -52,7 +52,7 @@ Topaz can also act as a master clock. If a MIDI output is passed to Topaz, MIDI
52
52
  ```ruby
53
53
  @output = UniMIDI::Output.first.open # a midi output
54
54
 
55
- @tempo = Topaz::Tempo.new(120, :midi => @output) do
55
+ @tempo = Topaz::Clock.new(120, :midi => @output) do
56
56
  sequencer.step
57
57
  end
58
58
  ```
@@ -60,7 +60,7 @@ end
60
60
  Input and multiple outputs can be used simultaneously
61
61
 
62
62
  ```ruby
63
- @tempo = Topaz::Tempo.new(@input, :midi => [@output1, @output2]) do
63
+ @tempo = Topaz::Clock.new(@input, :midi => [@output1, @output2]) do
64
64
  sequencer.step
65
65
  end
66
66
  ```
@@ -78,7 +78,7 @@ If you are syncing to external clock, nothing will happen until a "start" or "cl
78
78
  Whether or not you are using an internal or external clock source, the event block will be called at quarter note intervals by default. If you wish to change this set the option :interval. In this case, the event will be fired 4 times per beat (16th notes) at 138 BPM
79
79
 
80
80
  ```ruby
81
- @tempo = Topaz::Tempo.new(138, :interval => 16) do
81
+ @tempo = Topaz::Clock.new(138, :interval => 16) do
82
82
  sequencer.step
83
83
  end
84
84
  ```
data/lib/topaz/api.rb ADDED
@@ -0,0 +1,19 @@
1
+ module Topaz
2
+
3
+ # Convenience shortcuts for the clock
4
+ module API
5
+
6
+ def self.included(base)
7
+ base.send(:extend, Forwardable)
8
+ base.send(:def_delegators, :source, :interval, :interval=, :join,
9
+ :pause, :pause?, :paused?, :running?, :tempo, :toggle_pause, :unpause)
10
+ end
11
+
12
+ # Alias for Clock#time
13
+ # @return [Time]
14
+ def time_since_start
15
+ time
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,187 @@
1
+ module Topaz
2
+
3
+ # The main tempo clock
4
+ class Clock
5
+
6
+ include API
7
+
8
+ attr_reader :event, :midi_output, :source, :trigger
9
+
10
+ # @param [Fixnum, UniMIDI::Input] tempo_or_input
11
+ # @param [Hash] options
12
+ # @param [Proc] tick_event
13
+ def initialize(tempo_or_input, options = {}, &tick_event)
14
+ # The MIDI clock output is initialized regardless of whether there are devices
15
+ # so that it is ready if any are added during the running process.
16
+ @midi_output = MIDIClockOutput.new(:devices => options[:midi])
17
+ @event = Event.new
18
+ @trigger = EventTrigger.new
19
+ @source = TempoSource.new(tempo_or_input, options.merge({ :event => @event }))
20
+ initialize_events(&tick_event)
21
+ end
22
+
23
+ # Set the tempo
24
+ #
25
+ # If external MIDI tempo is being used, this will switch to internal tempo at the desired rate.
26
+ #
27
+ # @param [Fixnum] value
28
+ # @return [ExternalMIDITempo, InternalTempo]
29
+ def tempo=(value)
30
+ if @source.respond_to?(:tempo=)
31
+ @source.tempo = value
32
+ else
33
+ @source = TempoSource.new(event, tempo_or_input)
34
+ end
35
+ end
36
+
37
+ # This will start the clock source
38
+ #
39
+ # In the case that external midi tempo is being used, this will instead start the process
40
+ # of waiting for a start or clock message
41
+ #
42
+ # @param [Hash] options
43
+ # @option options [Boolean] :background Whether to run the timer in a background thread (default: false)
44
+ # @return [Boolean]
45
+ def start(options = {})
46
+ @start_time = Time.now
47
+ begin
48
+ @source.start(options)
49
+ rescue SystemExit, Interrupt => exception
50
+ stop
51
+ end
52
+ true
53
+ end
54
+
55
+ # This will stop the clock source
56
+ # @param [Hash] options
57
+ # @return [Boolean]
58
+ def stop(options = {})
59
+ @source.stop(options)
60
+ @start_time = nil
61
+ true
62
+ end
63
+
64
+ # Seconds since start was called
65
+ # @return [Float]
66
+ def time
67
+ (Time.now - @start_time).to_f unless @start_time.nil?
68
+ end
69
+
70
+ private
71
+
72
+ # Initialize the tick and MIDI clock events so that they can be passed to the source
73
+ # and fired when needed
74
+ # @param [Proc] block
75
+ # @return [Clock::Event]
76
+ def initialize_events(&block)
77
+ @event.tick << block if block_given?
78
+ clock = proc do
79
+ if @trigger.stop?
80
+ stop
81
+ else
82
+ @midi_output.do_clock
83
+ end
84
+ end
85
+ @event.clock = clock
86
+ @event.start << proc { @midi_output.do_start }
87
+ @event.stop << proc { @midi_output.do_stop }
88
+ @event
89
+ end
90
+
91
+ # Trigger clock events
92
+ class EventTrigger
93
+
94
+ def initialize
95
+ @stop = []
96
+ end
97
+
98
+ # Pass in a callback which will stop the clock if it evaluates to true
99
+ # @param [Proc] callback
100
+ # @return [Array<Proc>]
101
+ def stop(&callback)
102
+ if block_given?
103
+ @stop.clear
104
+ @stop << callback
105
+ end
106
+ @stop
107
+ end
108
+
109
+ # Should the stop event be triggered?
110
+ # @return [Boolean]
111
+ def stop?
112
+ !@stop.nil? && @stop.any?(&:call)
113
+ end
114
+
115
+ end
116
+
117
+ # Clock events
118
+ class Event
119
+
120
+ attr_accessor :clock
121
+
122
+ def initialize
123
+ @start = []
124
+ @stop = []
125
+ @tick = []
126
+ end
127
+
128
+ # @return [Array]
129
+ def do_clock
130
+ !@clock.nil? && @clock.call
131
+ end
132
+
133
+ # Pass in a callback that is called when start is called
134
+ # @param [Proc] callback
135
+ # @return [Array<Proc>]
136
+ def start(&callback)
137
+ if block_given?
138
+ @start.clear
139
+ @start << callback
140
+ end
141
+ @start
142
+ end
143
+
144
+ # @return [Array]
145
+ def do_start
146
+ @start.map(&:call)
147
+ end
148
+
149
+ # pass in a callback that is called when stop is called
150
+ # @param [Proc] callback
151
+ # @return [Array<Proc>]
152
+ def stop(&callback)
153
+ if block_given?
154
+ @stop.clear
155
+ @stop << callback
156
+ end
157
+ @stop
158
+ end
159
+
160
+ # @return [Array]
161
+ def do_stop
162
+ @stop.map(&:call)
163
+ end
164
+
165
+ # Pass in a callback which will be fired on each tick
166
+ # @param [Proc] callback
167
+ # @return [Array<Proc>]
168
+ def tick(&callback)
169
+ if block_given?
170
+ @tick.clear
171
+ @tick << callback
172
+ end
173
+ @tick
174
+ end
175
+
176
+ # @return [Array]
177
+ def do_tick
178
+ @tick.map(&:call)
179
+ end
180
+
181
+ end
182
+
183
+ end
184
+
185
+ Tempo = Clock # For backwards compat
186
+
187
+ end
@@ -0,0 +1,186 @@
1
+ module Topaz
2
+
3
+ # Trigger an event based on received midi clock messages
4
+ class MIDIClockInput
5
+
6
+ include Pausable
7
+
8
+ attr_reader :clock, :listening, :running
9
+ alias_method :listening?, :listening
10
+ alias_method :running?, :running
11
+
12
+ # @param [UniMIDI::Input] input
13
+ # @param [Hash] options
14
+ # @option options [Clock::Event] :event
15
+ def initialize(input, options = {})
16
+ @event = options[:event]
17
+ @tick_counter = 0
18
+ @pause = false
19
+ @listening = false
20
+ @running = false
21
+ @tempo_calculator = TempoCalculator.new
22
+ @tick_threshold = interval_to_ticks(options.fetch(:interval, 4))
23
+
24
+ initialize_listener(input)
25
+ end
26
+
27
+ # This will return a calculated tempo
28
+ # @return [Fixnum]
29
+ def tempo
30
+ @tempo_calculator.calculate
31
+ end
32
+
33
+ # Start the listener
34
+ # @param [Hash] options
35
+ # @option options [Boolean] :background Whether to run the listener in a background process
36
+ # @option options [Boolean] :focus (or :blocking) Whether to run the listener in a foreground process
37
+ # @return [MIDIInputClock] self
38
+ def start(options = {})
39
+ @listening = true
40
+ blocking = options[:focus] || options[:blocking]
41
+ background = options[:background] || blocking.nil? || blocking.eql?(false)
42
+ @listener.start(:background => background)
43
+ self
44
+ end
45
+
46
+ # Stop the listener
47
+ # @return [MIDIInputClock] self
48
+ def stop(*a)
49
+ @listening = false
50
+ @listener.stop
51
+ self
52
+ end
53
+
54
+ # Join the listener thread
55
+ # @return [MIDIInputClock] self
56
+ def join
57
+ @listener.join
58
+ self
59
+ end
60
+
61
+ # Change the clock interval
62
+ # Defaults to 4, which means click once every 24 ticks or one quarter note (per MIDI spec).
63
+ # Therefore, to fire the on_tick event twice as often, pass 8
64
+ #
65
+ # 1 = whole note
66
+ # 2 = half note
67
+ # 4 = quarter note
68
+ # 6 = dotted quarter
69
+ # 8 = eighth note
70
+ # 16 = sixteenth note
71
+ # etc
72
+ #
73
+ # @param [Fixnum] interval
74
+ # @return [Fixnum]
75
+ def interval=(interval)
76
+ @tick_threshold = interval_to_ticks(interval)
77
+ end
78
+
79
+ # Return the interval at which the tick event is fired
80
+ # @return [Fixnum]
81
+ def interval
82
+ ticks_to_interval(@tick_threshold)
83
+ end
84
+
85
+ private
86
+
87
+ # Convert a note interval to number of ticks
88
+ # @param [Fixnum] interval
89
+ # @param [Fixnum]
90
+ def interval_to_ticks(interval)
91
+ per_qn = interval / 4
92
+ 24 / per_qn
93
+ end
94
+
95
+ # Convert a number of ticks to a note interval
96
+ # @param [Fixnum] ticks
97
+ # @param [Fixnum]
98
+ def ticks_to_interval(ticks)
99
+ note_value = 24 / ticks
100
+ 4 * note_value
101
+ end
102
+
103
+ # Initialize the MIDI input listener
104
+ # @param [UniMIDI::Input] input
105
+ # @return [MIDIEye::Listener]
106
+ def initialize_listener(input)
107
+ @listener = MIDIEye::Listener.new(input)
108
+ @listener.listen_for(:name => "Clock") { |message| handle_clock_message(message) }
109
+ @listener.listen_for(:name => "Start") { handle_start_message }
110
+ @listener.listen_for(:name => "Stop") { handle_stop_message }
111
+ @listener
112
+ end
113
+
114
+ # Handle a received start message
115
+ # @return [Boolean]
116
+ def handle_start_message
117
+ @running = true
118
+ if !@event.nil?
119
+ @event.do_start
120
+ true
121
+ end
122
+ end
123
+
124
+ # Handle a received stop message
125
+ # @return [Boolean]
126
+ def handle_stop_message
127
+ @running = false
128
+ if !@event.nil?
129
+ @event.do_stop
130
+ true
131
+ end
132
+ end
133
+
134
+ # Handle a received clock message
135
+ # @param [Hash] message
136
+ # @return [Fixnum] The current counter
137
+ def handle_clock_message(message)
138
+ @running ||= true
139
+ thru
140
+ log(message)
141
+ tick? ? tick : advance
142
+ end
143
+
144
+ # Advance the tick counter
145
+ # @return [Fixnum]
146
+ def advance
147
+ @tick_counter += 1
148
+ end
149
+
150
+ # Log the timestamp of a message for tempo calculation
151
+ # @param [Hash] message
152
+ # @return [Array<Fixnum>]
153
+ def log(message)
154
+ time = message[:timestamp] / 1000.0
155
+ @tempo_calculator.timestamps << time
156
+ end
157
+
158
+ # Fire the clock event
159
+ # (this results in MIDI output sending clock, thus thru)
160
+ # @return [Boolean]
161
+ def thru
162
+ if !@event.nil?
163
+ @event.do_clock
164
+ true
165
+ end
166
+ end
167
+
168
+ # Fire the tick event
169
+ # @return [Boolean]
170
+ def tick
171
+ @tick_counter = 0
172
+ if !@event.nil? && !@pause
173
+ @event.do_tick
174
+ true
175
+ end
176
+ end
177
+
178
+ # Should the tick event be fired given the current state?
179
+ # @return [Boolean]
180
+ def tick?
181
+ @tick_counter >= (@tick_threshold - 1)
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -0,0 +1,49 @@
1
+ module Topaz
2
+
3
+ # Send clock messages via MIDI
4
+ class MIDIClockOutput
5
+
6
+ attr_reader :devices
7
+
8
+ # @param [Hash] options
9
+ # @option options [Array<UniMIDI::Output>, UniMIDI::Output] :device
10
+ def initialize(options)
11
+ device = options[:device] || options[:devices]
12
+ @devices = [device].flatten.compact
13
+ end
14
+
15
+ # Send a start message
16
+ # @return [Boolean] Whether a message was emitted
17
+ def do_start(*a)
18
+ start = MIDIMessage::SystemRealtime["Start"].new
19
+ emit(start)
20
+ !@devices.empty?
21
+ end
22
+
23
+ # Send a stop message
24
+ # @return [Boolean] Whether a message was emitted
25
+ def do_stop(*a)
26
+ stop = MIDIMessage::SystemRealtime["Stop"].new
27
+ emit(stop)
28
+ !@devices.empty?
29
+ end
30
+
31
+ # Send a clock tick message
32
+ # @return [Boolean] Whether a message was emitted
33
+ def do_clock(*a)
34
+ clock = MIDIMessage::SystemRealtime["Clock"].new
35
+ emit(clock)
36
+ !@devices.empty?
37
+ end
38
+
39
+ private
40
+
41
+ # Emit a message to the devices
42
+ # @param [MIDIMessage] message
43
+ def emit(message)
44
+ @devices.each { |device| device.puts(*message.to_bytes) }
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,32 @@
1
+ module Topaz
2
+
3
+ # Pause functionality
4
+ module Pausable
5
+
6
+ # Pause the clock
7
+ # @return [Boolean]
8
+ def pause
9
+ @pause = true
10
+ end
11
+
12
+ # Unpause the clock
13
+ # @return [Boolean]
14
+ def unpause
15
+ @pause = false
16
+ end
17
+
18
+ # Is this clock paused?
19
+ # @return [Boolean]
20
+ def paused?
21
+ @pause
22
+ end
23
+ alias_method :pause?, :paused?
24
+
25
+ # Toggle pausing the clock
26
+ # @return [Boolean]
27
+ def toggle_pause
28
+ @pause = !@pause
29
+ end
30
+
31
+ end
32
+ end
@@ -3,7 +3,7 @@ module Topaz
3
3
  # Calculate tempo given timestamps
4
4
  class TempoCalculator
5
5
 
6
- THRESHOLD = 6 # minimum number of ticks to analyze
6
+ THRESHOLD = 6 # Optimal number of ticks to analyze
7
7
 
8
8
  attr_reader :tempo, :timestamps
9
9
 
@@ -13,25 +13,42 @@ module Topaz
13
13
  end
14
14
 
15
15
  # Analyze the tempo based on the threshold
16
- def find_tempo
16
+ # @return [Float, nil] The tempo as a float, or nil if there's not enough data to calculate it
17
+ def calculate
17
18
  tempo = nil
18
- diffs = []
19
- @timestamps.shift while @timestamps.length > THRESHOLD
20
- @timestamps.each_with_index { |n, i| (diffs << (@timestamps[i+1] - n)) unless @timestamps[i+1].nil? }
21
- unless diffs.empty?
22
- avg = (diffs.inject { |a, b| a + b }.to_f / diffs.length.to_f)
23
- tempo = ppq24_millis_to_bpm(avg)
24
- end
25
- @tempo = tempo
19
+ if @timestamps.count >= 2
20
+ limit_timestamps
21
+ deltas = get_deltas
22
+ sum = deltas.inject(&:+)
23
+ average = sum.to_f / deltas.count
24
+ bpm = ppq24_millis_to_bpm(average)
25
+ @tempo = bpm
26
+ end
26
27
  end
27
28
 
28
29
  private
29
-
30
- # convert the raw tick intervals to bpm
30
+
31
+ # Limit the timestamp list to within the threshold
32
+ # @return [Array<Time>, nil]
33
+ def limit_timestamps
34
+ if @timestamps.count > THRESHOLD
35
+ @timestamps.slice!(THRESHOLD - @timestamps.count, THRESHOLD)
36
+ @timstamps
37
+ end
38
+ end
39
+
40
+ # Get the delta values between the timestamps
41
+ # @return [Array<Float>]
42
+ def get_deltas
43
+ @timestamps.each_cons(2).map { |a,b| b - a }
44
+ end
45
+
46
+ # Convert the raw tick intervals to beats-per-minute (BPM)
47
+ # @param [Float] ppq24
48
+ # @return [Float]
31
49
  def ppq24_millis_to_bpm(ppq24)
32
- quarter_note = (ppq24 * 24.to_f)
33
- minute = (60 * 1000) # one minute in millis
34
- minute/quarter_note
50
+ quarter_note = ppq24.to_f * 24.to_f
51
+ 60 / quarter_note
35
52
  end
36
53
 
37
54
  end
@@ -0,0 +1,27 @@
1
+ module Topaz
2
+
3
+ # Construct a tempo source object
4
+ module TempoSource
5
+
6
+ extend self
7
+
8
+ # Construct a tempo source
9
+ # @param [Fixnum, UniMIDI::Input] tempo_or_input
10
+ # @param [Hash] options
11
+ # @option options [Clock::Event] :event
12
+ # @return [MIDIClockInput, Timer]
13
+ def new(tempo_or_input, options = {})
14
+ klass = case tempo_or_input
15
+ when Numeric then Timer
16
+ when UniMIDI::Input then MIDIClockInput
17
+ else
18
+ raise "Not a valid tempo source"
19
+ end
20
+ source = klass.new(tempo_or_input, :event => options[:event])
21
+ source.interval = options[:interval] unless options[:interval].nil?
22
+ source
23
+ end
24
+
25
+ end
26
+
27
+ end