jsound 0.1.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/.yardopts +3 -0
  2. data/LICENSE.txt +27 -0
  3. data/README.md +96 -0
  4. data/Rakefile +19 -0
  5. data/examples/harmonizer.rb +25 -0
  6. data/examples/launchpad/launchpad.rb +65 -0
  7. data/examples/launchpad/launchpad_generator.rb +111 -0
  8. data/examples/list_devices.rb +6 -0
  9. data/examples/monitor.rb +15 -0
  10. data/examples/notes.rb +90 -0
  11. data/examples/transposer.rb +20 -0
  12. data/lib/jsound.rb +4 -0
  13. data/lib/jsound/convert.rb +30 -0
  14. data/lib/jsound/midi.rb +77 -0
  15. data/lib/jsound/midi/device.rb +72 -0
  16. data/lib/jsound/midi/device_list.rb +157 -0
  17. data/lib/jsound/midi/devices/generator.rb +22 -0
  18. data/lib/jsound/midi/devices/input_device.rb +51 -0
  19. data/lib/jsound/midi/devices/jdevice.rb +100 -0
  20. data/lib/jsound/midi/devices/monitor.rb +18 -0
  21. data/lib/jsound/midi/devices/output_device.rb +36 -0
  22. data/lib/jsound/midi/devices/recorder.rb +56 -0
  23. data/lib/jsound/midi/devices/repeater.rb +28 -0
  24. data/lib/jsound/midi/devices/transformer.rb +30 -0
  25. data/lib/jsound/midi/message.rb +165 -0
  26. data/lib/jsound/midi/message_builder.rb +52 -0
  27. data/lib/jsound/midi/messages/channel_pressure.rb +26 -0
  28. data/lib/jsound/midi/messages/control_change.rb +29 -0
  29. data/lib/jsound/midi/messages/note_off.rb +10 -0
  30. data/lib/jsound/midi/messages/note_on.rb +29 -0
  31. data/lib/jsound/midi/messages/pitch_bend.rb +43 -0
  32. data/lib/jsound/midi/messages/poly_pressure.rb +29 -0
  33. data/lib/jsound/midi/messages/program_change.rb +27 -0
  34. data/lib/jsound/type_from_class_name.rb +23 -0
  35. data/spec/jsound/convert_spec.rb +68 -0
  36. data/spec/jsound/midi/device_spec.rb +75 -0
  37. data/spec/jsound/midi/devices/generator_spec.rb +21 -0
  38. data/spec/jsound/midi/devices/output_device_spec.rb +22 -0
  39. data/spec/jsound/midi/devices/recorder_spec.rb +88 -0
  40. data/spec/jsound/midi/devices/repeater_device_spec.rb +19 -0
  41. data/spec/jsound/midi/devices/transformer_spec.rb +20 -0
  42. data/spec/jsound/midi/devlice_list_spec.rb +60 -0
  43. data/spec/jsound/midi/message_builder_spec.rb +22 -0
  44. data/spec/jsound/midi/message_spec.rb +30 -0
  45. data/spec/jsound/midi/messages/note_off_spec.rb +62 -0
  46. data/spec/jsound/midi/messages/note_on_spec.rb +109 -0
  47. data/spec/jsound/midi/messages/pitch_bend_spec.rb +88 -0
  48. data/spec/jsound/midi_spec.rb +33 -0
  49. data/spec/jsound/type_from_class_name_spec.rb +26 -0
  50. data/spec/spec_helper.rb +23 -0
  51. metadata +103 -0
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env jruby
2
+ require 'rubygems'
3
+ require 'jsound'
4
+ include JSound::Midi
5
+ include Devices
6
+
7
+ transposer = Transformer.new do |message|
8
+ message.pitch += 24 if message.respond_to? :pitch # transpose up two octaves
9
+ message
10
+ end
11
+
12
+ # Adjust the INPUTS and OUTPUTS as needed to use the devices you want:
13
+ INPUTS.open_first >> transposer >> OUTPUTS.open_first
14
+ # For example, to send my Akai keyboard through the harmonizer to the OS X IAC bus, I can do:
15
+ # INPUTS.Akai >> transposer >> OUTPUTS.IAC
16
+
17
+ # force the script to keep running (MIDI devices run in a background thread)
18
+ while(true)
19
+ sleep 5
20
+ end
@@ -0,0 +1,4 @@
1
+ require 'java'
2
+ require 'jsound/convert'
3
+ require 'jsound/type_from_class_name'
4
+ require 'jsound/midi'
@@ -0,0 +1,30 @@
1
+ module JSound
2
+
3
+ # Helper methods for converting MIDI data values
4
+ module Convert
5
+
6
+ # The maximum value for unsigned 14-bit integer
7
+ MAX_14BIT_VALUE = 16383 # == 127 + (127 << 7)
8
+
9
+ # Convert a single integer to a [least_significant, most_significant] pair of 7-bit ints
10
+ def to_7bit(value)
11
+ [value & 127, (value >> 7) & 127]
12
+ end
13
+
14
+ # Convert a [least_significant, most_significant] pair of 7-bit ints to a single integer
15
+ def from_7bit(lsb, msb)
16
+ lsb + (msb << 7)
17
+ end
18
+
19
+ # Scales a float from the range [-1.0, 1.0] to the integer range [0, 16383]
20
+ def normalized_float_to_14bit(float)
21
+ (MAX_14BIT_VALUE*(float+1)/2).round
22
+ end
23
+
24
+ # Make all methods be module functions (accessible by sending the method name to module directly)
25
+ instance_methods.each do |method|
26
+ module_function method
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,77 @@
1
+ require 'jsound/midi/message'
2
+ require 'jsound/midi/messages/channel_pressure'
3
+ require 'jsound/midi/messages/control_change'
4
+ require 'jsound/midi/messages/note_on'
5
+ require 'jsound/midi/messages/note_off'
6
+ require 'jsound/midi/messages/pitch_bend'
7
+ require 'jsound/midi/messages/poly_pressure'
8
+ require 'jsound/midi/messages/program_change'
9
+
10
+ require 'jsound/midi/message_builder'
11
+
12
+ require 'jsound/midi/device'
13
+ require 'jsound/midi/devices/generator'
14
+ require 'jsound/midi/devices/jdevice'
15
+ require 'jsound/midi/devices/input_device'
16
+ require 'jsound/midi/devices/output_device'
17
+ require 'jsound/midi/devices/monitor'
18
+ require 'jsound/midi/devices/recorder'
19
+ require 'jsound/midi/devices/repeater'
20
+ require 'jsound/midi/devices/transformer'
21
+
22
+ require 'jsound/midi/device_list'
23
+
24
+
25
+ module JSound
26
+
27
+ # Module containing all MIDI functionality.
28
+ #
29
+ # Also provies the core interface for accessing MIDI devices, see the device list constants defined here.
30
+ #
31
+ module Midi
32
+ include_package 'javax.sound.midi'
33
+
34
+ # All MIDI devices
35
+ # @return [DeviceList]
36
+ DEVICES = DeviceList.new
37
+
38
+ # MIDI input devices
39
+ # @return [DeviceList] a list of {Devices::InputDevice}s
40
+ INPUTS = DeviceList.new
41
+
42
+ # MIDI output devices
43
+ # @return [DeviceList] a list of {Devices::OutputDevice}s
44
+ OUTPUTS = DeviceList.new
45
+
46
+ # MIDI synthesizer devices
47
+ # @return [DeviceList]
48
+ SYNTHESIZERS = SYNTHS = DeviceList.new
49
+
50
+ # MIDI sequencer devices
51
+ # @return [DeviceList]
52
+ SEQUENCERS = DeviceList.new
53
+
54
+ # Refresh the list of connected devices.
55
+ # @note this happens automatically when JSound is required.
56
+ def refresh_devices
57
+ [DEVICES,INPUTS,OUTPUTS,SYNTHESIZERS,SEQUENCERS].each{|collection| collection.clear}
58
+ MidiSystem.getMidiDeviceInfo.each do |device_info|
59
+ java_device = MidiSystem.getMidiDevice(device_info)
60
+ device = Devices::JDevice.from_java(java_device)
61
+ case device.type
62
+ when :sequencer then SEQUENCERS << device
63
+ when :synthesizer then SYNTHESIZERS << device
64
+ when :input then INPUTS << device
65
+ when :output then OUTPUTS << device
66
+ end
67
+ DEVICES << device
68
+ end
69
+ DEVICES
70
+ end
71
+ module_function :refresh_devices
72
+
73
+ # Refresh devices automatically when loaded:
74
+ refresh_devices()
75
+
76
+ end
77
+ end
@@ -0,0 +1,72 @@
1
+ module JSound
2
+ module Midi
3
+
4
+ # A device that can transmit and/or receive messages (typically MIDI messages).
5
+ # This default implementation simply passes through all messages.
6
+ class Device
7
+ include JSound::Mixins::TypeFromClassName
8
+
9
+ # Open the device and allocate the needed resources so that it can send and receive messages
10
+ # @note this operation is typically only relevant for Java-based devices such as {Devices::InputDevice} and {Devices::OutputDevice}
11
+ # @see DeviceList#open
12
+ def open
13
+ end
14
+
15
+ # return true if this device is currently open
16
+ def open?
17
+ # typically, ruby devices are always open, subclasses might not be
18
+ true
19
+ end
20
+
21
+ # Close the device and free up any resources used by this device.
22
+ # @note this operation is typically only relevant for Java-based devices such as {Devices::InputDevice} and {Devices::OutputDevice}
23
+ def close
24
+ end
25
+
26
+ def type
27
+ # The base Device behaves like a 'pass through'
28
+ @type ||= (self.class == Device ? :pass_through : self.class.type)
29
+ end
30
+
31
+ # the device connected to this device's output
32
+ # @return [Device] the connected device, or nil if nothing is connected
33
+ def output
34
+ @output
35
+ end
36
+
37
+ # connect a device as the output for this device
38
+ # @param [Device] the device to connect, or nil to disconnect the currently connected device
39
+ # @see {#>>}
40
+ def output= device
41
+ @output = device
42
+ end
43
+
44
+ # Connect a device as the output for this device. shortcut for {#output=}
45
+ # @param [Device] the device to connect, or nil to disconnect the currently connected device
46
+ # @see {#output=}
47
+ def >> device
48
+ self.output= device
49
+ end
50
+
51
+ # Send a message to this device
52
+ # @param [Message]
53
+ # @see #<=
54
+ def message(message)
55
+ # default behavior is to pass the message to any connected output
56
+ @output.message(message) if @output
57
+ end
58
+
59
+ # send a message to this device. shortcut for {#message}
60
+ # @see #message
61
+ def <=(message)
62
+ message(message)
63
+ end
64
+
65
+ def to_s
66
+ "MIDI #{type} device"
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,157 @@
1
+ module JSound
2
+ module Midi
3
+
4
+ # A collection of MIDI {Device}s that provides various methods to lookup (and optionally auto-open) specific devices
5
+ #
6
+ # @see Midi the Midi module for constants to access your system devices
7
+ #
8
+ class DeviceList
9
+
10
+ # The devices within this collection.
11
+ attr_reader :devices
12
+
13
+ def initialize(list=[])
14
+ @devices = list
15
+ end
16
+
17
+ # Find the first {Device} matching the criteria
18
+ #
19
+ # @param criteria matches description against for a Regexp argument, matches the device field against the value for a Hash argument, otherwise checks for an equal description
20
+ # @return [Device] the first matching {Device} or nil if nothing matched
21
+ # @note I/O devices need to be opened before they can be used (see {Device#open})
22
+ # @see #find_all
23
+ # @see #open
24
+ # @example {OUTPUTS}.find "IAC Driver Bus 1" #=> find the first output with the exact description "IAC Driver Bus 1"
25
+ # @example {OUTPUTS}.find /IAC/ #=> find the first output with a description containing "IAC"
26
+ # @example {OUTPUTS}.find :vendor => "Apple Inc." #=> find the first output with the exact vendor field "Apple Inc."
27
+ # @example {OUTPUTS}.find :vendor => /Apple / #=> find the first output with a vendor field containing "Apple"
28
+ #
29
+ def find(criteria)
30
+ search :find, criteria
31
+ end
32
+
33
+ # Find all {Device}s matching the criteria
34
+ #
35
+ # @param criteria matches description against for a Regexp argument, matches the device field against the value for a Hash argument, otherwise checks for an equal description
36
+ # @return [Array] an Array of all matching {Device}s or [] if nothing matched
37
+ # @note I/O devices need to be opened before they can be used (see {Device#open})
38
+ # @see #find for examples
39
+ # @see #open
40
+ #
41
+ def find_all(criteria)
42
+ search :find_all, criteria
43
+ end
44
+
45
+ # Acts like Array#[] for Numeric and Range arguments. Otherwise acts like {#find_all}.
46
+ #
47
+ # @see #find_all
48
+ #
49
+ def [](criteria)
50
+ if criteria.kind_of? Fixnum or criteria.kind_of? Range
51
+ @devices[criteria]
52
+ else
53
+ find_all(criteria)
54
+ end
55
+ end
56
+
57
+ # Find the first device matching the argument and open it.
58
+ #
59
+ # @param device matches description against for a Regexp argument, matches the device field against the value for a Hash argument, otherwise checks for an equal description
60
+ # @return [Device] the device
61
+ # @raise an error if no device can be found
62
+ # @see #find
63
+ # @see #find_all
64
+ #
65
+ def open(device)
66
+ device = find device unless device.is_a? Device
67
+ if device
68
+ device.open
69
+ else
70
+ raise "Device note found: #{device}"
71
+ end
72
+ device
73
+ end
74
+
75
+ # Find and open the first device with a description matching the argument.
76
+ #
77
+ # @return [Device] the device
78
+ # @raise an error if no device can be found
79
+ # @see #open
80
+ #
81
+ def /(regexp)
82
+ regexp = Regexp.new(regexp.to_s, Regexp::IGNORECASE) if not regexp.kind_of? Regexp
83
+ open regexp
84
+ end
85
+
86
+ # open the first device in this list
87
+ #
88
+ # @return [Device] the device
89
+ # @raise an error if no device can be found
90
+ # @see #open
91
+ #
92
+ def open_first
93
+ open @devices.first
94
+ end
95
+
96
+ # open and return the last device in this list
97
+ #
98
+ # @return [Device] the device
99
+ # @raise an error if no device can be found
100
+ # @see #open
101
+ #
102
+ def open_last
103
+ open @devices.first
104
+ end
105
+
106
+ def to_s
107
+ @devices.join("\n")
108
+ end
109
+
110
+ ########################
111
+ private
112
+
113
+ def search(iterator, criteria)
114
+ field, target_value = field_and_target_value_for(criteria)
115
+ matcher = matcher_for(target_value)
116
+ @devices.send(iterator) do |device|
117
+ device.send(field).send(matcher, target_value)
118
+ end
119
+ end
120
+
121
+ def field_and_target_value_for(criteria)
122
+ if criteria.is_a? Hash
123
+ first_key = criteria.keys.first
124
+ return first_key, criteria[first_key]
125
+ else
126
+ return :description, criteria
127
+ end
128
+ end
129
+
130
+ def matcher_for(target_value)
131
+ case target_value
132
+ when Regexp then '=~'
133
+ else '=='
134
+ end
135
+ end
136
+
137
+ # Pass method calls through to the underlying Array.
138
+ # If Array doesn't understand the method, call {#open} with a case-insensitive regexp match
139
+ # against description, treating underscore as a wildcard.
140
+ #
141
+ # @example # {INPUTS}.Akai => open the first "Akai" input device connected to this computer
142
+ # @example # {INPUTS}.Akai_2 => open an input device matching /Akai.*2/i
143
+ #
144
+ def method_missing(sym, *args, &block)
145
+ if @devices.respond_to? sym
146
+ @devices.send(sym, *args, &block)
147
+ elsif args.length == 0 and block.nil?
148
+ self.open /#{sym.to_s.sub '_','.*'}/i
149
+ else
150
+ super
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,22 @@
1
+ module JSound
2
+ module Midi
3
+ module Devices
4
+
5
+ # A device which provides methods for generating MIDI command messages.
6
+ # See JSound::Midi::Messages::Builder for the available methods.
7
+ class Generator < Device
8
+
9
+ # For all the methods defined in the MessageBuilder module (note_on, pitch_bend, control_change, etc),
10
+ # define a corresponding device method that constructs the message and sends the connected device
11
+ MessageBuilder.private_instance_methods.each do |method|
12
+ define_method(method) do |*args|
13
+ message = MessageBuilder.send(method, *args)
14
+ self.message message
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ module JSound
2
+ module Midi
3
+ module Devices
4
+
5
+ # A device that receives messages from a system MIDI input port,
6
+ # and passes the messages to any connected device.
7
+ #
8
+ # Available inputs are contained in the {INPUTS} list in the {Midi} module.
9
+ #
10
+ class InputDevice < JDevice
11
+
12
+ # Wrap a javax.sound.midi.MidiDevice receiver to provide MIDI output.
13
+ #
14
+ # @note Typically you won't instantiate these directly. Instead, find an input via the {INPUTS} list in the {Midi} module.
15
+ #
16
+ def initialize(java_device)
17
+ super(java_device, :input)
18
+ @bridge = Bridge.new(self)
19
+ java_device.transmitter.receiver = @bridge
20
+ end
21
+
22
+ def output=(device)
23
+ super
24
+ @bridge.output= device
25
+ end
26
+
27
+ # A subcomponent of {InputDevice} that implements javax.sound.midi.Receiver
28
+ # by translating incoming Java MIDI Messages to Ruby Messages.
29
+ class Bridge < Device
30
+ include javax.sound.midi.Receiver
31
+
32
+ def initialize(source_device)
33
+ @source_device = source_device
34
+ end
35
+
36
+ # Receives incoming Java MIDI Messages, converts them to Ruby Messages,
37
+ # and sends them to any connected devices.
38
+ def send(java_message, timestamp=-1)
39
+ self.message Message.from_java(java_message, :source => @source_device)
40
+ rescue
41
+ STDERR.puts $! if $DEBUG # otherwise this can get swallowed
42
+ raise
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,100 @@
1
+ module JSound
2
+ module Midi
3
+ module Devices
4
+
5
+ # A Java-provided MIDI device (wraps javax.sound.midi.MidiDevice objects)
6
+ class JDevice < Device
7
+
8
+ # the javax.sound.midi.MidiDevice.Info object for this java device
9
+ attr_reader :info
10
+
11
+ # the description of this device
12
+ # @return [String]
13
+ attr_reader :description
14
+
15
+ attr_reader :type
16
+
17
+ # All open JDevices
18
+ # @note All open devices will be automatically closed at_exit
19
+ def self.open_devices
20
+ @@open_devices ||= []
21
+ end
22
+ at_exit do
23
+ # Close all open devices so we don't hang the program at shutdown time
24
+ for device in JDevice.open_devices
25
+ device.close
26
+ end
27
+ end
28
+
29
+ def initialize(java_device, type)
30
+ @java_device = java_device
31
+ @info = @java_device.deviceInfo
32
+ @description = @info.description
33
+ @type = type
34
+ end
35
+
36
+ def self.from_java(java_device)
37
+ case java_device
38
+ when javax.sound.midi.Sequencer then type = :sequencer
39
+ when javax.sound.midi.Synthesizer then type = :synthesizer
40
+ else
41
+ # This assumes a single device cannot be both an input and an output:
42
+ if java_device.maxTransmitters != 0
43
+ return InputDevice.new(java_device)
44
+ elsif java_device.maxReceivers != 0
45
+ return OutputDevice.new(java_device)
46
+ else
47
+ type = :unknown
48
+ end
49
+ end
50
+ new java_device, type
51
+ end
52
+
53
+ # @see Device#open
54
+ # @note All open devices will be automatically closed at_exit
55
+ def open
56
+ unless @java_device.open?
57
+ puts "Opening #{to_s}"
58
+ @java_device.open
59
+ self.class.open_devices << self
60
+ end
61
+ end
62
+
63
+ # @see Device#close
64
+ def close
65
+ if @java_device.open?
66
+ puts "Closing #{to_s}"
67
+ @java_device.close
68
+ self.class.open_devices.delete(self)
69
+ end
70
+ end
71
+
72
+ def method_missing(sym, *args, &block)
73
+ if @java_device.respond_to? sym
74
+ @java_device.send(sym, *args, &block)
75
+ else
76
+ @info.send(sym, *args, &block)
77
+ end
78
+ end
79
+
80
+ def respond_to?(sym)
81
+ super or @java_device.respond_to? sym or info.respond_to? sym
82
+ end
83
+
84
+ def [](field)
85
+ send field
86
+ end
87
+
88
+ def to_s
89
+ "#{super}: #{info.description}"
90
+ end
91
+
92
+ def inspect
93
+ to_s
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+ end
100
+ end