MIDI Output in Unity - c#

I'm trying to build simple software to connect to a MIDI Output device on Windows in Unity and send MIDI data.
As to avoid re-inventing the wheel, I started with the use of the C# Midi Toolkit on CodeProject built with support for .NET 2.0.
The issue I'm having is that it works fine in the Unity editor but then
fails in the standalone Windows build.
Here is the basic connection/play sound code:
// Log devices
int deviceCount = OutputDevice.DeviceCount;
for (int i = 0; i < deviceCount; i++)
{
Debug.Log(string.Format("Detected MIDI Device with ID {0}:{1}", i, OutputDevice.GetDeviceCapabilities(i).name));
}
deviceID = 1;
Debug.Log(string.Format("Connected to {0}", deviceID));
// Connect to device
device = new OutputDevice(deviceID);
// Play Middle C
device.Send(new ChannelMessage(ChannelCommand.NoteOn, 0, note, 127));
And in the standalone build I get the following exception:
OutputDeviceException: The specified device handle is invalid.
I looked through the source and noticed that the library is using
Win32 handles to winmm.dll, I figured this might have something to do with it but not certain where to go from here.
Can anyone provide any insight in how to approach this? I'll probably look at alternatives built specifically for Unity but I'm interested in learning why something like this wouldn't work in the first place.

I don't know if this kind of problem, but the x86 definition of midiOutOpen function that this old codeproject code uses (OutputDevice.cs) is
[DllImport("winmm.dll")]
50 private static extern int midiOutOpen(ref int handle, int deviceID,
51 MidiOutProc proc, int instance, int flags);
While on Pinvoke I can find this definition:
[DllImport("winmm.dll")]
static extern uint midiOutOpen(out IntPtr lphMidiOut, uint uDeviceID, IntPtr dwCallback, IntPtr dwInstance, uint dwFlags);
Maybe it is a platform problem.

You can take a look at how it's implemented in DryWetMIDI, for example: Output device.
Usage:
using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Core;
// ...
using (var outputDevice = OutputDevice.GetByName("Output device")) // or GetById(1)
{
outputDevice.SendEvent(new NoteOnEvent());
}

Related

DllNotFoundException when importing "winmm.dll" in WP8.1

