DerekCook / CoreMidi4J

Core MIDI Service provider Interace (SPI) for Java 1.7 and above on OS X
Eclipse Public License 1.0
55 stars 15 forks source link

Non-unique names for devices #21

Closed odbuser2 closed 7 years ago

odbuser2 commented 7 years ago

Note: this post has been rewritten for clarity.

Say you have two physical devices (same manufacturer, same model). It is very difficult to figure out which MIDIDevice.Info belongs to which device. Oracle Java's midi implementation does not include enough information so you're left to guess or rely on the order that the information is retrieved. CoreMidi4J loses some information but adds a unique id (via casting).

I had written a similar library to CoreMidi4J that exposed all of the OSX MIDIDeviceRef to the Java world. However, I'd like to switch to CoreMidi4J and propose the following in order to do so:

OSX properly models the midi devices via MIDIDeviceRef. At the top level, it has: uniqueID manufacturer model name : the one that's user modifiable via Audio Midi entities : midi ports that can have sources and destinations. The entity, entity.source, entity.destination have uniqueIDs and names. The names are retrieved from the device and are not changeable unless the entity is something like an IAC - e.g. a virtual device.

Unfortunately, Oracle's java doesn't map these very well. Java provides MidiDevice.Info with: name vendor description

An Info object usually corresponds to a {midiDeviceRef}.entities.{entity}.(source|destination). So Java has essentially flattened the sources and destinations into a list of infos. The class name MidiDevice is misleading to me since it doesn't represent the physical device but rather the source or destination of a midi device.

On Oracle JDK on the OSX you end up with the following mapping: name -> {midiDeviceRef}.entities.{entity}.(destination|source).name vendor -> {midiDeviceRef}.manufacturer description -> {midiDeviceRef}.name + {midiDeviceRef}.entities.{entity}.(destination|source).name

Arg! The description unnecessarily contains the Info.name. Well, ok, it's easy enough to get the {midiDeviceInfo}.name from the description which is what I'm after. Without it I am unable to differentiate between like kind hardware.

In CoreMidi4J with Oracle JDK on OSX you end up with the following mapping: name -> "CoreMIDI4J - " + {midiDeviceRef}.entities.{entity}.(destination|source).name vendor -> {midiDeviceRef}.manufacturer description -> {midiDeviceRef}.model

Arg again! I no longer have access to {midiDeviceRef}.name. What's better about CoreMidi4J though is that CoreMidiDeviceInfo contains: uniqueID -> {midiDeviceRef}.entities.{entity}.(destination|source).uniqueID endPointReference -> not needed in this context

Solution? Model MIDIDeviceRef, Entity, Source, Destination in Java:

...
public List<Entity> getEntities();
}

public interface Entity {
    ...
    public List<Source> getSources();
    public List<Destination> getDestinations();
    public MIDIDevice getMIDIDevice();
}

public interface Endpoint {
    ...
    public Entity getEntity();
}

public interface Source extends Endpoint

public interface Destination extends Endpoint

have CoreMidiDeviceInfo implement Endpoint, MidiDevice.Info

introduce two more classes:

public class CoreMidiDeviceInfoSource extends CoreMidiDeviceInfo implements Source
public class CoreMidiDeviceInfoDestination extends CoreMidiDeviceInfo implements Destination

When returning CoreMidiDeviceInfo's via MidiSystem.getMidiDeviceInfo() or CoreMidiDeviceProvider.getMidiDeviceInfo() make sure that the MIDIDevice model is built fully and only return objects that are consistent with the model. In other words, if two info objects belong to the same device, then the following would be true:

coreMidiDeviceInfoSource.getEntity().getMIDIDeviceInfo() == coreMidiDeviceInfoDestination.getEntity().getMIDIDeviceInfo()
DerekCook commented 7 years ago

Hi,

Yes. Makes sense. Will need to look into seeing if we can get the info from Java

odbuser2 commented 7 years ago

