midi-topaz 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2011 Ari Russo
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.rdoc ADDED
@@ -0,0 +1,96 @@
1
+ = topaz
2
+
3
+ {pic}[http://images.treetrouble.net/images/topaz.jpg]
4
+
5
+ MIDI syncable tempo in Ruby
6
+
7
+ == Installation
8
+
9
+ gem install midi-topaz
10
+
11
+ == Requirements
12
+
13
+ * {gamelan}[http://github.com/jvoorhis/gamelan]
14
+ * {midi-eye}[http://github.com/arirusso/midi-eye]
15
+ * {midi-message}[http://github.com/arirusso/midi-message]
16
+ * {unimidi}[http://github.com/arirusso/unimidi]
17
+
18
+ these will install automatically with the gem
19
+
20
+ == Usage
21
+
22
+ require "topaz"
23
+
24
+ For demonstration purposes, here's a mock sequencer class and object
25
+
26
+ class Sequencer
27
+
28
+ def step
29
+ @i ||= 0
30
+ $stdout.puts "step #{@i+=1}"
31
+ end
32
+
33
+ end
34
+
35
+ seq = Sequencer.new
36
+
37
+ 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
38
+
39
+ @tempo = Topaz::Tempo.new(130) { seq.step }
40
+
41
+ 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
42
+
43
+ @input = UniMIDI::Input.first.open # an midi input
44
+
45
+ @tempo = Topaz::Tempo.new(:midi => @input) { seq.step }
46
+
47
+ 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
48
+
49
+ @output = UniMIDI::Output.first.open # a midi output
50
+
51
+ @tempo = Topaz::Tempo.new(120, :midi => @output) { seq.step }
52
+
53
+ Input and multiple outputs can be used simultaneously
54
+
55
+ @tempo = Topaz::Tempo.new(:midi => [@input, @output1, @output2]) { seq.step }
56
+
57
+ Once the Tempo object is initialized, start the clock
58
+
59
+ @tempo.start
60
+
61
+ If you are syncing to external clock, nothing will happen until a "start" or "clock" message is received
62
+
63
+ ==== Other things to note
64
+
65
+ 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, 16th notes will occur at 138 BPM
66
+
67
+ @tempo = Topaz::Tempo.new(138, :interval => 16) { seq.step }
68
+
69
+ View the current tempo, which is calculated by Topaz if you're using an external MIDI source.
70
+ (this feature is currently in-progress and may return some low values - 5/26/2011)
71
+
72
+ @tempo.tempo
73
+ => 132.422000
74
+
75
+ Run the generator in a background thread by passing :background => true to Tempo#start
76
+
77
+ @tempo.start(:background => true)
78
+
79
+ Pass in a block that will stop the clock when it evaluates to true
80
+
81
+ @tempo.stop_when { @i.eql?(20) }
82
+
83
+ == Documentation
84
+
85
+ * {examples}[http://github.com/arirusso/topaz/tree/master/examples]
86
+ * {rdoc}[http://rdoc.info/gems/midi-topaz]
87
+
88
+ == Author
89
+
90
+ * {Ari Russo}[http://github.com/arirusso] <ari.russo at gmail.com>
91
+
92
+ == License
93
+
94
+ Apache 2.0, See the file LICENSE
95
+
96
+ Copyright (c) 2011 Ari Russo
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ module Topaz
3
+
4
+ # trigger an event based on received midi clock messages
5
+ class ExternalMIDITempo
6
+
7
+ attr_accessor :action
8
+ attr_reader :clock
9
+
10
+ def initialize(input, options = {})
11
+ @action = options[:action]
12
+ self.interval = options[:interval] || 4
13
+ @tempo_calculator = TempoCalculator.new
14
+ @clock = MIDIEye::Listener.new(input)
15
+
16
+ initialize_clock
17
+ end
18
+
19
+ # this will return a calculated tempo
20
+ def tempo
21
+ @tempo_calculator.find_tempo
22
+ end
23
+
24
+ def start(*a)
25
+ @action[:on_start].call unless @action[:on_start].nil?
26
+ @clock.start(*a)
27
+ end
28
+
29
+ def stop(*a)
30
+ @action[:on_stop].call unless @action[:on_stop].nil?
31
+ @clock.start(*a)
32
+ end
33
+
34
+ def join
35
+ @clock.join
36
+ end
37
+
38
+ #
39
+ # change the clock interval
40
+ # defaults to click once every 24 ticks or one quarter note which is the MIDI standard.
41
+ # however, if you wish to fire the on_tick event twice as often
42
+ # (or once per 12 clicks), pass 8
43
+ #
44
+ # 1 = whole note
45
+ # 2 = half note
46
+ # 4 = quarter note
47
+ # 6 = dotted quarter
48
+ # 8 = eighth note
49
+ # 16 = sixteenth note
50
+ # etc
51
+ #
52
+ def interval=(val)
53
+ per_qn = val / 4
54
+ @per_tick = 24 / per_qn
55
+ end
56
+
57
+ private
58
+
59
+ def initialize_clock
60
+ @counter = 0
61
+ @clock.listen_for(:name => "Clock") do |msg|
62
+ if !@action[:stop_when].nil? && @action[:stop_when].call
63
+ stop
64
+ return
65
+ end
66
+ @action[:destinations].each do |output|
67
+ output.on_tick
68
+ end
69
+ @tempo_calculator.timestamps << msg[:timestamp]
70
+ if @counter.eql?(@per_tick)
71
+ @action[:on_tick].call
72
+ @counter = 0
73
+ else
74
+ @counter += 1
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ end
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ module Topaz
3
+
4
+ class InternalTempo < Gamelan::Timer
5
+
6
+ attr_accessor :action
7
+
8
+ def initialize(tempo, options = {})
9
+ @action = options[:action]
10
+ self.interval = options[:interval] || 4
11
+ @destinations = options[:destinations]
12
+ @last = 0
13
+ @last_sync = 0
14
+ super({:tempo => tempo})
15
+ end
16
+
17
+ # start the internal timer
18
+ # pass :background => true to keep the timer in a background thread
19
+ def start(options = {})
20
+ @action[:on_start].call unless @action[:on_start].nil?
21
+ run
22
+ join unless options[:background]
23
+ end
24
+
25
+ # change the timer's click interval
26
+ def interval=(val)
27
+ @interval = val / 4
28
+ end
29
+
30
+ # stop the timer
31
+ def stop(*a)
32
+ @action[:on_stop].call unless @action[:on_stop].nil?
33
+ super
34
+ end
35
+
36
+ protected
37
+
38
+ # Run all ready tasks.
39
+ def dispatch
40
+ # stuff to do on every tick
41
+ unless @last_sync.eql?((@phase * 24).to_i)
42
+ # look for stop
43
+ if !@action[:stop_when].nil? && @action[:stop_when].call
44
+ stop
45
+ return
46
+ end
47
+ @action[:destinations].each { |dest| dest.on_tick }
48
+ @last_sync = (@phase * 24).to_i
49
+ end
50
+ # stuff to do on @interval
51
+ unless @last.eql?((@phase * @interval).to_i)
52
+ @action[:on_tick].call
53
+ @last = (@phase * @interval).to_i
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ module Topaz
3
+
4
+ # send sync messages via MIDI
5
+ class MIDISyncOutput
6
+
7
+ def initialize(output, options = {})
8
+ @output = output
9
+ end
10
+
11
+ # send a start message
12
+ def on_start
13
+ @output.puts(MIDIMessage::SystemRealtime["Start"].new.to_a)
14
+ end
15
+
16
+ # send a stop message
17
+ def on_stop
18
+ @output.puts(MIDIMessage::SystemRealtime["Stop"].new.to_a)
19
+ end
20
+
21
+ # send a clock message
22
+ def on_tick
23
+ @output.puts(MIDIMessage::SystemRealtime["Clock"].new.to_a)
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby
2
+ module Topaz
3
+
4
+ # main tempo class
5
+ class Tempo
6
+
7
+ extend Forwardable
8
+
9
+ attr_reader :source
10
+
11
+ def_delegators :source, :tempo, :interval, :interval=, :join
12
+
13
+ def initialize(*args, &event)
14
+ @destinations = []
15
+
16
+ if args.first.kind_of?(Numeric)
17
+ @source = InternalTempo.new(args.shift)
18
+ end
19
+ options = args.first
20
+
21
+ initialize_midi_io(options)
22
+ raise "You must specify an internal tempo rate or an external tempo source" if @source.nil?
23
+ @source.action = {
24
+ :on_start => nil,
25
+ :on_stop => nil,
26
+ :on_tick => event,
27
+ :destinations => @destinations,
28
+ :stop_when => nil
29
+ }
30
+ @source.interval = options[:interval] unless options.nil? || options[:interval].nil?
31
+ end
32
+
33
+ # this will change the tempo
34
+ #
35
+ # be warned, in the case that external midi tempo is being used, this will switch to internal
36
+ # tempo at the desired rate
37
+ #
38
+ def tempo=(val)
39
+ if @source.respond_to?(:tempo=)
40
+ @source.tempo = val
41
+ else
42
+ @source = InternalTempo.new(tempo, @action)
43
+ end
44
+ end
45
+
46
+ # pass in a callback that is called when start is called
47
+ def on_start(&block)
48
+ @source.action[:on_start] = block
49
+ end
50
+
51
+ # pass in a callback that is called when stop is called
52
+ def on_stop(&block)
53
+ @source.action[:on_stop] = block
54
+ end
55
+
56
+ # pass in a callback which will
57
+ def stop_when(&block)
58
+ @source.action[:stop_when] = block
59
+ end
60
+
61
+ # this will start the generator
62
+ #
63
+ # in the case that external midi tempo is being used, this will wait for a start
64
+ # or clock message
65
+ #
66
+ def start(options = {})
67
+ @start_time = Time.now
68
+ @destinations.each { |dest| dest.on_start }
69
+ @source.start(options)
70
+ end
71
+
72
+ # this will stop tempo
73
+ def stop(options = {})
74
+ @destinations.each { |dest| dest.on_stop }
75
+ @source.stop(options)
76
+ end
77
+
78
+ # seconds since start was called
79
+ def start_time
80
+ @start_time.nil? ? nil : (Time.now - @start_time).to_f
81
+ end
82
+ alias_method :time_since_start, :start_time
83
+
84
+ private
85
+
86
+ def initialize_midi_io(args)
87
+ ports = args.kind_of?(Hash) ? args[:midi] : args
88
+ unless ports.nil?
89
+ if ports.kind_of?(Array)
90
+ ports.each { |port| initialize_midi_io(port) }
91
+ elsif ports.type.eql?(:input) && @source.nil?
92
+ @source = ExternalMIDITempo.new(ports)
93
+ elsif ports.type.eql?(:output)
94
+ @destinations << MIDISyncOutput.new(ports)
95
+ end
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ module Topaz
3
+
4
+ class TempoCalculator
5
+
6
+ attr_reader :tempo, :timestamps
7
+
8
+ def initialize(tempo = nil)
9
+ @tempo = tempo
10
+ @timestamps = []
11
+ @counter = 0
12
+ end
13
+
14
+ # analyse the tempo based on the last 6 ticks
15
+ def find_tempo
16
+ tempo = nil
17
+ diffs = []
18
+ @timestamps.shift while @timestamps.length > 6
19
+ @timestamps.each_with_index { |n, i| (diffs << (@timestamps[i+1] - n)) unless @timestamps[i+1].nil? }
20
+ unless diffs.empty?
21
+ avg = (diffs.inject { |a, b| a + b }.to_f / diffs.length.to_f)
22
+ tempo = ppq24_millis_to_bpm(avg)
23
+ end
24
+ @tempo = tempo
25
+ end
26
+
27
+ private
28
+
29
+ # convert the raw tick intervals to bpm
30
+ def ppq24_millis_to_bpm(ppq24)
31
+ quarter_note = (ppq24 * 24.to_f)
32
+ minute = (60 * 1000) # one minute in millis
33
+ minute/quarter_note
34
+ end
35
+
36
+ end
37
+
38
+ end
data/lib/topaz.rb ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # MIDI syncable tempo module in Ruby
4
+ # (c)2011 Ari Russo and licensed under the Apache 2.0 License
5
+ #
6
+
7
+ require 'forwardable'
8
+ require 'gamelan'
9
+ require 'midi-eye'
10
+ require 'midi-message'
11
+
12
+ require 'topaz/external_midi_tempo'
13
+ require 'topaz/internal_tempo'
14
+ require 'topaz/midi_sync_output'
15
+ require 'topaz/tempo_calculator'
16
+ require 'topaz/tempo'
17
+
18
+ module Topaz
19
+
20
+ VERSION = "0.0.1"
21
+
22
+ end
data/test/config.rb ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module TestHelper::Config
4
+
5
+ include UniMIDI
6
+
7
+ # adjust these constants to suit your hardware configuration
8
+ # before running tests
9
+
10
+ TestInput = Input.first # this is the device you wish to use to test input
11
+ TestOutput = Output.first # likewise for output
12
+
13
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ dir = File.dirname(File.expand_path(__FILE__))
4
+ $LOAD_PATH.unshift dir + '/../lib'
5
+
6
+ require 'test/unit'
7
+ require 'topaz'
8
+
9
+ module TestHelper
10
+
11
+
12
+ end
13
+
14
+ require File.dirname(__FILE__) + '/config'
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'helper'
4
+
5
+ class InternalTempoTest < Test::Unit::TestCase
6
+
7
+ include Topaz
8
+ include TestHelper
9
+
10
+ def test_stop_when
11
+ i = 0
12
+ count_to = 5
13
+
14
+ tempo = Tempo.new(120) { i += 1 }
15
+ tempo.stop_when { i.eql?(count_to) }
16
+ tempo.start
17
+
18
+ assert_equal(count_to, i)
19
+ end
20
+
21
+ def test_timer
22
+ i = 0
23
+ count_to = 5
24
+
25
+ tempo = Tempo.new(60) { i += 1 }
26
+ end
27
+
28
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: midi-topaz
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Ari Russo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-26 00:00:00 -04:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: gamelan
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: midi-message
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 0.0.4
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: midi-eye
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.0.7
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: unimidi
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 0.1.10
58
+ type: :runtime
59
+ version_requirements: *id004
60
+ description: MIDI syncable tempo in Ruby
61
+ email:
62
+ - ari.russo@gmail.com
63
+ executables: []
64
+
65
+ extensions: []
66
+
67
+ extra_rdoc_files: []
68
+
69
+ files:
70
+ - lib/topaz.rb
71
+ - lib/topaz/midi_sync_output.rb
72
+ - lib/topaz/internal_tempo.rb
73
+ - lib/topaz/tempo_calculator.rb
74
+ - lib/topaz/external_midi_tempo.rb
75
+ - lib/topaz/tempo.rb
76
+ - test/helper.rb
77
+ - test/config.rb
78
+ - test/test_internal_tempo.rb
79
+ - LICENSE
80
+ - README.rdoc
81
+ has_rdoc: true
82
+ homepage: http://github.com/arirusso/topaz
83
+ licenses: []
84
+
85
+ post_install_message:
86
+ rdoc_options: []
87
+
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 1.3.6
102
+ requirements: []
103
+
104
+ rubyforge_project: midi-topaz
105
+ rubygems_version: 1.6.2
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: MIDI syncable tempo in Ruby
109
+ test_files: []
110
+