|
With MIDI In, the procedure is similar. The problem is you have to prepare a MIDIHDR buffer of x size (x is up to you), add it using midiInAddBuffer and that will be returned to you through a MIM_LONGDATA message in a MidiInProc callback - dwParam1 (you will need to cast the IntPtr to int ) contains the MIDIHDR (dwParam2 contains the timestamp which you probably don't need). To get a callback, you will need to set CALLBACK_FUNCTION in the dwFlags when using midiInOpen and also call midiInStart .
Checking the dwBytesRecorded (you will need to add a property to expose it) will tell you how much data there actually is in the buffer. The real data can be copied out of the buffer using Marshal.Copy . Don't forget to unprepare the buffer, free the GCHandle and the data pointer! If it is full and the last byte isn't EOX, you will need to quickly add another buffer to get the rest of the sysex data (you can add more than one buffer)!
Once you have all the data and freed everything you can stop and close the input when you're done - one issue though. You may have prepared and added a buffer(s) but received no sysex into it so it's just sitting there. To deal with these you will need to call midiInReset , get the buffers via the callback, unprepare them (and free the GCHandle and data pointer) before calling midiInClose . There is a midiInStop function, this doesn't return unused buffers. I personally call midiInReset , get the buffers in the callback (freeing and unpreparing them) and then call midiInStop to ensure compatibility with all device drivers just in case.
If you get stuck I will happily knock you up some smple code. Based on what I've already given you and this[^] you should be OK though.
Short messages are a doddle. Open, Start, receive MIM_DATA in the callback and extract the bytes from dwParam1, and once you're done Stop (or Reset then Stop) and Close.
Edit: Just checked, calling midiInClose on a MIDI In that still has buffers will not close the device. Instead the function result will be MIDIERR_STILLPLAYING.
modified 8-Sep-12 4:29am.
|
|
|
|
|
Thanks Dave!
I'm not sure I'm that smart, but with a bit more research on the web, I may figure it out. TBO, I need to re-learn C# programming every time I have a project like this to work on. When complete, I'll probably not touch programming in C# until I get another project I want to tackle maby 6-12 months from now. I tend to do most of my programming in AutoHotkey, unless I want something a bit beefier. It's definatly easier to step through studio code than ahk code, as you can't set breakpoins and step into AHK code.
|
|
|
|
|
I'll see if I can knock up some code for you. May take a few days due to other commitments and it's not the easiest part of the MIDI API to deal with! If you get it sorted, let me know
|
|
|
|
|
That would be more than fine! Thanks again Dave!
|
|
|
|
|
Haven't forgotten this Jeff! I figured that this was worth making into a full blown article so I'm putting in more effort than I would otherwise - fun stuff though
I'll keep you updated - hopefully it'll finish up with a library that will make it a doddle to use all the low level MIDI API and Stream API directly from C# with no effort. It shouldn't take long now, it's nearly finished.
|
|
|
|
|
That would be so awesome Dave! There needs to be something better and more updated available like that for the population. I'm sure someone besides myself will benefit from your efforts! Thanks for doing so, and I look forward to the article.
|
|
|
|
|
Well work has put a delay on this. Got a huge project on with a deadline of only two weeks so throwing in 12 hour plus days I'm not going to get time to do anything with this until that's over unfortunately.
In the meantime, this is what I have completed so far. All the MIDI In and MIDI Out is working including SysEx
Not everything is commented and handle locking needs some more attention to ensure thread safety. There is no inclusion of the MIDI Stream API yet.
Check out the GeneralInstructions.txt file in the root folder. That should be enough to get you underway.
http://www.codeproject.com/script/Membership/Uploads/3392848/Midi.Net_Solution.zip[^]
I will get back to this and complete it/clean it up and write the article, but it's usable as is and will probably be enough for your purposes.
|
|
|
|
|
Dave,
Thanks for this! I'll attempt to decipher what you've done here. It is much appriciated.
|
|
|
|
|
I managed to get this somewhat working Dave. I'm having issues however with the SystemExclusiveMessageRecieved however. It reports back as Midi.Net.Messages.SystemCommon.Sysex.SystemExclusive, and then hangs the app.
The ShortMessageRecieved works ok, although I would have preffered to have raw data reported rather than transformed. I do believe I can work around that however. Again, Thanks for your effort.
Jeff
|
|
|
|
|
I've reworked alot of the code and thoroughly tested it too so I know it works just fine. The handle locking is improved so there should be no deadlocks. I've still not got to the Stream API stuff but that can wait.
For the short message, in the MidiInput class, the Callback method receives the data as an IntPtr in dwParam1. To get the raw data, cast this to an int. The int is packed by Windows like this:
bits 0-7: Status byte - will be 0x80-0xFF
bits 8-15: First data byte (if applicable) - will be 0x00-0x7F
bits 16-23: second data byte (if applicable) - will be 0x00-0x7F
bits 24-31: unused - should be 0x00
The new version handles this better too with interfaces to give more suitable access to the data if required. Implementing a ToByteArray() in the base interface would be no big deal. I'll have a look before uploading
I'm not sure why your app is hanging on SystemExclusiveMessageRecieved, it's probably fixed in the new code anyway so let me know.
I'll upload the revised code later and drop you a message to let you know.
|
|
|
|
|
Dave,
TBO, I don't need any streaming stuff, but it should still be usefull in your article for others. My needs are stritly for communication to and from a hardware controller device to create a map for another software I use. I look forward to your revised code, and again, thanks for your efforts.
Jeff
|
|
|
|
|
The code has been updated, just use the link from before
There are still numerous XML comments missing (a minor but time consuming thing to do) and as I mentionsed, the Stream API (which at the moment you don't require), but otherwise it's fairly complete and much 'tighter' than before.
As you will see, I've changed the way some things work quite drastically:
- There is one common event for receiving both short and long messages, check out the Console Test app to see examples.
- Short and long messages are both sent via the same method (as an
IMidiMessage ). - Combined messages such as 14bit controllers, RPNs etc implement
IEnumerable<IMidiMessage> , and can sent by simply calling the Send method overload. - The raw bytes received are available in
e.MidiMessage.ToByteArray() as requested. - Program changes, controllers etc have the standard names defined in enums - or you can use a numeric value if needed.
- Many classes have static readonly instances of commonly required things or static methods that with the required parameters will create commonly required instances.
There are many other things too, but as you can see, this is now nearing a complet project!
If you have any issues, let me know and I'll happily look into them.
|
|
|
|
|
I have uploaded an update to the solution: Midi.Net_Solution.zip (392.5 KB)
Just the stream stuff to do now I think, and MIDI file/sysex dump file handling if I decide to add it (I may add some more Universal System Exclusive static stuff too). Then, an article to write - should be fun
Let me know if you have any issues.
|
|
|
|
|
Will Do Dave!
Thanks again!
|
|
|
|
|
Dave,
Just an FYI... When closing the app, it tends to hang on the "InternalReset()" when comparing "NativeMethods.midiInReset(handle)". It never exits unless I bypass the if statement completely. This is the first I've had time to play with the latest code.
|
|
|
|
|
Can you navigate to private void Dispose(bool disposing) in MidiInput and put a breakpoint on if (buffers.Count != 0) (line 351). Does it get hit, and if so, what is the value of buffers.Count ?
|
|
|
|
|
Yes, it gets hit, and the buffer count is 7.
|
|
|
|
|
The issue is actually caused by line 454. If I remark 454, and the brackets associated with that if statement, all works as expected. I also verified it to hang on the actual statement NativeMethods.midiInReset(handle) by inserting it above line 454 in a console.write command.
Not sure if it matters, but I'm testing this with Maple Virtual Midi Cable. Perhaps its a problem with that. I don't have this on my other machine that I'm using the controller on, and haven't had time to verify it on that machine.
|
|
|
|
|
I can't test with Maple Virtual Midi Cable as it won't run on Win 7 x64. I have to suspect that there is a problem with their driver as it works fine with all my MidiMan and MAudio hardware, as well as with MidiYoke.
There seems to be something a little odd. There are 8 buffers assigned when Record is called. Every time one is used, another is assigned to take it's place unless the input is closing or resetting, so the first time line 454 is hit, there should be 8 buffers, even if you have received data.
Perhaps there is something going wrong for you in Callback ? The buffers are returned to there which will reduce the count to 7 when the first SysEx is received, but should be re-added in the block from line 280. Can you put breakpoints on:
- Line 253 and check the value of
wMsg it should be 964. This should get hit 9 times - the first time SysEx is received, then once for each of the 8 buffers when midiInReset is called.
- Line 276: Does it get hit? I'm suspecting not from what you describe. If not, the call to
midiInUnprepareHeader on line 274 may be failing. If so, refactor that line so you can capture the result and let me know what the value is.
- Line 288: Does it get hit? If the above succeeded then it's possible that
midiInPrepareHeader or midiInAddBuffer are failing on lines 285 and 286. If so, refactor and call the two functions separately and let me know the return result(s).
It's very important that midiInReset is called so that the buffers can be returned by Windows, unprepared and freed - all this while the handle is still valid. Bypassing it may get the program to the end but could eventually cause RAM access problems without a reboot, BSOD or just an entire system hang/freeze.
I did consider creating a custom MidiException class and throwing an instance when a function returned non-zero. I decided against it in the end as it's important that everything gets disposed while live and not during a later GC. Halting on an exception would have made that impossible.
If we can't get to the bottom of this I will add some logging so the error codes are captured and saved.
|
|
|
|
|
Ah!
I don't run Maple on my win7 x64 system because it has a controller, but am running it on the XP system because it doesn't, and I needed some way to test.
Is line 253 = "MidiBuffer midiBuffer = null;" Buffers = 8. from there, it makes it to line 274 and stalls as stated previously. It does not hit 276 or 288. It simply stalls dead.
Dave,
I do appriciate the effort you've put into this, but as with the PureMIDI example, this has become much too complex for my needs, and has way too much overhead. I can connect to ports without issue, I can send data to the controller, now I simply need to capture the raw data from the controller. I don't even want to split the status byte into type and channel. I would like it as 0xD2D1st. Anything and everything beyond that is complete overkill for my needs. At most providing this as a byte array would be acceptable as well.
For my application, I will be capturing the control input, and placing into a map file in the format 0xst:0xD1:0xD2. I will likely be looping that back to the controller with possible D2 and status mods to light the corresponding Button lamp. That is basically it. I found that some controllers like the M-Audio xponent, will send a proper note on and note off status with a velocity of 0x40, while others will always send a note on with a velocity of 0x00 or 0x7f to indicate note on or off. I need to be able to work with both these possabilities while mapping the controllers.
I can send a sysex command if necesary as well to change controller modes. As for reading the sysex feedback, I may or may not chase that at some point, but not sure it's necessary.
I'm glad you're putting together an article for this, as I do think it needs better attention, but I also feel that it's going to be too advanced for most judging by the current code. For a novice, I would think providing a basic example would suffice, and then maybe expand on that with a couple more articles for the seasoned, and then advanced users to learn from.
What I need, is to learn how to make the callback, to collect the Midi Input.
Again, Thanks, but as it stands, I can't use much of this.
Jeff
|
|
|
|
|
 OK, as simple as I can make it. MIDI input (without SysEx), no error checking:
using System;
using System.Runtime.InteropServices;
class Program
{
private static MidiTest midiTest;
static void Main(string[] args)
{
midiTest = new MidiTest();
midiTest.Start();
Console.ReadKey();
midiTest.Stop();
}
}
internal class MidiTest
{
private delegate void MidiInProc(IntPtr hMidiIn, int wMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2);
private IntPtr handle;
private MidiInProc callback;
public void Start()
{
callback = new MidiInProc(Callback);
midiInOpen(out handle, 0, callback, IntPtr.Zero, 0x00030000);
midiInStart(handle);
}
public void Stop()
{
midiInStop(handle);
midiInClose(handle);
handle = IntPtr.Zero;
callback = null;
}
private void Callback(IntPtr hMidiIn, int wMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2)
{
switch (wMsg)
{
case 0x3C3:
int data = (int)dwParam1;
int timestamp = (int)dwParam2;
break;
}
}
[DllImport("Winmm.dll")]
private static extern int midiInOpen(out IntPtr lphMidiIn, int uDeviceID, MidiInProc dwCallback, IntPtr dwCallbackInstance, int dwFlags);
[DllImport("Winmm.dll")]
private static extern int midiInStart(IntPtr hMidiIn);
[DllImport("Winmm.dll")]
private static extern int midiInStop(IntPtr hMidiIn);
[DllImport("Winmm.dll")]
private static extern int midiInClose(IntPtr hMidiIn);
}
|
|
|
|
|
Just what I was looking for Dave!
Thanks much!
|
|
|
|
|
One quick question, are you terminating the application by pressing a key or by the close button on the console window. If the latter, that could be why it's hanging.
Edit: Some research reveals this quote from MSDN:
"To stop recording, use midiInStop. Before closing the device by using the midiInClose function, mark any pending data blocks as being done by calling midiInReset".
So it appears that the procedure should be Stop, Reset, Unprepare and free the buffers, then Close. I haven't used the Stop as it appeared (possibly erroneously) that Reset did the same thing, and easy fix thankfully.
modified 11-Oct-12 17:54pm.
|
|
|
|
|
Well, in the begining, the first time I tried your first example, I closed it via the close button of the console, and quickly learned a leason. When I get this error now, it is only with Maple VMC. In my WPF app, I have a onclose function that closes the midi ports. I don't know of a way to do this with a console window however, and as it's just for simple testing, I'm not too worried about that... Just have to remember to close it properly with any keypress.
I've got to reboot now, as I tried your simple Midi, and, guess what. I did it again! Not I've got an active service and it won't run because it's locked. lol.
I'm guessing that's just what I needed. Thanks once again!
Jeff
|
|
|
|
|
BTW, I removed Maple VMC, and installed Midi-Yoke, and problem above appears resolved. I suspec you are right about Maples driver. I've heard others having problems with Maple also, so I'll put that down as accurate.
|
|
|
|
|