Derek, I cleaned up my original post and comments. Please see the rewritten top post. And for clarity, Derek's comment "Yes. Makes sense..." is in response to my original post which no longer appears. Hopefully it still makes sense :0)

DerekCook commented 7 years ago

Hi, I think it still makes sense to me! :)

Unfortunately I am pretty "rammed" on the day job right now, and it is not going to get any easier this side of Christmas. With all the other things I am doing in the spare time I have and with CoreMIDI4J doing everything I need for my Java Librarians, then I am struggling to find the time on this.

But CoreMIDI4K is meant to a project others can contribute to.

So, if you think you can improve it based on your needs, and are happy to be a contributor then I am happy to add you. I did not want it to be completely open just to retain, some modicum of control, but having got the initial concept up and running, I am really struggling for time on it, so maybe time to allow a few more active contributors with good ideas?

odbuser2 commented 7 years ago

Yes absolutely. I will share whatever I come up with if I have time. You don't need to add me as a contributor just yet. I can fork and work on it from there and share back if I have something useful. I haven't looked at the project code yet but what I've proposed shouldn't be too hard.

BradleyRoss commented 7 years ago

Try the code in DeviceTracker.txt. If you run this, it will give you the unique id and endpoint references for the CoreMidi devices. I would be interested in what happens if you run it with two identical devices attached (such as 2 of the same model Sapphires)

What happens in this code is that casting to a subclass that allows you to obtain the desired information will enable you to meet your needs without changes to CoreMidi4J.

odbuser2 commented 7 years ago

@BradleyRoss yes, I recognize that and that's what caused me to rewrite the issue. While casting gives me the unique ids of the sources and destinations (in OSX speak), it's not sufficient to fully identify and group the output. Let's say I had a fictitious device:

vendor=Acme
model=Ham
name=Ham 1
entities:
  entity
    name=Ham Port
    uniqueid=34
    source
        name=Midi Out
        uniqueid=25
    destination
        name=Midi In
        uniqueid=12

and another one with an optional additional midi port in the expansion bay:

vendor=Acme
model=Ham
name=Ham 2
entities:
  entity
    name=Ham Port
    uniqueid=50
    source
        name=Midi Out
        uniqueid=80
    destination
        name=Midi In
        uniqueid=90
  entity
    name=Ham Port
    uniqueid=60
    source
        name=Midi Out
        uniqueid=20
    destination
        name=Midi In
        uniqueid=30

CoreMIDI4j will have the following infos:

name=CoreMIDI4J - Midi In
vendor=Acme
description=Ham
uniqueid=25
name=CoreMIDI4J - Midi Out
vendor=Acme
description=Ham
uniqueid=12
name=CoreMIDI4J - Midi In
vendor=Acme
description -> Ham
uniqueid=80
name=CoreMIDI4J - Midi Out
vendor=Acme
description=Ham
uniqueid=90
name=CoreMIDI4J - Midi In
vendor=Acme
description=Ham
uniqueid=20
name=CoreMIDI4J - Midi Out
vendor=Acme
description=Ham
uniqueid=30

Given this info, how do I group the ins and outs to a specific device? How do I know which device is which? The name of the "device" isn't present (in Oracle's the name is at the end of the info.description). Order is actually significant but I still won't know I should group the first 4 and the last 2 or the first 2 and the last 4.

You wouldn't notice this issue unless you have multiples of devices. I have multiple effects processors and other devices and it would become a guessing game except that Apple allows that top level device name to be changed. The vendor supplies the source and destination names and I haven't ran into any uniqueness issues there - thankfully b/c you can't change those names.

BradleyRoss commented 7 years ago

What I have actually been doing is creating a list of objects that contain the MidiDevice and MidiDevice.Info. The class for this object than contains additional information that I use for clarifying which device is which. Part of the problem is that you can't use the standard means for obtaining a MidiDevice object given a MidiDevice.Info when dealing with synthesizers and sequencers (and probably other software devices as well). The enclosing object than contains a display name which contains the unique identifier and a method of retrieving the display name. You could include getter and setter methods for the display name in the uk.co.xfactorylibrarians.coremidi4j.CoreMidiDeviceInfo but it isn't necessary.

