midi-winmm 0.1.10
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|