jsound 0.1.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +3 -0
- data/LICENSE.txt +27 -0
- data/README.md +96 -0
- data/Rakefile +19 -0
- data/examples/harmonizer.rb +25 -0
- data/examples/launchpad/launchpad.rb +65 -0
- data/examples/launchpad/launchpad_generator.rb +111 -0
- data/examples/list_devices.rb +6 -0
- data/examples/monitor.rb +15 -0
- data/examples/notes.rb +90 -0
- data/examples/transposer.rb +20 -0
- data/lib/jsound.rb +4 -0
- data/lib/jsound/convert.rb +30 -0
- data/lib/jsound/midi.rb +77 -0
- data/lib/jsound/midi/device.rb +72 -0
- data/lib/jsound/midi/device_list.rb +157 -0
- data/lib/jsound/midi/devices/generator.rb +22 -0
- data/lib/jsound/midi/devices/input_device.rb +51 -0
- data/lib/jsound/midi/devices/jdevice.rb +100 -0
- data/lib/jsound/midi/devices/monitor.rb +18 -0
- data/lib/jsound/midi/devices/output_device.rb +36 -0
- data/lib/jsound/midi/devices/recorder.rb +56 -0
- data/lib/jsound/midi/devices/repeater.rb +28 -0
- data/lib/jsound/midi/devices/transformer.rb +30 -0
- data/lib/jsound/midi/message.rb +165 -0
- data/lib/jsound/midi/message_builder.rb +52 -0
- data/lib/jsound/midi/messages/channel_pressure.rb +26 -0
- data/lib/jsound/midi/messages/control_change.rb +29 -0
- data/lib/jsound/midi/messages/note_off.rb +10 -0
- data/lib/jsound/midi/messages/note_on.rb +29 -0
- data/lib/jsound/midi/messages/pitch_bend.rb +43 -0
- data/lib/jsound/midi/messages/poly_pressure.rb +29 -0
- data/lib/jsound/midi/messages/program_change.rb +27 -0
- data/lib/jsound/type_from_class_name.rb +23 -0
- data/spec/jsound/convert_spec.rb +68 -0
- data/spec/jsound/midi/device_spec.rb +75 -0
- data/spec/jsound/midi/devices/generator_spec.rb +21 -0
- data/spec/jsound/midi/devices/output_device_spec.rb +22 -0
- data/spec/jsound/midi/devices/recorder_spec.rb +88 -0
- data/spec/jsound/midi/devices/repeater_device_spec.rb +19 -0
- data/spec/jsound/midi/devices/transformer_spec.rb +20 -0
- data/spec/jsound/midi/devlice_list_spec.rb +60 -0
- data/spec/jsound/midi/message_builder_spec.rb +22 -0
- data/spec/jsound/midi/message_spec.rb +30 -0
- data/spec/jsound/midi/messages/note_off_spec.rb +62 -0
- data/spec/jsound/midi/messages/note_on_spec.rb +109 -0
- data/spec/jsound/midi/messages/pitch_bend_spec.rb +88 -0
- data/spec/jsound/midi_spec.rb +33 -0
- data/spec/jsound/type_from_class_name_spec.rb +26 -0
- data/spec/spec_helper.rb +23 -0
- metadata +103 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
module Devices
|
4
|
+
|
5
|
+
# A device that prints out all incoming MIDI message.
|
6
|
+
class Monitor < Device
|
7
|
+
def message(message)
|
8
|
+
source = message.source
|
9
|
+
if source and source.respond_to? :description
|
10
|
+
source = "#{source.description} => "
|
11
|
+
end
|
12
|
+
puts "#{source}#{message}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
module Devices
|
4
|
+
|
5
|
+
# A device that sends all it's received messages to a system MIDI output port.
|
6
|
+
#
|
7
|
+
# Available outputs are contained in the {OUTPUTS} list in the {Midi} module.
|
8
|
+
#
|
9
|
+
class OutputDevice < JDevice
|
10
|
+
|
11
|
+
# Wrap a javax.sound.midi.MidiDevice transmitter to provide MIDI output.
|
12
|
+
#
|
13
|
+
# @note Typically you won't instantiate these directly. Instead, find an output via the {OUTPUTS} list in the {Midi} module.
|
14
|
+
#
|
15
|
+
def initialize(java_device)
|
16
|
+
super(java_device, :output)
|
17
|
+
end
|
18
|
+
|
19
|
+
def output= device
|
20
|
+
raise "#{self.class} cannot be assigned an output"
|
21
|
+
end
|
22
|
+
|
23
|
+
def message(message)
|
24
|
+
# unwrap the ruby message wrapper, if needed:
|
25
|
+
message = message.to_java if message.respond_to? :to_java
|
26
|
+
|
27
|
+
# Use java_send to call Receiver.send() since it conflicts with Ruby's built-in send method
|
28
|
+
# -1 means no timestamp, so we're not supporting timestamps
|
29
|
+
@java_device.receiver.java_send(:send, [javax.sound.midi.MidiMessage, Java::long], message, -1)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
module Devices
|
4
|
+
|
5
|
+
# A Device that records incoming messages, and the timestamp at which they were received.
|
6
|
+
class Recorder < Device
|
7
|
+
|
8
|
+
# The recorded [message,timestamp] pairs
|
9
|
+
attr_reader :messages_with_timestamps
|
10
|
+
|
11
|
+
# The recorded messages without timestamps
|
12
|
+
def messages
|
13
|
+
@messages_with_timestamps.map{|m,t| m }
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(autostart=true)
|
17
|
+
clear
|
18
|
+
if autostart
|
19
|
+
start
|
20
|
+
else
|
21
|
+
stop
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# clear any recorded messages
|
26
|
+
def clear
|
27
|
+
@messages_with_timestamps = []
|
28
|
+
end
|
29
|
+
|
30
|
+
# start recording
|
31
|
+
def open
|
32
|
+
@recording = true
|
33
|
+
end
|
34
|
+
alias start open
|
35
|
+
|
36
|
+
# stop recording
|
37
|
+
def close
|
38
|
+
@recording = false
|
39
|
+
end
|
40
|
+
alias stop close
|
41
|
+
|
42
|
+
# true if this object is currently recording
|
43
|
+
def open?
|
44
|
+
@recording
|
45
|
+
end
|
46
|
+
alias recording? open?
|
47
|
+
|
48
|
+
def message(message)
|
49
|
+
@messages_with_timestamps << [message, Time.now.to_i] if recording?
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
module Devices
|
4
|
+
|
5
|
+
# A device which repeats the input message to multiple outputs.
|
6
|
+
class Repeater < Device
|
7
|
+
|
8
|
+
# connect device(s) as the outputs for this device
|
9
|
+
# @param [Enumberable, Device] the device or devices to connect, or nil to disconnect the currently connected device
|
10
|
+
# @see {#>>}
|
11
|
+
def output= device
|
12
|
+
device = [device] if not device.is_a? Enumerable
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def message(message)
|
17
|
+
if @output
|
18
|
+
for device in @output
|
19
|
+
device.message(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
module Devices
|
4
|
+
|
5
|
+
class Transformer < Device
|
6
|
+
|
7
|
+
# The transformation block: a lambda that takes a message and returns
|
8
|
+
# either a transformed message or an Enumerable list of messages
|
9
|
+
attr_accessor :message_processor
|
10
|
+
|
11
|
+
def initialize(&message_processor)
|
12
|
+
@message_processor = message_processor
|
13
|
+
end
|
14
|
+
|
15
|
+
def message(message)
|
16
|
+
if @output and @message_processor
|
17
|
+
transformed_message = @message_processor.call(message)
|
18
|
+
if transformed_message.is_a? Enumerable
|
19
|
+
transformed_message.each{|m| @output.message(m) }
|
20
|
+
else
|
21
|
+
@output.message(transformed_message) if transformed_message
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
|
4
|
+
# A generic MIDI message.
|
5
|
+
# The various subclasses in this module deal with specific message details.
|
6
|
+
# See http://www.midi.org/techspecs/midimessages.php
|
7
|
+
# for info on how the MIDI spec defines these messages.
|
8
|
+
class Message
|
9
|
+
include JSound::Mixins::TypeFromClassName
|
10
|
+
|
11
|
+
# The MIDI input {Device} which received this message.
|
12
|
+
attr_reader :source
|
13
|
+
|
14
|
+
# The variable data for this message type. Contents depend on the message type.
|
15
|
+
# @example a NoteOn's #data is [pitch,velocity]
|
16
|
+
attr_reader :data
|
17
|
+
|
18
|
+
# The channel number of the message
|
19
|
+
attr_reader :channel
|
20
|
+
|
21
|
+
# The type of message, such as :note_on or :control_change
|
22
|
+
# @return [Symbol]
|
23
|
+
attr_reader :type
|
24
|
+
|
25
|
+
def initialize(data, channel=0, options={})
|
26
|
+
@data = data
|
27
|
+
@channel = channel
|
28
|
+
|
29
|
+
# Generic Message objects specify a type explicitly (see initialize).
|
30
|
+
# Subclasses will typically use the class type (see JSound::Mixins::TypeFromClassName).
|
31
|
+
@type = options[:type] ||= self.class.type
|
32
|
+
|
33
|
+
@java_message = options[:java_message]
|
34
|
+
|
35
|
+
@source = options[:source]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Map java message status values to ruby classes
|
39
|
+
CLASS_FOR_STATUS = {}
|
40
|
+
|
41
|
+
# Map ruby classes to java message status values
|
42
|
+
STATUS_FOR_CLASS = {}
|
43
|
+
|
44
|
+
def self.inherited(child_class)
|
45
|
+
# I'm using the convention that the message class names
|
46
|
+
# correspond to the java ShortMessage constants, like:
|
47
|
+
# NoteOn => ShortMessage::NOTE_ON
|
48
|
+
const_name = child_class.type.to_s.upcase
|
49
|
+
if javax.sound.midi.ShortMessage.const_defined? const_name
|
50
|
+
status = javax.sound.midi.ShortMessage.const_get(const_name)
|
51
|
+
CLASS_FOR_STATUS[status] = child_class
|
52
|
+
STATUS_FOR_CLASS[child_class] = status
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
TYPE_FOR_STATUS = {
|
57
|
+
javax.sound.midi.ShortMessage::ACTIVE_SENSING => :active_sensing,
|
58
|
+
javax.sound.midi.ShortMessage::CONTINUE => :continue,
|
59
|
+
javax.sound.midi.ShortMessage::END_OF_EXCLUSIVE => :end_of_exclusive,
|
60
|
+
javax.sound.midi.ShortMessage::MIDI_TIME_CODE => :multi_time_code,
|
61
|
+
javax.sound.midi.ShortMessage::SONG_POSITION_POINTER => :song_position_pointer,
|
62
|
+
javax.sound.midi.ShortMessage::SONG_SELECT => :song_select,
|
63
|
+
javax.sound.midi.ShortMessage::START => :start,
|
64
|
+
javax.sound.midi.ShortMessage::STOP => :stop,
|
65
|
+
javax.sound.midi.ShortMessage::SYSTEM_RESET => :system_reset,
|
66
|
+
javax.sound.midi.ShortMessage::TIMING_CLOCK => :timing_clock,
|
67
|
+
javax.sound.midi.ShortMessage::TUNE_REQUEST => :tune_request
|
68
|
+
}
|
69
|
+
|
70
|
+
STATUS_FOR_TYPE = TYPE_FOR_STATUS.invert
|
71
|
+
|
72
|
+
# true when the argument has the same {#type}, {#channel}, and {#data}
|
73
|
+
def == other
|
74
|
+
other.respond_to? :type and type == other.type and
|
75
|
+
other.respond_to? :channel and channel == other.channel and
|
76
|
+
other.respond_to? :data and data == other.data
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.from_java(java_message, options={})
|
80
|
+
case java_message
|
81
|
+
when javax.sound.midi.SysexMessage
|
82
|
+
type = :sysex
|
83
|
+
data = java_message.data # this is a byte array in Java, might need conversion?
|
84
|
+
|
85
|
+
when javax.sound.midi.MetaMessage
|
86
|
+
type = :meta
|
87
|
+
data = java_message.data # this is a byte array in Java, might need conversion?
|
88
|
+
|
89
|
+
when javax.sound.midi.ShortMessage
|
90
|
+
# For command-type messages, the least significant 4 bits of the status byte will be the channel number.
|
91
|
+
# java_message.command will return the desired command's status code in this case, or
|
92
|
+
# we can just use a bitmask to grab the most significant 4 bits of the status byte like so:
|
93
|
+
status = (java_message.status & 0xF0)
|
94
|
+
|
95
|
+
message_class = CLASS_FOR_STATUS[status]
|
96
|
+
return message_class.from_java(java_message, options) if message_class
|
97
|
+
|
98
|
+
type = TYPE_FOR_STATUS[status] || :unknown
|
99
|
+
data = [java_message.data1, java_message.data2]
|
100
|
+
|
101
|
+
else
|
102
|
+
type = :unknown
|
103
|
+
data = []
|
104
|
+
end
|
105
|
+
|
106
|
+
new data, java_message.channel, options.merge({:type => type, :java_message => java_message})
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_java
|
110
|
+
if not @java_message
|
111
|
+
@java_message = javax.sound.midi.ShortMessage.new
|
112
|
+
update_java_message
|
113
|
+
end
|
114
|
+
# else, since all ruby message classes are backed by "ShortMessage",
|
115
|
+
# we should be able to rely on @java_message being set for everything else
|
116
|
+
@java_message
|
117
|
+
end
|
118
|
+
|
119
|
+
def update_java_message
|
120
|
+
@java_message.setMessage(status, @channel, @data[0], @data[1]) if @java_message
|
121
|
+
end
|
122
|
+
|
123
|
+
def data= data
|
124
|
+
@data = data
|
125
|
+
update_java_message
|
126
|
+
end
|
127
|
+
|
128
|
+
def data1
|
129
|
+
@data[0] if @data
|
130
|
+
end
|
131
|
+
|
132
|
+
def data1= data
|
133
|
+
@data[0] = data
|
134
|
+
update_java_message
|
135
|
+
end
|
136
|
+
|
137
|
+
def data2
|
138
|
+
@data[1] if @data
|
139
|
+
end
|
140
|
+
|
141
|
+
def data2= data
|
142
|
+
@data[1] = data
|
143
|
+
update_java_message
|
144
|
+
end
|
145
|
+
|
146
|
+
def status
|
147
|
+
@status ||= (STATUS_FOR_CLASS[self.class] || STATUS_FOR_TYPE[@type])
|
148
|
+
end
|
149
|
+
|
150
|
+
def value
|
151
|
+
@data
|
152
|
+
end
|
153
|
+
|
154
|
+
def to_s
|
155
|
+
"#{type}(#{channel}): #{value.inspect}"
|
156
|
+
end
|
157
|
+
|
158
|
+
def clone
|
159
|
+
self.class.new(@data, @channel, {:type => @type})
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
|
4
|
+
# A collection of methods for building MIDI messages.
|
5
|
+
module MessageBuilder
|
6
|
+
|
7
|
+
def note_on(pitch, velocity=127, channel=0)
|
8
|
+
Messages::NoteOn.new(pitch,velocity,channel)
|
9
|
+
end
|
10
|
+
|
11
|
+
def note_off(pitch, velocity=127, channel=0)
|
12
|
+
Messages::NoteOff.new(pitch,velocity,channel)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Most methods in here take 7-bit ints for their args, but this one takes a 14-bit
|
16
|
+
# The value can be an int in the range 0-16383 (8192 is no bend)
|
17
|
+
# or it can be a float, which is assumed to be in the range -1.0 to 1.0
|
18
|
+
def pitch_bend(value, channel=0)
|
19
|
+
Messages::PitchBend.new(value, channel)
|
20
|
+
end
|
21
|
+
|
22
|
+
def control_change(control, value, channel=0)
|
23
|
+
Messages::ControlChange.new(control, value, channel)
|
24
|
+
end
|
25
|
+
|
26
|
+
def all_notes_off(channel=0)
|
27
|
+
control_change(123, 0, channel)
|
28
|
+
end
|
29
|
+
|
30
|
+
def channel_pressure(pressure, channel=0)
|
31
|
+
Messages::ChannelPressure.new(pressure, channel)
|
32
|
+
end
|
33
|
+
alias channel_aftertouch channel_pressure
|
34
|
+
|
35
|
+
def poly_pressure(pitch, pressure, channel=0)
|
36
|
+
Messages::PolyPressure.new(pitch, pressure, channel)
|
37
|
+
end
|
38
|
+
alias poly_aftertouch poly_pressure
|
39
|
+
|
40
|
+
def program_change(program, channel=0)
|
41
|
+
Messages::ProgramChange.new(program, channel)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Make all methods be module functions (accessible by sending the method name to module directly)
|
45
|
+
instance_methods.each do |method|
|
46
|
+
module_function method
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
module Messages
|
4
|
+
|
5
|
+
class ChannelPressure < Message
|
6
|
+
|
7
|
+
def initialize(pressure, channel=0, options={})
|
8
|
+
super([pressure, 0], channel, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias pressure data1
|
12
|
+
alias pressure= data1=
|
13
|
+
|
14
|
+
def self.from_java(java_message, options={})
|
15
|
+
new java_message.data1, java_message.channel, options.merge({:java_message => java_message})
|
16
|
+
end
|
17
|
+
|
18
|
+
def clone
|
19
|
+
self.class.new(pressure,@channel)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module JSound
|
2
|
+
module Midi
|
3
|
+
module Messages
|
4
|
+
|
5
|
+
class ControlChange < Message
|
6
|
+
|
7
|
+
def initialize(control, value, channel=0, options={})
|
8
|
+
super([control,value], channel, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias control data1
|
12
|
+
alias control= data1=
|
13
|
+
|
14
|
+
alias value data2
|
15
|
+
alias value= data2=
|
16
|
+
|
17
|
+
def self.from_java(java_message, options={})
|
18
|
+
new java_message.data1, java_message.data2, java_message.channel, options.merge({:java_message => java_message})
|
19
|
+
end
|
20
|
+
|
21
|
+
def clone
|
22
|
+
self.class.new(control,value,@channel)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|