The Oracle implementation is so buggy that I really don't use the MidiSystem methods to obtain the device object, but only to create my own list of objects when I start the program. I would probably rebuild the list if a received a message that the MIDI configuration has changed.

If you have multiple effects processors of the same model, the driver supplied by the vendor probably gives different names when you have multiple instances of the same model. You might also want to create a name of display names indexed by Unique ID's and store it in a file. That way, physical pieces of equipment would always have the same name no matter what order is used to attach the devices.

DerekCook commented 7 years ago

Ok. Back from a business trip and should be home for a few weeks (yay!) and didn't feel like starting anything else tonight. So have been battling getting XCODE reinstalled (painful due to slow broadband in this lovely part of Wales....) as the version I had would not run on the latest version of OSX, which has been a huge struggle as my MAC is quite old and I think is struggling performance wise now. Maybe some extra mem would help (or a new MAC....)

Anyhow, I have got the latest code from Github (James has done the last few code updates), and am facing the usual problem when you have not looked at code for a year! I'm sure I will get back up to speed on how the heck it all works! :-)

Anyway, given the sudden interest (i.e. new problem reports), I am now ready to help look at this issue as best I can. Suggest we focus on this one before moving onto #22.

odbuser2 commented 7 years ago

Here's some code you can use as a quick fix in CoreMidiDeviceProvider.cpp. This grabs a flattened view of the model starting from the endpoint. The property that I've been harping on is "deviceName". That's the one that is changeable via AudioMIDI Setup. There are many more properties that should be passed back to the java side but I haven't shown them here.

I also modified CoreMidiDeviceInfo's constructor and properties on the java side to capture all of these variables and then built the equivalent object model of devices -> entities -> enpoints -> sources|destinations. I will share the full implementation when I have time but wanted to post this in case others need immediate direction.

  CFStringRef deviceName         = NULL;
  CFStringRef deviceManufacturer = NULL;
  CFStringRef deviceModel        = NULL;
  CFStringRef deviceOffline      = NULL;
  SInt32      deviceUniqueID;

  CFStringRef entityName         = NULL;
  SInt32      entityUniqueID;

  CFStringRef endpointName       = NULL;
  SInt32      endpointUniqueID;

  SInt32      driverVersion;

  MIDIObjectGetStringProperty(endPointReference,  kMIDIPropertyModel,         &deviceModel);
  MIDIObjectGetStringProperty(endPointReference,  kMIDIPropertyManufacturer,  &deviceManufacturer);
  MIDIObjectGetStringProperty(endPointReference,  kMIDIPropertyOffline,       &deviceOffline);

  MIDIObjectGetStringProperty(endPointReference,  kMIDIPropertyName,          &endpointName);
  MIDIObjectGetIntegerProperty(endPointReference, kMIDIPropertyUniqueID,      &endpointUniqueID);

  MIDIEntityRef entity = NULL;
  MIDIEndpointGetEntity(endPointReference, &entity);
  MIDIObjectGetStringProperty (entity, kMIDIPropertyName,     &entityName);
  MIDIObjectGetIntegerProperty(entity, kMIDIPropertyUniqueID, &entityUniqueID);

  MIDIDeviceRef device = NULL;
  MIDIEntityGetDevice         (entity, &device);
  MIDIObjectGetStringProperty (device, kMIDIPropertyName,     &deviceName);
  MIDIObjectGetIntegerProperty(device, kMIDIPropertyUniqueID, &deviceUniqueID);

  MIDIObjectGetIntegerProperty(endPointReference, kMIDIPropertyDriverVersion, &driverVersion);
DerekCook commented 7 years ago

Thanks, I've been mooching around the code tonight to re familiarise myself with the code. But if you are making a modification, I don't really want to replicate the effort, so I will wait and see what you come up with.

I am a little confused though, as I am using kMIDIPropertyName to get the name, or is it the issue that I am calling