I'm trying to import winmm.dll on a WP8.1 app to try and control device volume. Based on research from Google, I have created a Windows Runtime Component to wrap the actual function call, and then I call this from the main app. Since the issue is clearly in the wrapper, here's the code:
public sealed class VolumeControl
{
[DllImport("winmm.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
internal static extern int waveOutSetVolume(IntPtr uDeviceID, int dwVolume);
public static void Set(int volume)
{
// get volume as proportion of maximum
double newVolume = ushort.MaxValue * volume / 10.0;
// convert this into volume for two channels
uint v = ((uint)newVolume) & 0xffff;
uint vAll = v | (v << 16);
// set volume
waveOutSetVolume(IntPtr.Zero, (int)vAll);
}
I have also enabled unsafe code in the wrapper's project properties. DllImport is possible for native libraries in WP8.1, as far as I understand. I don't expect this app to pass certification on the Windows Store, but still I can't see why this code wouldn't work on a developer unlocked device.
Any idea if I've missed something here?
On Windows Mobile, all waveform audio function are implemented in 'coredll.dll'. Use this DLL instead of 'winmm.dll'.
The documentation has the answer:
Requirements
Minimum supported client
Windows 2000 Professional [desktop apps only]
In other words this function is not available from Windows Store app.
Turns out that WP8.1 has a WinMMBase.dll rather than plain old winmm.dll.
I found this by running a web server hack to browse the System32 folder (see xda-developers). After downloading the dll and inspecting it with DLL Export (http://www.nirsoft.net/utils/dll_export_viewer.html), I found that it does have the waveSetOutVolume function. The function itself doesn't seem to affect the volume though, but that wasn't the point of the question I guess :)

Accessing Fonts folder from console application with target framework 3.5

I need to get the path of a specific font in my c:/windows/ folder
The below code works perfectly when the target framework is 4.0
But my application can target only 3.5 and i need to use this in a console application c#
How can i achieve this ? Thanks.
string arialuniTff = path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Fonts), "arial.TTF");
Error Msg in 3.5 : 'System.Environment.SpecialFolder' does not contain a definition
The fonts folder is typically located at %windir%\Fonts, so you should be able to get the location like this:
Path.Combine(
System.Environment.GetEnvironmentVariable("windir"),
"Fonts");
It is a virtual folder, so in theory it could be located somewhere else. In practice, I've never seen that happen or heard of it happening. (Microsoft is confident enough in this location to reference it on their "how to install a font" page). I'm sure that if you're trying to locate a specific file name like that you have good error handling already, though.
Bonus information:
You might know this already, but if you need to know what classes, methods, etc. are available in a specific version of the .net framework, you can find out from MSDN. Go to the documentation page (say this one on Environment.SpecialFolder), and click on the ".NET Framework 4.5" link in the top left corner and choose a different version to see the page you are looking at as it was in that version.
Please refer How to get the path to CSIDL_COMMON_DOCUMENTS in .NET 3.5?
It provides the location for const int CSIDL_COMMON_DOCUMENTS = 0x002e;.
For Fonts folder, use const int CSIDL_FONTS = 0x0014;
It would be:
[DllImport("shell32.dll"), CharSet = CharSet.Auto]
static extern int SHGetFolderPath(IntPtr hwndOwner, int nFolder, IntPtr hToken, uint dwFlags, [Out] StringBuilder pszPath);
const int CSIDL_FONTS = 0x0014;
const int CSIDL_FLAG_CREATE = 0x8000;
StringBuilder sb = new StringBuilder();
int retVal = SHGetFolderPath(IntPtr.Zero,
CSIDL_FONTS | CSIDL_FLAG_CREATE,
IntPtr.Zero,
0,
sb);
Debug.Assert(retVal >= 0); // assert that the function call succeeded
String folderLocation = sb.ToString();

"No Disk" error using GDAL from C#/.NET

I am using Tamas Szekeres builds of GDAL including the C# bindings in a desktop GIS application using C# and .net 4.0
I am including the entire GDAL distribution in a sub-directory of my executable with the following folder structure:
\Plugins\GDAL
\Plugins\GDAL\gdal
\Plugins\GDAL\gdal-data
\Plugins\GDAL\proj
We are using EPSG:4326, and the software is built using 32-bit target since the GDAL C# API is using p/invoke to the 32-bit libraries (could try 64 bit since Tamas provides these, haven't gotten around to it yet).
When I run my application I get the following error
This error typically happens when software tries to access a device that is no longer attached, such as a removable drive. It is not possible to "catch" this exception because it pops up a system dialog.
After dismissing the dialog using any of the buttons, the software continues to execute as designed.
The error occurs the first time I call the following method
OSGeo.OSR.CoordinateTransformation.TransformPoint(double[] inout);
The strange stuff:
The error occurs on one, and only one computer (so far)
I've run this software in several other computers both 32 and 64 bit without problems
The error does not ocurr on the first run after compiling the GDAL shim library I am using, it only occurrs on each subsequent run
it happens regardless of release, or debug builds
it happens regardless of whether the debugger is attached or not
it happens regardless of whether I turn on or off Gdal.UseExceptions or Osr.UseExceptions();
disabling removable drives causes the bug to disappear. This is not what I consider a real solution as I will not be able to ask a customer to do this.
I have tried the following:
catching the error
changing GDAL directories and environment settings
changing computers and operating systems: this worked
used SysInternals ProcMon to trace what files are being opened with no luck, they all appear to be files that exist
I re-built the computer in question when the hard drive failed, to no avail.
"cleaning" the registry using CCleaner
files in GDAL Directory are unchanged on execution
Assumptions
Error is happening in unmanaged code
During GDAL initialization, some path is referring to a drive on the computer that is no longer attached.
I am also working on the assumption this is limited to a computer configuration error
Configuration
Windows 7 Pro
Intel Core i7 920 # 2,67GHz
12.0 GB RAM
64-bit OS
Drive C: 120 GB SSD with OS, development (Visual Studio 10), etc
Drive D: 1 TB WD 10,000k with data, not being accessed for data.
The Question
I either need a direction to trap the error, or a tool or technique that will allow me to figure out what is causing it. I don't want to release the software with the possibility that some systems will have this behaviour.
I have no experience with this library, but perhaps some fresh eyes might give you a brainwave...
Firstly, WELL WRITTEN QUESTION! Obviously this problem really has you stumped...
Your note about the error not occurring after a rebuild screams out: Does this library generate some kind of state file, in its binary directory, after it runs?
If so, it is possible that it is saving incorrect path information into that 'configuration' file, in a misguided attempt to accelerate its next start-up.
Perhaps scan this directory for changes between a 'fresh build' and 'first run'?
At very least you might find a file you can clean up on shut-down to avoid this alert...
HTH
Maybe you can try this:
Run diskmgmt.msc
Change the driveletter for Disk 2 (right click) if my assumption that Disk 2 is a Removable Disk is true
Run your application
If this removes the error, something in the application is referring to the old driveletter
It could be in the p/invoked libs
Maybe see: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=46501 It talks about gcc somehow compiling a driveletter into a binary
+1 Great question, but It is not possible to "catch"
Its one of these awful solutions that will turn up on DailyWTF in 5 years. But for now it is stored here http://www.pinvoke.net/default.aspx/user32.senddlgitemmessage
using Microsoft.VisualBasic; //this reference is for the Constants.vbNo;
public partial class Form1 : Form
{
[DllImport("user32.dll")]
static extern IntPtr SendDlgItemMessage(IntPtr hDlg, int nIDDlgItem, uint Msg, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetActiveWindow(IntPtr hWnd);
// For Windows Mobile, replace user32.dll with coredll.dll
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
// Find window by Caption only. Note you must pass IntPtr.Zero as the first parameter.
[DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetDlgItemText(IntPtr hDlg, int nIDDlgItem,[Out] StringBuilder lpString, int nMaxCount);
public void ClickSaveBoxNoButton()
{
//In this example, we've opened a Notepad instance, entered some text, and clicked the 'X' to close Notepad.
//Of course we received the 'Do you want to save...' message, and we left it sitting there. Now on to the code...
//
//Note: this example also uses API calls to FindWindow, GetDlgItemText, and SetActiveWindow.
// You'll have to find those separately.
//Find the dialog box (no need to find a "parent" first)
//classname is #32770 (dialog box), dialog box title is Notepad
IntPtr theDialogBoxHandle; // = null;
string theDialogBoxClassName = "#32770";
string theDialogBoxTitle = "Notepad";
int theDialogItemId = Convert.ToInt32("0xFFFF", 16);
StringBuilder theDialogTextHolder = new StringBuilder(1000);
//hardcoding capacity - represents maximum text length
string theDialogText = string.Empty;
string textToLookFor = "Do you want to save changes to Untitled?";
bool isChangeMessage = false;
IntPtr theNoButtonHandle; // = null;
int theNoButtonItemId = (int)Constants.vbNo;
//actual Item ID = 7
uint theClickMessage = Convert.ToUInt32("0x00F5", 16);
//= BM_CLICK value
uint wParam = 0;
uint lParam = 0;
//Get a dialog box described by the specified info
theDialogBoxHandle = FindWindow(theDialogBoxClassName, theDialogBoxTitle);
//a matching dialog box was found, so continue
if (theDialogBoxHandle != IntPtr.Zero)
{
//then get the text
GetDlgItemText(theDialogBoxHandle, theDialogItemId, theDialogTextHolder, theDialogTextHolder.Capacity);
theDialogText = theDialogTextHolder.ToString();
}
//Make sure it's the right dialog box, based on the text we got.
isChangeMessage = Regex.IsMatch(theDialogText, textToLookFor);
if ((isChangeMessage))
{
//Set the dialog box as the active window
SetActiveWindow(theDialogBoxHandle);
//And, click the No button
SendDlgItemMessage(theDialogBoxHandle, theNoButtonItemId, theClickMessage, (System.UIntPtr)wParam, (System.IntPtr)lParam);
}
}
It turns out there was no way to definitely answer this question.
I ended up "solving" the problem by figuring out that there was some hardware registered on the system that wasn't present. It is still a mystery to me why, after several years, only GDAL managed to provoke this bug.
I will put the inability to catch this exception down to the idiosyncrasies involved with p/invoke and the hardware error thrown at a very low level on the system.
You could add custom error handlers to gdal. This may help:
Link
http://trac.osgeo.org/gdal/ticket/2895

C#/.NET app doesn't recognize Environment Var Change (PATH)

In my C# app, I am programmatically installing an Oracle client if one is not present, which requires adding a dir to the PATH system environment variable. This all works fine, but it doesn't take effect until the user logs out/in to windows, or reboots. How can I get my app to recognize and use the new PATH var without this step? Even restarting my app would be better than requiring the user to log out/in.
Supposedly, broadcasting this change to other processes should work. Here's what I've tried, with no success:
using System.Runtime.InteropServices;
private const int HWND_BROADCAST = 0xffff;
private const int WM_WININICHANGE = 0x001a, WM_SETTINGCHANGE = WM_WININICHANGE, INI_INTL = 1;
[DllImport("user32.dll")]
private static extern int SendMessageTimeoutA(int hWnd, uint wMsg, uint wParam, string lParam, int fuFlags, int uTimeout, int lpdwResult);
int rtnVal = 0;
SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment", 2, 5000, rtnVal);
I've been told if you stop and restart the process in question, it should pick up these kinds of changes, but restarting my app doesn't do it. I suppose it could be an Oracle issue, that something about Oracle requires the login to recognize the change, I'm not sure. Thanks in advance.
Does Environment.GetEnvironmentVariable("MYVAR", EnvironmentVariableTarget.Machine) not work?
If my app is running elevated then I can
Environment.SetEnvironmentVariable("MYVAR", "cool", EnvironmentVariableTarget.Machine);
//do some other stuff...
Console.WriteLine(Environment.GetEnvironmentVariable("MYVAR", EnvironmentVariableTarget.Machine));
C:\TestApp>>TestApp.exe
cool
I don't know if this will work for other running processes but it should for your app doing the getting/setting
Your problem is only certain apps listen for that message (such as explorer) so it will not be used by your application at all. As the environment is generally inherited then restarting your app from within itself isn't going to help as it will get your current Environment block. If the user restarts from the start menu it will work (assuming the WM_SETTINGCHANGE has been broadcast).
You are best using Environment.GetEnvironmentVariable to read out the current value from the registry and merge it back into you current environment. Basically doing Environment.SetEnvironmentVariable("PATH", Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) + ";" + (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User)));
In our project we setup Oracle Instant Client with use of "install.bat" from Instant Client archive. For example:
install.bat odp.net1x %1 name

Cannot Load C++ DLL in C#

>My previous thread<
I created this one,because I installed WinXP on VMBox and I cannot get it working again.
This time I created an OnLoad Event on my form
if (LoadLibrary("blowfish.dll") == 0)
{
Misc.LogToFile("Could not load dll", true);
Application.Exit();
}
Runs fine on my PC,but on VMBox LoadLibrary returns 0.
Some users mentioned that the problem would be in mixing older NET Framework(2.0) with dlls made on newest MS Visual studio(2008 SP1) so I took action and now the program properties it's set to work with NET 3.5
On the VMBox I have NET 2.0,but this is not the problem - the program itself runs fine.I also have C++ Redistributable(2005,2005 SP1 and 2008).
What could be the problem?
To further trouble should you could call
Marshal.GetLastWin32Error();
which should give you an error code.
Is it possible that you deployed a debug version of your native dll which also requires a debug version of MSVCR90D.DLL? You should have distributed the release version because the debug version requires a different set of dlls to be present on the target system.
It obviously works on your development machine because all debug versions of the required libraries come with Visual Studio.
This is how you would get the message belonging to an error code:
[DllImport("kernel32.dll")]
private static extern int FormatMessage(int dwFlags,
IntPtr lpSource, int dwMessageId, int dwLanguageId,
out string lpBuffer, int nSize, IntPtr pArguments);
public static string GetErrorMessage(int errorCode)
{
const int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
const int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
const int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
string lpMsgBuf;
int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
int retVal = FormatMessage(dwFlags, IntPtr.Zero, errorCode, 0,
out lpMsgBuf, 0, IntPtr.Zero);
if (0 == retVal)
{
return null;
}
return lpMsgBuf;
}
Call GetLastError after LoadLibrary, check the error code value here: http://msdn.microsoft.com/en-us/library/ms681381.aspx
and see if that helps.
It could be that the dll's location is on the path in one environment and not in the other. It could also be permissions in one environment are not the same as the other.
Try running dependency walker on the DLL - see if any modules are missing.

Categories

Resources