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 +4 -4
- data/README.md +5 -5
- data/lib/topaz/api.rb +19 -0
- data/lib/topaz/clock.rb +187 -0
- data/lib/topaz/midi_clock_input.rb +186 -0
- data/lib/topaz/midi_clock_output.rb +49 -0
- data/lib/topaz/pausable.rb +32 -0
- data/lib/topaz/tempo_calculator.rb +32 -15
- data/lib/topaz/tempo_source.rb +27 -0
- data/lib/topaz/timer.rb +132 -0
- data/lib/topaz.rb +10 -6
- data/test/clock_test.rb +63 -0
- data/test/helper.rb +12 -2
- data/test/midi_clock_input_test.rb +51 -0
- data/test/midi_clock_output_test.rb +50 -0
- data/test/tempo_calculator_test.rb +39 -0
- data/test/tempo_source_test.rb +33 -0
- data/test/timer_test.rb +76 -0
- metadata +15 -9
- data/lib/topaz/events.rb +0 -44
- data/lib/topaz/external_midi_tempo.rb +0 -81
- data/lib/topaz/internal_tempo.rb +0 -88
- data/lib/topaz/midi_sync_output.rb +0 -33
- data/lib/topaz/tempo.rb +0 -164
- data/test/config.rb +0 -11
- data/test/internal_tempo_test.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ce370c533a7d6bf26395da93632c35985e939c3
|
4
|
+
data.tar.gz: 2d225a7fd71f4fe2390278fd63c1c50e4ac3e3b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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::
|
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::
|
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::
|
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::
|
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::
|
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
|
data/lib/topaz/clock.rb
ADDED
@@ -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 #
|
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
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
#
|
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 =
|
33
|
-
|
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
|