MIDIObjectGetStringProperty(endPointReference, kMIDIPropertyName, &name);

I.e. I am getting the name of the end point reference, but from the code above you are also looking at the entity and the device?

Out of endpointName, entityName and deviceName, which one would you like to use as the name in the CoreMidiDeviceInfo?

I'll be honest I have never played around with Device Names in Audio/MIDI setup as I only have two, sometimes three, devices usually attached and they are all different device types, so it has never been an issue for me. If we can agree on the name that should be passed to the CoreMidiDeviceInfo constructor it should not be difficult.

The next question then is there any info that CoreMidiDeviceInfo ought to hold as its own fields in addition to what it passes to the MidiDevice.Info Is it information that you could make use of in the Java world?

odbuser2 commented 7 years ago

I am getting the name of the end point reference, but from the code above you are also looking at the entity and the device?

Yes. The following gets the endpoint name but it's not going to help much - especially if it's named "MIDI Out" or something like that. You can't tell what device it's on and you'll have no idea which input (if any) it corresponds to.

MIDIObjectGetStringProperty(endPointReference, kMIDIPropertyName, &name);

However the following retrieves the device name which is user definable via AudioMIDI Setup. Apple allowed that for the very reason that users with multiple devices couldn't tell them apart in most apps. This walks up the hierarchy from endpoint -> entity -> device:

MIDIEntityRef entity = NULL;
MIDIEndpointGetEntity(endPointReference, &entity);

MIDIDeviceRef device = NULL;
MIDIEntityGetDevice(entity, &device);
MIDIObjectGetStringProperty (device, kMIDIPropertyName,     &deviceName);

Java allows the following in the Info class: name vendor description version

I'm not sure what will make sense yet to cover a broad set of cases but it's probably something like: To make it useful and parse-able:

name={device.name}.{entity.name}.{endpoint.name}
vendor={device.manufacturer}
description={device.name}.{entity.name}.{endpoint.name}

But really, because of the incredible inconsistency from device to device, exposing everything available on the device, entity and endpoints via casting is the only way to go. However, we can make it flexible to the user and provide a sane default and also allow the user to choose the mapping that they prefer (this can be done on the Java side in the CoreMidiDeviceInfo class).

I have what I need for right now and will need some time in the future to share additional code. In the meantime, you'll probably want to at least add the fields I mentioned in the previous post via casting and change the Info.name field to "CoreMIDI4J - {device.name} - {endpoint.name}".

DerekCook commented 7 years ago

Thanks for the clarification, I guessed as much, but had not tried running the code (long night in work last night, and tonight is no better.....).

I'll experiment on the weekend with naming my interfaces to ensure that the Device Name is picked up.

TBH, I am not sure if your final suggestion is over complicating it. CoreMID4J is currently using the Model Name, as that is what the endpoint ref was returning for kMIDIPropertyName.

I would simply use the Device Name as obtained by your extra code in the super constructor for Midi.Info that CoreMidiDeviceInfo calls.

If the user does not change the Device Name (I never have) then they will get the default name, which is the Model Name, but if they change the name, then they get the name they have changed to, which is what I would expect.

We can extend CoreMidiDeviceInfo to include any information we wish it to hold if you need it, but of course that means the program being aware of CoreMidi4J.

odbuser2 commented 7 years ago

What is module name? There are names at each level in the hierarchy : device, entity, endpoint. Manufacturers do not name these consistently so unless you expose all of them via the Java Midi api (vendor, name, description) it won't be sufficient for users to determine what's what.

Personally, I will end up casting b/c Java's Midi api is inadequate and no combination of naming (unless fully parse-able) is going to fix that. I'll propose a solution via a fork of CoreMIDI4J and everyone can let me know what they think.

DerekCook commented 7 years ago

Sorry, I meant Model Name, not Module Name, now fixed above.

I intend to work on this later, so will see what I come up with.

DerekCook commented 7 years ago

A little later than intended (spent yesterday actually making music for a change, and was on a roll with some new ideas!)...

