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.
- 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
|