midi-winmm 0.1.10
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/LICENSE +13 -0
- data/README.rdoc +46 -0
- data/lib/midi-winmm.rb +15 -0
- data/lib/midi-winmm/device.rb +81 -0
- data/lib/midi-winmm/input.rb +155 -0
- data/lib/midi-winmm/map.rb +229 -0
- data/lib/midi-winmm/output.rb +139 -0
- data/test/config.rb +12 -0
- data/test/helper.rb +37 -0
- data/test/test_input_buffer.rb +45 -0
- data/test/test_io.rb +82 -0
- metadata +67 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2010-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,46 @@
|
|
1
|
+
= midi-winmm
|
2
|
+
|
3
|
+
== Summary
|
4
|
+
|
5
|
+
Realtime MIDI input and output with Ruby for Windows/Cygwin. Uses the WinMM system API
|
6
|
+
|
7
|
+
Note that in the interest of allowing people on other platforms to utilize your code, you should consider using {unimidi}[http://github.com/arirusso/unimidi]. Unimidi is a platform independent wrapper which implements midi-winmm.
|
8
|
+
|
9
|
+
== Features
|
10
|
+
|
11
|
+
* Input and output on multiple devices concurrently
|
12
|
+
* Agnostically handle different MIDI Message types (including SysEx)
|
13
|
+
* Timestamped input events
|
14
|
+
|
15
|
+
== Requirements
|
16
|
+
|
17
|
+
* {ffi}[http://github.com/ffi/ffi] (gem install ffi)
|
18
|
+
|
19
|
+
== Install
|
20
|
+
|
21
|
+
gem install midi-winmm
|
22
|
+
|
23
|
+
== Examples
|
24
|
+
|
25
|
+
* {input}[http://github.com/arirusso/midi-winmm/blob/master/examples/input.rb]
|
26
|
+
* {output}[http://github.com/arirusso/midi-winmm/blob/master/examples/output.rb]
|
27
|
+
|
28
|
+
({more}[http://github.com/arirusso/midi-winmm/blob/master/examples])
|
29
|
+
|
30
|
+
== Tests
|
31
|
+
|
32
|
+
please see {test/config.rb}[http://github.com/arirusso/midi-winmm/blob/master/test/config.rb] before running tests
|
33
|
+
|
34
|
+
== Documentation
|
35
|
+
|
36
|
+
{rdoc}[http://rdoc.info/gems/midi-winmm]
|
37
|
+
|
38
|
+
== Author
|
39
|
+
|
40
|
+
{Ari Russo}[http://github.com/arirusso] <ari.russo at gmail.com>
|
41
|
+
|
42
|
+
== License
|
43
|
+
|
44
|
+
Apache 2.0, See the file LICENSE
|
45
|
+
|
46
|
+
Copyright (c) 2010-2011 Ari Russo
|
data/lib/midi-winmm.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Set of modules and classes for interacting with MIDI functions
|
4
|
+
# of the WinMM System API
|
5
|
+
#
|
6
|
+
module MIDIWinMM
|
7
|
+
VERSION = "0.1.10"
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'ffi'
|
11
|
+
|
12
|
+
require 'midi-winmm/device'
|
13
|
+
require 'midi-winmm/input'
|
14
|
+
require 'midi-winmm/map'
|
15
|
+
require 'midi-winmm/output'
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
module MIDIWinMM
|
3
|
+
|
4
|
+
#
|
5
|
+
# Module containing methods used by both input and output devices when using the
|
6
|
+
# WinMM driver interface
|
7
|
+
#
|
8
|
+
module Device
|
9
|
+
|
10
|
+
attr_reader :enabled, :id, :info, :name, :type
|
11
|
+
|
12
|
+
alias_method :enabled?, :enabled
|
13
|
+
|
14
|
+
def initialize(id, name, options = {}, &block)
|
15
|
+
@id = id
|
16
|
+
@info = options[:info]
|
17
|
+
@type = self.class.name.split('::').last.downcase.to_sym
|
18
|
+
@name = name
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.all
|
22
|
+
all_by_type.values.flatten
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.all_by_type
|
26
|
+
types = { :input => Input,
|
27
|
+
:output => Output }
|
28
|
+
available_devices = { :input => [], :output => [] }
|
29
|
+
count = 0
|
30
|
+
types.each do |type, klass|
|
31
|
+
(0..(Map::cfunc(type, :getNumDevs)-1)).each do |i|
|
32
|
+
data = Map::DeviceInfo[type].new
|
33
|
+
Map::cfunc(type, :getDevCapsA, i, data.to_ptr, Map::DeviceInfo[type].size)
|
34
|
+
name = data[:szPname].to_s
|
35
|
+
dev = klass.new(i, name, :info => data)
|
36
|
+
available_devices[type] << dev
|
37
|
+
end
|
38
|
+
end
|
39
|
+
available_devices
|
40
|
+
end
|
41
|
+
|
42
|
+
# select the first device of type <em>type</em>
|
43
|
+
def self.first(type)
|
44
|
+
all_by_type[type].first
|
45
|
+
end
|
46
|
+
|
47
|
+
# select the last device of type <em>type</em>
|
48
|
+
def self.last(type)
|
49
|
+
all_by_type[type].last
|
50
|
+
end
|
51
|
+
|
52
|
+
WinmmCallbackFlag = 0x30000 # we plan to use a callback to collect events
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# High word
|
57
|
+
# High-order byte: Not used.
|
58
|
+
# Low-order byte: Contains a second byte of MIDI data (when needed).
|
59
|
+
# Low word
|
60
|
+
# High-order byte: Contains the first byte of MIDI data (when needed).
|
61
|
+
# Low-order byte: Contains the MIDI status.
|
62
|
+
#
|
63
|
+
def dwmsg_to_array_of_bytes(m)
|
64
|
+
# there has to be a better way to do this
|
65
|
+
s = []
|
66
|
+
m = m.to_s(16)
|
67
|
+
(1..m.length).step(2) { |i| s << m[(m.length-i)-1, 2].hex }
|
68
|
+
s
|
69
|
+
end
|
70
|
+
|
71
|
+
def error?(num)
|
72
|
+
Map::error?(num)
|
73
|
+
end
|
74
|
+
|
75
|
+
def error(num)
|
76
|
+
Map::error(num)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
module MIDIWinMM
|
3
|
+
|
4
|
+
#
|
5
|
+
# Input device class for the WinMM driver interface
|
6
|
+
#
|
7
|
+
class Input
|
8
|
+
|
9
|
+
include Device
|
10
|
+
|
11
|
+
BufferSize = 256
|
12
|
+
|
13
|
+
attr_reader :buffer
|
14
|
+
|
15
|
+
# initializes this device
|
16
|
+
def enable(options = {}, &block)
|
17
|
+
init_input_buffer
|
18
|
+
handle_ptr = FFI::MemoryPointer.new(FFI.type_size(:int))
|
19
|
+
initialize_local_buffer
|
20
|
+
@event_callback = get_event_callback
|
21
|
+
|
22
|
+
Map.winmm_func(:midiInOpen, handle_ptr, @id, @event_callback, 0, Device::WinmmCallbackFlag)
|
23
|
+
|
24
|
+
@handle = handle_ptr.read_int
|
25
|
+
|
26
|
+
Map.winmm_func(:midiInPrepareHeader, @handle, @header.pointer, @header.size)
|
27
|
+
Map.winmm_func(:midiInAddBuffer, @handle, @header.pointer, @header.size)
|
28
|
+
Map.winmm_func(:midiInStart, @handle)
|
29
|
+
|
30
|
+
@enabled = true
|
31
|
+
|
32
|
+
unless block.nil?
|
33
|
+
begin
|
34
|
+
yield(self)
|
35
|
+
ensure
|
36
|
+
close
|
37
|
+
end
|
38
|
+
else
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
alias_method :start, :enable
|
44
|
+
alias_method :open, :enable
|
45
|
+
|
46
|
+
#
|
47
|
+
# returns an array of MIDI event hashes as such:
|
48
|
+
# [
|
49
|
+
# { :data => [144, 90, 100], :timestamp => 1024 },
|
50
|
+
# { :data => [128, 90, 100], :timestamp => 1100 },
|
51
|
+
# { :data => [146, 60, 120], :timestamp => 1200 }
|
52
|
+
# ]
|
53
|
+
#
|
54
|
+
# message data is an array of Numeric bytes
|
55
|
+
#
|
56
|
+
def gets
|
57
|
+
until queued_messages?
|
58
|
+
end
|
59
|
+
msgs = queued_messages
|
60
|
+
@pointer = @buffer.length
|
61
|
+
msgs
|
62
|
+
end
|
63
|
+
|
64
|
+
# same as gets but returns message data as string of hex digits as such:
|
65
|
+
# [
|
66
|
+
# { :data => "904060", :timestamp => 904 },
|
67
|
+
# { :data => "804060", :timestamp => 1150 },
|
68
|
+
# { :data => "90447F", :timestamp => 1300 }
|
69
|
+
# ]
|
70
|
+
#
|
71
|
+
#
|
72
|
+
def gets_s
|
73
|
+
msgs = gets
|
74
|
+
msgs.each { |msg| msg[:data] = numeric_bytes_to_hex_string(msg[:data]) }
|
75
|
+
msgs
|
76
|
+
end
|
77
|
+
alias_method :gets_bytestr, :gets_s
|
78
|
+
alias_method :gets_hex, :gets_s
|
79
|
+
|
80
|
+
# close the device
|
81
|
+
def close
|
82
|
+
Map.winmm_func(:midiInUnprepareHeader, @handle, @header.pointer, @header.size)
|
83
|
+
Map.winmm_func(:midiInStop, @handle)
|
84
|
+
Map.winmm_func(:midiInClose, @handle)
|
85
|
+
@enabled = false
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.first
|
89
|
+
Device.first(:input)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.last
|
93
|
+
Device.last(:input)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.all
|
97
|
+
Device.all_by_type[:input]
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def queued_messages
|
103
|
+
@buffer.slice(@pointer, @buffer.length - @pointer)
|
104
|
+
end
|
105
|
+
|
106
|
+
def queued_messages?
|
107
|
+
@pointer < @buffer.length
|
108
|
+
end
|
109
|
+
|
110
|
+
# prepare the header struct where input event information is held
|
111
|
+
def init_input_buffer
|
112
|
+
@header = Map::MIDIHdr.new
|
113
|
+
@header.write_data(Input::BufferSize)
|
114
|
+
@header[:dwBytesRecorded] = 0
|
115
|
+
@header[:dwFlags] = 0
|
116
|
+
@header[:dwUser] = 0
|
117
|
+
@header[:dwBufferLength] = Input::BufferSize
|
118
|
+
end
|
119
|
+
|
120
|
+
# returns a Proc that is called when the device receives a message
|
121
|
+
def get_event_callback
|
122
|
+
Proc.new do |hMidiIn,wMsg,dwInstance,dwParam1,dwParam2|
|
123
|
+
msg_type = Map::CallbackMessageTypes[wMsg] || ''
|
124
|
+
case msg_type
|
125
|
+
when :input_data then
|
126
|
+
msg = { :data => dwmsg_to_array_of_bytes(dwParam1), :timestamp => dwParam2 }
|
127
|
+
@buffer << msg
|
128
|
+
when :input_long_data then
|
129
|
+
@receiving_sysex = true
|
130
|
+
data = @header[:lpData].read_string(Input::BufferSize).gsub(/ /, '')
|
131
|
+
unless data.eql?("")
|
132
|
+
str = data.unpack(("C" * (data.length-1)))
|
133
|
+
msg = { :data => str, :timestamp => dwParam2 }
|
134
|
+
@buffer << msg
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def initialize_local_buffer
|
141
|
+
@pointer = 0
|
142
|
+
@buffer = []
|
143
|
+
def @buffer.clear
|
144
|
+
super
|
145
|
+
@pointer = 0
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def numeric_bytes_to_hex_string(bytes)
|
150
|
+
bytes.map { |b| s = b.to_s(16).upcase; b < 16 ? s = "0" + s : s; s }.join
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
module MIDIWinMM
|
3
|
+
|
4
|
+
#
|
5
|
+
# Module containing C function and struct binding for the WinMM
|
6
|
+
# driver interface library
|
7
|
+
#
|
8
|
+
module Map
|
9
|
+
|
10
|
+
extend FFI::Library
|
11
|
+
ffi_lib 'Winmm'
|
12
|
+
ffi_convention :stdcall
|
13
|
+
|
14
|
+
HeaderFlags = {
|
15
|
+
0x00000001 => :done,
|
16
|
+
0x00000002 => :prepared,
|
17
|
+
0x00000004 => :inqueue,
|
18
|
+
0x00000008 => :isstream
|
19
|
+
}
|
20
|
+
|
21
|
+
CallbackMessageTypes = {
|
22
|
+
0x3C1 => :input_open,
|
23
|
+
0x3C2 => :input_close,
|
24
|
+
0x3C3 => :input_data,
|
25
|
+
0x3C4 => :input_long_data,
|
26
|
+
0x3C5 => :input_error,
|
27
|
+
0x3C6 => :input_long_error,
|
28
|
+
0x3C7 => :output_open,
|
29
|
+
0x3C8 => :output_close,
|
30
|
+
0x3C9 => :output_data,
|
31
|
+
0x3CC => :output_more_data
|
32
|
+
}
|
33
|
+
|
34
|
+
Errors = {
|
35
|
+
1 => "Unspecified",
|
36
|
+
2 => "Bad Device ID",
|
37
|
+
3 => "Not Enabled",
|
38
|
+
4 => "Allocation",
|
39
|
+
5 => "Invalid Handle",
|
40
|
+
6 => "No Driver",
|
41
|
+
7 => "No Memory",
|
42
|
+
8 => "Not Supported",
|
43
|
+
9 => "Bad Error Number",
|
44
|
+
10 => "Invalid Flag",
|
45
|
+
11 => "Invalid Parameter",
|
46
|
+
12 => "Handle Busy",
|
47
|
+
13 => "Invalid Alias"
|
48
|
+
}
|
49
|
+
|
50
|
+
# Byte (8 bits). Declared as unsigned char.
|
51
|
+
typedef :uchar, :BYTE
|
52
|
+
# 32-bit unsigned integer. The range is 0 through 4,294,967,295 decimal.
|
53
|
+
typedef :uint32, :DWORD
|
54
|
+
# Unsigned long type for pointer precision. Use when casting a pointer to a long type.
|
55
|
+
typedef :ulong, :DWORD_PTR
|
56
|
+
# (L) Handle to an object. WinNT.h: #typedef PVOID HANDLE;
|
57
|
+
typedef :ulong, :HANDLE
|
58
|
+
# Handle for a MIDI input device.
|
59
|
+
typedef :HANDLE, :HMIDIIN
|
60
|
+
# Handle for a MIDI output device.
|
61
|
+
typedef :HANDLE, :HMIDIOUT
|
62
|
+
# system function result codes
|
63
|
+
typedef :uint, :MMRESULT
|
64
|
+
# Unsigned INT_PTR.
|
65
|
+
typedef :uint, :UINT_PTR
|
66
|
+
# 16-bit unsigned integer. The range is 0 through 65535 decimal.
|
67
|
+
typedef :ushort, :WORD
|
68
|
+
|
69
|
+
class MIDIEvent < FFI::Struct
|
70
|
+
layout :dwDeltaTime, :ulong,
|
71
|
+
:dwStreamID, :ulong,
|
72
|
+
:dwEvent, :ulong,
|
73
|
+
:dwParms, [:ulong, 8]
|
74
|
+
end
|
75
|
+
|
76
|
+
class MIDIHdr < FFI::Struct
|
77
|
+
layout :lpData, :pointer,
|
78
|
+
:dwBufferLength, :DWORD,
|
79
|
+
:dwBytesRecorded, :DWORD,
|
80
|
+
:dwUser, :DWORD_PTR,
|
81
|
+
:dwFlags, :DWORD,
|
82
|
+
:lpNext, :pointer,
|
83
|
+
:reserved, :DWORD_PTR,
|
84
|
+
:dwOffset, :DWORD,
|
85
|
+
:dwReserved, :DWORD_PTR
|
86
|
+
|
87
|
+
def write_data(size, string = '')
|
88
|
+
ptr = FFI::MemoryPointer.new(:char, size)
|
89
|
+
blank = " " * (size-string.length-1)
|
90
|
+
ptr.put_string(0, string + blank)
|
91
|
+
self[:lpData] = ptr
|
92
|
+
self[:dwBufferLength] = string.length
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class MIDIInputInfo < FFI::Struct
|
97
|
+
layout :wMid, :WORD,
|
98
|
+
:wPid, :WORD,
|
99
|
+
:vDriverVersion, :ulong,
|
100
|
+
:szPname, [:char, 32],
|
101
|
+
:dwSupport, :DWORD
|
102
|
+
end
|
103
|
+
|
104
|
+
class MIDIOutputInfo < FFI::Struct
|
105
|
+
layout :wMid, :WORD,
|
106
|
+
:wPid, :WORD,
|
107
|
+
:vDriverVersion, :ulong,
|
108
|
+
:szPname, [:char, 32],
|
109
|
+
:wTechnology, :WORD,
|
110
|
+
:wVoices, :WORD,
|
111
|
+
:wNotes, :WORD,
|
112
|
+
:wChannelMask, :WORD,
|
113
|
+
:dwSupport, :DWORD
|
114
|
+
end
|
115
|
+
|
116
|
+
DeviceInfo = {
|
117
|
+
|
118
|
+
:input => MIDIInputInfo,
|
119
|
+
:output => MIDIOutputInfo
|
120
|
+
|
121
|
+
}
|
122
|
+
|
123
|
+
# void CALLBACK MidiInProc(HMIDIIN hMidiIn,UINT wMsg,DWORD_PTR dwInstance,DWORD_PTR dwParam1,DWORD_PTR dwParam2)
|
124
|
+
callback :input_callback, [:pointer, :uint, :DWORD_PTR, :DWORD_PTR, :DWORD_PTR], :void
|
125
|
+
|
126
|
+
# void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
|
127
|
+
callback :output_callback, [:pointer, :uint, :DWORD_PTR, :DWORD_PTR, :DWORD_PTR], :void
|
128
|
+
|
129
|
+
#
|
130
|
+
# initialize/close devices
|
131
|
+
#
|
132
|
+
|
133
|
+
# MMRESULT midiInOpen(LPHMIDIIN lphMidiIn, UINT_PTR uDeviceID, DWORD_PTR dwCallback, DWORD_PTR dwCallbackInstance, DWORD dwFlags)
|
134
|
+
# LPHMIDIIN = *HMIDIIN
|
135
|
+
attach_function :midiInOpen, [:pointer, :UINT_PTR, :input_callback, :DWORD_PTR, :DWORD], :MMRESULT
|
136
|
+
|
137
|
+
# MMRESULT midiOutOpen(LPHMIDIOUT lphmo, UINT uDeviceID, DWORD_PTR dwCallback, DWORD_PTR dwCallbackInstance, DWORD dwFlags)
|
138
|
+
# LPHMIDIOUT = *HMIDIOUT
|
139
|
+
# :output_callback
|
140
|
+
attach_function :midiOutOpen, [:pointer, :uint, :output_callback, :DWORD_PTR, :DWORD], :MMRESULT
|
141
|
+
|
142
|
+
attach_function :midiInClose, [:ulong], :ulong
|
143
|
+
attach_function :midiOutClose, [:ulong], :ulong
|
144
|
+
attach_function :midiInReset, [:pointer], :ulong
|
145
|
+
|
146
|
+
# MMRESULT midiOutReset(HMIDIOUT hmo)
|
147
|
+
attach_function :midiOutReset, [:HMIDIOUT], :MMRESULT
|
148
|
+
|
149
|
+
#
|
150
|
+
# for message output
|
151
|
+
#
|
152
|
+
|
153
|
+
# MMRESULT midiOutShortMsg(HMIDIOUT hmo, DWORD dwMsg)
|
154
|
+
attach_function :midiOutShortMsg, [:HMIDIOUT, :DWORD], :MMRESULT
|
155
|
+
|
156
|
+
# MMRESULT midiOutLongMsg(HMIDIOUT hmo, LPMIDIHDR lpMidiOutHdr,UINT cbMidiOutHdr)
|
157
|
+
# LPMIDIHDR = *MIDIHDR
|
158
|
+
attach_function :midiOutLongMsg, [:HMIDIOUT, :pointer, :uint], :MMRESULT
|
159
|
+
|
160
|
+
# MMRESULT midiStreamOpen(LPHMIDISTRM lphStream,LPUINT puDeviceID, DWORD cMidi, DWORD_PTR dwCallback, DWORD_PTR dwInstance, DWORD fdwOpen)
|
161
|
+
attach_function :midiStreamOpen, [:pointer, :pointer, :DWORD, :DWORD_PTR, :DWORD_PTR, :DWORD], :MMRESULT
|
162
|
+
|
163
|
+
# MMRESULT midiOutPrepareHeader(HMIDIOUT hmo, LPMIDIHDR lpMidiOutHdr, UINT cbMidiOutHdr)
|
164
|
+
attach_function :midiOutPrepareHeader, [:HMIDIOUT, :pointer, :uint], :MMRESULT
|
165
|
+
|
166
|
+
#MMRESULT midiOutUnprepareHeader(HMIDIOUT hmo, LPMIDIHDR lpMidiOutHdr, UINT cbMidiOutHdr)
|
167
|
+
attach_function :midiOutUnprepareHeader, [:HMIDIOUT, :ulong, :uint], :MMRESULT
|
168
|
+
|
169
|
+
#MMRESULT midiOutGetVolume(HMIDIOUT hmo, LPDWORD lpdwVolume)
|
170
|
+
attach_function :midiOutGetVolume, [:HMIDIOUT, :pointer], :MMRESULT
|
171
|
+
|
172
|
+
# MMRESULT midiOutSetVolume(HMIDIOUT hmo, DWORD dwVolume)
|
173
|
+
attach_function :midiOutSetVolume, [:HMIDIOUT, :DWORD], :MMRESULT
|
174
|
+
|
175
|
+
#
|
176
|
+
# input
|
177
|
+
#
|
178
|
+
|
179
|
+
# MMRESULT midiInPrepareHeader(HMIDIIN hMidiIn, LPMIDIHDR lpMidiInHdr, UINT cbMidiInHdr)
|
180
|
+
attach_function :midiInPrepareHeader, [:HMIDIIN, :pointer, :uint], :MMRESULT
|
181
|
+
|
182
|
+
attach_function :midiInUnprepareHeader, [:HMIDIIN, :pointer, :uint], :MMRESULT
|
183
|
+
attach_function :midiInAddBuffer, [:HMIDIIN, :pointer, :uint], :MMRESULT
|
184
|
+
|
185
|
+
# MMRESULT midiInStart(HMIDIIN hMidiIn)
|
186
|
+
attach_function :midiInStart, [:HMIDIIN], :MMRESULT
|
187
|
+
|
188
|
+
# MMRESULT midiInStop(HMIDIIN hMidiIn)
|
189
|
+
attach_function :midiInStop, [:HMIDIIN], :MMRESULT
|
190
|
+
|
191
|
+
#
|
192
|
+
# enumerate devices
|
193
|
+
#
|
194
|
+
|
195
|
+
# UINT midiInGetNumDevs(void)
|
196
|
+
attach_function :midiInGetNumDevs, [], :uint
|
197
|
+
|
198
|
+
# UINT midiOutGetNumDevs(void)
|
199
|
+
attach_function :midiOutGetNumDevs, [], :uint
|
200
|
+
|
201
|
+
# MMRESULT midiInGetDevCaps(UINT_PTR uDeviceID, LPMIDIINCAPS lpMidiInCaps, UINT cbMidiInCaps);
|
202
|
+
attach_function :midiInGetDevCapsA, [:UINT_PTR, :pointer, :uint], :MMRESULT
|
203
|
+
|
204
|
+
attach_function :midiOutGetDevCapsA, [:UINT_PTR, :pointer, :uint], :MMRESULT
|
205
|
+
|
206
|
+
# shortcut for calling winmm midi functions
|
207
|
+
def self.cfunc(type, funcname, *args)
|
208
|
+
t = type.to_s.gsub(/put/,'') # convert output to out, input to in
|
209
|
+
name = funcname.to_s
|
210
|
+
name[0] = name[0,1].upcase # capitalize
|
211
|
+
t[0] = t[0,1].upcase # capitalize
|
212
|
+
self.send("midi#{t}#{name}", *args)
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.winmm_func(name, *args)
|
216
|
+
status = self.send(name, *args)
|
217
|
+
raise "#{name.to_s}: #{error(status)}" if error?(status)
|
218
|
+
end
|
219
|
+
|
220
|
+
def self.error?(num)
|
221
|
+
!Map::Errors[num].nil?
|
222
|
+
end
|
223
|
+
|
224
|
+
def self.error(num)
|
225
|
+
Map::Errors[num]
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
module MIDIWinMM
|
3
|
+
|
4
|
+
#
|
5
|
+
# Output device class for the WinMM driver interface
|
6
|
+
#
|
7
|
+
class Output
|
8
|
+
|
9
|
+
include Device
|
10
|
+
|
11
|
+
BufferSize = 2048
|
12
|
+
|
13
|
+
attr_reader :buffer
|
14
|
+
|
15
|
+
# initialize this device
|
16
|
+
def enable(options = {}, &block)
|
17
|
+
init_output_buffer
|
18
|
+
Map.winmm_func(:midiOutOpen, Output::HandlePointer, @id, Output::EventCallback, 0, Device::WinmmCallbackFlag)
|
19
|
+
@handle = HandlePointer.read_int
|
20
|
+
@enabled = true
|
21
|
+
unless block.nil?
|
22
|
+
begin
|
23
|
+
yield(self)
|
24
|
+
ensure
|
25
|
+
close
|
26
|
+
end
|
27
|
+
else
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
alias_method :start, :enable
|
32
|
+
alias_method :open, :enable
|
33
|
+
|
34
|
+
# close this device
|
35
|
+
def close
|
36
|
+
Map.winmm_func(:midiOutClose, @handle)
|
37
|
+
@enabled = false
|
38
|
+
end
|
39
|
+
|
40
|
+
# returns a hash of fixnum values { :left => n, :right => n2 }
|
41
|
+
def volume
|
42
|
+
volume = FFI::MemoryPointer.new(FFI.type_size(:ulong))
|
43
|
+
|
44
|
+
Map.winmm_func(:midiOutGetVolume, @handle, volume)
|
45
|
+
|
46
|
+
str = dwmsg_to_array_of_bytes(volume.read_ulong)
|
47
|
+
left = str.slice!(0,4)
|
48
|
+
|
49
|
+
{ :left => left.hex, :right => str.hex }
|
50
|
+
end
|
51
|
+
|
52
|
+
# accepts either a hash of fixnums { :left => n, :right => n2 } or
|
53
|
+
# a single fixnum that will be applied to both channels
|
54
|
+
def volume=(val)
|
55
|
+
vol = val.kind_of?(Hash) ? (val[:left] + (val[:right] << 16)) : (val + (val << 16))
|
56
|
+
Map.winmm_func(:midiOutSetVolume, @handle, vol)
|
57
|
+
end
|
58
|
+
|
59
|
+
def reset
|
60
|
+
Map.winmm_func(:midiOutReset, @handle)
|
61
|
+
end
|
62
|
+
|
63
|
+
# send a message of an indeterminate type
|
64
|
+
def puts(*a)
|
65
|
+
case a.first
|
66
|
+
when Array then puts_bytes(*a.first)
|
67
|
+
when Numeric then puts_bytes(*a)
|
68
|
+
when String then puts_s(*a)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# send a message consisting of Numeric bytes
|
73
|
+
def puts_bytes(*message_bytes)
|
74
|
+
format = "C" * message_bytes.size
|
75
|
+
|
76
|
+
packed = message_bytes.pack(format)
|
77
|
+
data_pointer = FFI::MemoryPointer.new(message_bytes.size).put_bytes(0, packed)
|
78
|
+
|
79
|
+
@header[:dwBufferLength] = message_bytes.size
|
80
|
+
@header[:dwBytesRecorded] = message_bytes.size
|
81
|
+
@header[:lpData] = data_pointer
|
82
|
+
|
83
|
+
Map.winmm_func(:midiOutPrepareHeader, @handle, @header.pointer, @header.size)
|
84
|
+
|
85
|
+
Map.winmm_func(:midiOutLongMsg, @handle, @header.pointer, @header.size)
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
# send a message consisisting of a String of hex digits
|
90
|
+
def puts_s(data)
|
91
|
+
data = data.dup
|
92
|
+
output = []
|
93
|
+
until (str = data.slice!(0,2)).eql?("")
|
94
|
+
output << str.hex
|
95
|
+
end
|
96
|
+
puts_bytes(*output)
|
97
|
+
end
|
98
|
+
alias_method :puts_bytestr, :puts_s
|
99
|
+
alias_method :puts_hex, :puts_s
|
100
|
+
|
101
|
+
def self.first
|
102
|
+
Device::first(:output)
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.last
|
106
|
+
Device::last(:output)
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.all
|
110
|
+
Device.all_by_type[:output]
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
EventCallback = Proc.new do |hmo, wMsg, dwInstance, dwParam1, dwParam2|
|
116
|
+
msg_type = Map::CallbackMessageTypes[wMsg] || ''
|
117
|
+
if msg_type.eql?(:output_data)
|
118
|
+
header = dwParam1
|
119
|
+
handle = HandlePointer.read_int
|
120
|
+
Map.winmm_func(:midiOutUnprepareHeader, handle, header, Map::MIDIHdr.size)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
HandlePointer = FFI::MemoryPointer.new(FFI.type_size(:int))
|
125
|
+
|
126
|
+
def init_output_buffer
|
127
|
+
@header = Map::MIDIHdr.new
|
128
|
+
d = FFI::MemoryPointer.new(:char, Output::BufferSize)
|
129
|
+
d.put_string(0, " " * (Output::BufferSize - 1)) # initialize with a string of spaces
|
130
|
+
@header[:lpData] = d.address
|
131
|
+
@header[:dwBufferLength] = Output::BufferSize
|
132
|
+
@header[:dwBytesRecorded] = 0
|
133
|
+
@header[:dwFlags] = 0
|
134
|
+
@header[:dwUser] = 0
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
data/test/config.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module TestHelper::Config
|
2
|
+
|
3
|
+
include MIDIWinMM
|
4
|
+
|
5
|
+
# adjust these constants to suit your hardware configuration
|
6
|
+
# before running tests
|
7
|
+
|
8
|
+
NumDevices = 4 # this is the total number of MIDI devices that are connected to your system
|
9
|
+
TestInput = Input.first # this is the device you wish to use to test input
|
10
|
+
TestOutput = Output.all[1] # likewise for output
|
11
|
+
|
12
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
2
|
+
$LOAD_PATH.unshift dir + '/../lib'
|
3
|
+
|
4
|
+
require 'test/unit'
|
5
|
+
require 'midi-winmm'
|
6
|
+
|
7
|
+
module TestHelper
|
8
|
+
|
9
|
+
def bytestrs_to_ints(arr)
|
10
|
+
data = arr.map { |m| m[:data] }.join
|
11
|
+
output = []
|
12
|
+
until (bytestr = data.slice!(0,2)).eql?("")
|
13
|
+
output << bytestr.hex
|
14
|
+
end
|
15
|
+
output
|
16
|
+
end
|
17
|
+
|
18
|
+
# some MIDI messages
|
19
|
+
VariousMIDIMessages = [
|
20
|
+
[0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7], # SysEx
|
21
|
+
[0x90, 100, 100], # note on
|
22
|
+
[0x90, 43, 100], # note on
|
23
|
+
[0x90, 76, 100], # note on
|
24
|
+
[0x90, 60, 100], # note on
|
25
|
+
[0x80, 100, 100] # note off
|
26
|
+
]
|
27
|
+
|
28
|
+
# some MIDI messages
|
29
|
+
VariousMIDIByteStrMessages = [
|
30
|
+
"F04110421240007F0041F7", # SysEx
|
31
|
+
"906440", # note on
|
32
|
+
"804340" # note off
|
33
|
+
]
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
require File.dirname(__FILE__) + '/config'
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class InputBufferTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
include MIDIWinMM
|
8
|
+
include TestHelper
|
9
|
+
include TestHelper::Config # before running these tests, adjust the constants in config.rb to suit your hardware setup
|
10
|
+
# ** this test assumes that TestOutput is connected to TestInput
|
11
|
+
|
12
|
+
def test_input_buffer
|
13
|
+
sleep(1)
|
14
|
+
|
15
|
+
messages = VariousMIDIMessages
|
16
|
+
bytes = []
|
17
|
+
|
18
|
+
TestOutput.open do |output|
|
19
|
+
TestInput.open do |input|
|
20
|
+
|
21
|
+
messages.each do |msg|
|
22
|
+
|
23
|
+
$>.puts "sending: " + msg.inspect
|
24
|
+
|
25
|
+
output.puts(msg)
|
26
|
+
|
27
|
+
bytes += msg
|
28
|
+
|
29
|
+
sleep(0.5)
|
30
|
+
|
31
|
+
buffer = input.buffer.map { |m| m[:data] }.flatten
|
32
|
+
|
33
|
+
$>.puts "received: " + buffer.to_s
|
34
|
+
|
35
|
+
assert_equal(bytes, buffer)
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
assert_equal(bytes.length, input.buffer.map { |m| m[:data] }.flatten.length)
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/test/test_io.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class IoTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
include MIDIWinMM
|
8
|
+
include TestHelper
|
9
|
+
include TestHelper::Config # before running these tests, adjust the constants in config.rb to suit your hardware setup
|
10
|
+
# ** this test assumes that TestOutput is connected to TestInput
|
11
|
+
|
12
|
+
def test_full_io
|
13
|
+
sleep(1)
|
14
|
+
messages = VariousMIDIMessages
|
15
|
+
messages_arr = messages.inject { |a,b| a+b }.flatten
|
16
|
+
received_arr = []
|
17
|
+
pointer = 0
|
18
|
+
TestOutput.open do |output|
|
19
|
+
TestInput.open do |input|
|
20
|
+
|
21
|
+
messages.each do |msg|
|
22
|
+
|
23
|
+
$>.puts "sending: " + msg.inspect
|
24
|
+
|
25
|
+
output.puts(msg)
|
26
|
+
sleep(1)
|
27
|
+
received = input.gets.map { |m| m[:data] }.flatten
|
28
|
+
|
29
|
+
|
30
|
+
$>.puts "received: " + received.inspect
|
31
|
+
|
32
|
+
assert_equal(messages_arr.slice(pointer, received.length), received)
|
33
|
+
|
34
|
+
pointer += received.length
|
35
|
+
|
36
|
+
received_arr += received
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
assert_equal(messages_arr.length, received_arr.length)
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# ** this test assumes that TestOutput is connected to TestInput
|
47
|
+
def test_full_io_bytestr
|
48
|
+
sleep(1) # pause between tests
|
49
|
+
|
50
|
+
messages = VariousMIDIByteStrMessages
|
51
|
+
messages_str = messages.join
|
52
|
+
received_str = ""
|
53
|
+
pointer = 0
|
54
|
+
|
55
|
+
TestOutput.open do |output|
|
56
|
+
TestInput.open do |input|
|
57
|
+
|
58
|
+
messages.each do |msg|
|
59
|
+
|
60
|
+
$>.puts "sending: " + msg.inspect
|
61
|
+
|
62
|
+
output.puts(msg)
|
63
|
+
sleep(1)
|
64
|
+
received = input.gets_bytestr.map { |m| m[:data] }.flatten.join
|
65
|
+
$>.puts "received: " + received.inspect
|
66
|
+
|
67
|
+
assert_equal(messages_str.slice(pointer, received.length), received)
|
68
|
+
|
69
|
+
pointer += received.length
|
70
|
+
|
71
|
+
received_str += received
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
assert_equal(messages_str, received_str)
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: midi-winmm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.10
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ari Russo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-11 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ffi
|
16
|
+
requirement: &70311577275740 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70311577275740
|
25
|
+
description: Realtime MIDI IO with Ruby in Windows/Cygwin using the WinMM system API
|
26
|
+
email:
|
27
|
+
- ari.russo@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- lib/midi-winmm/device.rb
|
33
|
+
- lib/midi-winmm/input.rb
|
34
|
+
- lib/midi-winmm/map.rb
|
35
|
+
- lib/midi-winmm/output.rb
|
36
|
+
- lib/midi-winmm.rb
|
37
|
+
- test/config.rb
|
38
|
+
- test/helper.rb
|
39
|
+
- test/test_input_buffer.rb
|
40
|
+
- test/test_io.rb
|
41
|
+
- LICENSE
|
42
|
+
- README.rdoc
|
43
|
+
homepage: http://github.com/arirusso/midi-winmm
|
44
|
+
licenses: []
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.3.6
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project: midi-winmm
|
63
|
+
rubygems_version: 1.8.6
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Realtime MIDI IO with Ruby in Windows/Cygwin
|
67
|
+
test_files: []
|