I have the build environment setup and working again. The version of XCODE I had when developing CoreMIDI4J is not compatible with latest OS X, so had to update that, and I decided to make the jump and use James's Maven build process to build the JAR file just to ensure consistency.

Anyhow, I am now at the point where (thanks to odbuser2's descriptions above) I can now get the Device Name (however you edit it) via CoreMIDI4J. Have edited the device name for my main interface a few times, and all the changes have been picked up on the Java side.

Need to think about what next, but probably that is refreshing my memory on devices, entities and endpoints! So I can think how they can usually be held in a CoreMidiDeviceInfo class and how best to build that.

Comments and suggestions welcome, I was going to upload the Jar here, but GITHUB won't let me and it's late now. Will see about posting it tomorrow.

DerekCook commented 7 years ago

Hi.

Reporting in again a little later than intended. Quite a lot going on in my life! But have been chipping away slowly at it.

OK. I thought quite a bit about this, including the original suggestion, which I can see the benefit of, but is rather a lot of work. For now I have gone for the "path of least resistance" approach, which gets me what I want, hopefully solves the non-unique names issue and is hopefully something which we can continue working on.

So what I have done is as follows:

Note on Version. I noticed that the version number is always zero (device, entity and end point). I don't seem to be able to get something else, but suspect it is because you need to get the driver opened first, but have ran out of time this week, and need to get back onto other things.

So attached below is my work in progress. Don't forget to unzip first!

coremidi4j-1.1-SNAPSHOT.jar.zip

PS: Kudos to James and his Maven process - I have been using that to build the JAR file whenever I make changes either on the Java or Native side, and it has been working faultlessly.

brunchboy commented 7 years ago

I am so glad it is working for you! I felt a bit guilty railroading your original build process to make it create a Maven artifact, but I needed to do that in order to use the library from my Clojure projects.

brunchboy commented 7 years ago

And I should say that sounds like excellent progress. Let me know if there is anything in particular that you would like me to poke at in testing, and I will try to find some time, although I will be away at a music festival this coming weekend.

DerekCook commented 7 years ago

Nothing specific in testing other than looking at the names being returned to see if they make sense for you. I have made no other functional changes.

brunchboy commented 7 years ago

Say, @odbuser2, could you take a look at the latest snapshot build I posted in the discussion for #19 and see if it meets your needs? It contains our fixes for disambiguation of device and port names, and for disconnecting/reconnecting devices that have been opened. If it meets your needs we would like to release it soon.

brunchboy commented 7 years ago

This is looking good, we are about to publish a release that fixes this and #19. @DerekCook, do you host the JavaDoc for this project anywhere? If so I would like to be able to link to the updated CoreMidiDeviceInfo class in the changes I am making to the read me. If not, perhaps I will throw it on Deep Symmetry in order to be able to do that. (But that refinement can wait, too, I don’t want to hold up the release of the much improved code while I sort that out.)

brunchboy commented 7 years ago

For now I am just linking to the class source in GitHub.

DerekCook commented 7 years ago

No, I don't host the JavaDoc anywhere. Can't we host them on GitHub? Never looked.

brunchboy commented 7 years ago

Unfortunately GitHub will not serve raw HTML from a repository. There are complicated workarounds you can dance through using third-party reflectors like rawgit, but I ended up deciding for my own projects it was easier to host them on my own web servers and link the documentation to those other servers.

BradleyRoss commented 7 years ago

See https://bradleyross.github.io/CoreMidi4J/. You can place the Javadoc, Doxygen documentation, and other HTML files in the gh-pages branch. The gh-pages branch is treated as a website that can be viewed http://xxx.github.io/yyy where xxx is the user name and yyy is the project name. In this case the project name is case-sensitive.

brunchboy commented 7 years ago

Yup, I did that for a while and decided it was a mess, because gh-pages is a branch, so there is no practical way to have the versions of the JavaDoc stay consistent with the branch/tag of the version of the source you are linking from. Now I just use my own server and scripts to change the links to point at the right paths when I do a release.