loupiottes/DMX-2.0/MidiEventProvider.cs
2014-12-02 16:18:46 +00:00

744 lines
19 KiB
C#

/*
Copyright (C) Arnaud Houdelette 2012-2014
Copyright (C) Emmanuel Langlois 2012-2014
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Linq;
namespace DMX2
{
public class MidiEventProvider : IEventProvider, IDisposable
{
class internalEvent
{
readonly string internalName;
bool bound = false;
readonly uint page;
readonly int midiEvCode;
public bool Bound {
get {
return bound;
}
set {
bound = value;
}
}
public string InternalName {
get {
return internalName;
}
}
public uint Page {
get {
return page;
}
}
public int MidiEvCode {
get {
return midiEvCode;
}
}
public internalEvent (string _id, uint _page, int _evHCode)
{
internalName = _id;
page = _page;
midiEvCode = _evHCode;
}
int lastknownvalue = -1;
public int LastKnownValue {
get {
return lastknownvalue;
}
set {
lastknownvalue = value;
}
}
AlsaSeqLib.snd_seq_event_t storedevent;
public AlsaSeqLib.snd_seq_event_t StoredEvent {
get {
return storedevent;
}
set {
storedevent = value;
}
}
}
abstract class feedbackinfo : IFeedbackInfo
{
MidiEventProvider prov;
readonly internalEvent iev;
protected AlsaSeqLib.snd_seq_event_t ev;
public feedbackinfo (MidiEventProvider _prov, internalEvent _iev)
{
prov = _prov;
iev = _iev;
}
protected abstract bool UpdateEvent (byte data);
#region IFeedbackInfo implementation
bool IFeedbackInfo.FeedBack (byte data)
{
bool update = UpdateEvent (data);
iev.StoredEvent = ev;
iev.LastKnownValue = data;
if (!update)
return true;
if (prov.CurrentPage == iev.Page || iev.Page == 0) {
prov.SendEvent (ev);
foreach (int srcid in prov.feedbacksources) {
prov.lastValueOfSrc [CombineHash (srcid, iev.MidiEvCode)] = data;
}
}
return true;
}
#endregion
}
class ctrlfeedbackinfo : feedbackinfo
{
public ctrlfeedbackinfo (MidiEventProvider _prov, internalEvent _iev, byte channel, uint param)
:base(_prov,_iev)
{
ev = new AlsaSeqLib.snd_seq_event_t ();
ev.data_ev_ctrl.channel = channel;
ev.data_ev_ctrl.param = param;
ev.type = AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CONTROLLER;
}
protected override bool UpdateEvent (byte data)
{
int newvalue = (int)data * 127 / 255;
if (newvalue == ev.data_ev_ctrl.value)
return false;
ev.data_ev_ctrl.value = newvalue;
return true;
}
}
class pitchbendfeedbackinfo : feedbackinfo
{
public pitchbendfeedbackinfo (MidiEventProvider _prov, internalEvent _iev, byte channel)
:base(_prov,_iev)
{
ev = new AlsaSeqLib.snd_seq_event_t ();
ev.data_ev_ctrl.channel = channel;
ev.type = AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PITCHBEND;
}
protected override bool UpdateEvent (byte data)
{
int newvalue = (int)data * 14000 / 255 - 7000;
if (newvalue == ev.data_ev_ctrl.value)
return false;
ev.data_ev_ctrl.value = newvalue;
return true;
}
}
public class MidiDev
{
string name;
public string Name { get { return name; } }
public bool HasFeedback { get; set; }
readonly List<int> connected = new List<int> ();
public List<int> ConnectedPorts {
get{ return connected;}
}
public MidiDev (string _name)
{
name = _name;
}
}
/// <summary>
/// Etat interne des evenements midi paginés.
/// </summary>
readonly Dictionary<string,internalEvent> eventlist = new Dictionary<string, internalEvent> ();
/// <summary>
/// Liste des peripheriques connus (presents ou non)
/// </summary>
readonly Dictionary<string,MidiDev> knowndevices = new Dictionary<string,MidiDev> ();
/// <summary>
/// Liste des ports connectés avec feedback
/// </summary>
readonly List<int> feedbacksources = new List<int> ();
//static readonly Dictionary<int,MidiDev> srcidToDev = new Dictionary<int, MidiDev>();
readonly List<byte> unpaginatedchannels = new List<byte> ();
/// <summary>
/// Derniere valeur connue pour une evenement sur source donnée :
/// Soit recue, soit envoyée (feedback / changement de page)
/// </summary>
readonly Dictionary<int,byte> lastValueOfSrc = new Dictionary<int, byte> ();
EventData last;
internalEvent levent = null;
bool connected = false;
bool guirefreshflag = false;
uint page = 1;
uint maxpage = 8;
public uint CurrentPage {
get {
return page;
}
set {
if (value < 1 || value > maxpage)
return;
page = value;
Refresh ();
}
}
public uint Maxpage {
get {
return maxpage;
}
set {
maxpage = value;
}
}
public List<byte> UnpaginatedChannels {
get {
return unpaginatedchannels;
}
}
public bool GuiRefreshFlag {
get {
if (guirefreshflag) {
guirefreshflag = false;
return true;
}
return false;
}
}
public void ConnectDevice (string name)
{
knowndevices.Add (name, new MidiDev (name));
AutoConnect ();
}
public void RefreshFeedback (string name)
{
foreach (int port in knowndevices[name].ConnectedPorts) {
if (knowndevices [name].HasFeedback) {
if (!feedbacksources.Contains (port))
feedbacksources.Add (port);
} else {
if (feedbacksources.Contains (port))
feedbacksources.Remove (port);
}
}
}
public void DisconnectDevice (MidiEventProvider.MidiDev dev)
{
if (!knowndevices.ContainsKey (dev.Name))
return;
knowndevices.Remove (dev.Name);
foreach (int connectedport in dev.ConnectedPorts) {
int client = connectedport >> 8;
int port = connectedport & 0xFF;
AlsaSeqLib.Deconnecte (client, port);
}
}
public bool IsKnownDevice (string name)
{
return knowndevices.ContainsKey (name);
}
public IEnumerable<MidiDev> KnownDevices {
get {
return knowndevices.Values;
}
}
public MidiEventProvider (EventManager manager)
{
/*MidiDev dev = new MidiDev("VMPK Input:VMPK Input");
dev.HasFeedback = true;
knowndevices.Add(dev.Name,dev);
dev = new MidiDev("VMPK Output:VMPK Output");
dev.HasFeedback = true;
knowndevices.Add(dev.Name,dev);*/
manager.RegisterProvider (this);
AlsaSeqLib.Init ();
AlsaSeqLib.ConnectFrom (AlsaSeqLib.SND_SEQ_CLIENT_SYSTEM, AlsaSeqLib.SND_SEQ_PORT_SYSTEM_ANNOUNCE);
AutoConnect ();
}
void AutoConnect ()
{
foreach (var cli in AlsaSeqLib.EnumClients()) {
foreach (var p in cli.Ports) {
PortDetected (cli, p);
}
}
}
void PortDetected (AlsaSeqLib.Client cli, AlsaSeqLib.Port p)
{
// Execute a chaque 'apparition' d'un port midi
// teste si connection auto au port et connecte si besoin
guirefreshflag = true;
string fullportname = cli.Name + ':' + p.Name;
if (knowndevices.ContainsKey (fullportname)) {
int srcid = p.ClientId << 8 + p.PortId;
if (knowndevices [fullportname].ConnectedPorts.Contains (srcid))
return;
AlsaSeqLib.Connect (p);
}
}
void PortConnect (AlsaSeqLib.snd_seq_connect_t cn, bool connect)
{
int clientId, portId;
if (cn.dest.client == AlsaSeqLib.ClientId) {
clientId = cn.sender.client;
portId = cn.sender.port;
} else {
clientId = cn.dest.client;
portId = cn.dest.port;
}
int srcid = clientId << 8 + portId;
if (connect) {
string fpname = AlsaSeqLib.GetClientByID (clientId).Name + ":" + AlsaSeqLib.GetPortByIDs (clientId, portId).Name;
if (!knowndevices.ContainsKey (fpname))
return;
if (knowndevices [fpname].ConnectedPorts.Contains (srcid))
return;
knowndevices [fpname].ConnectedPorts.Add (srcid);
if (knowndevices [fpname].HasFeedback)
feedbacksources.Add (srcid);
guirefreshflag = true;
//srcidToDev[srcid] = knowndevices [fpname];
return;
}
foreach (var dev in knowndevices.Values) {
if (dev.ConnectedPorts.Contains (srcid)) {
/*if(srcidToDev.ContainsKey(srcid))
srcidToDev.Remove(srcid);*/
dev.ConnectedPorts.Remove (srcid);
guirefreshflag = true;
return;
}
}
}
static int CombineHash (int hash1, int hash2)
{
unchecked {
return hash1 * 33 + hash2;
}
}
protected bool HasFeedback (int source)
{
return feedbacksources.Contains (source);
}
public void SendEvent (AlsaSeqLib.snd_seq_event_t ev)
{
AlsaSeqLib.SendEventToSubscribers (ev);
}
public void Refresh ()
{
foreach (var ievent in eventlist.Values) {
if (ievent.Page == page) {
SendEvent (ievent.StoredEvent);
foreach (int src in feedbacksources) {
int lnvk = CombineHash (src, ievent.MidiEvCode);
if (ievent.LastKnownValue != -1)
lastValueOfSrc [lnvk] = (byte)ievent.LastKnownValue;
}
}
}
}
#region IEventProvider implementation
static System.Text.RegularExpressions.Regex regexEventID = new System.Text.RegularExpressions.Regex (
@"MIDI-PAGE(?<page>\d+)-(?<id>.+)",
System.Text.RegularExpressions.RegexOptions.Compiled);
bool IEventProvider.Bind (string eventId)
{
// On indique a l'EventManager qu'on traite, si l'ID commence par 'MIDI-'
if (! eventId.StartsWith ("MIDI-"))
return false;
if (! eventlist.ContainsKey (eventId)) {
var res = regexEventID.Match (eventId);
if (!res.Success)
return false;
uint _page = uint.Parse (res.Groups ["page"].Value);
int _evHC = res.Groups ["id"].Value.GetHashCode ();
eventlist.Add (eventId, new internalEvent (eventId, _page, _evHC));
}
eventlist [eventId].Bound = true;
return true;
}
void IEventProvider.Unbind (string eventId)
{
if (! eventlist.ContainsKey (eventId))
return;
eventlist [eventId].Bound = false;
return;
}
Gtk.Menu IEventProvider.GetProviderSubMenu (EventManager.EventMenuData state, Gtk.ButtonPressEventHandler handler)
{
if (!connected)
return null; // Si pas encore recu d'evenements => pas de menu
Gtk.Menu retmenu = new Gtk.Menu ();
if (levent != null) { // Creation du sous menu "Dernier"
/*Gtk.MenuItem lmenuitem = new Gtk.MenuItem ("Dernier");
retmenu.Add (lmenuitem);
Gtk.Menu lmenu = new Gtk.Menu ();
lmenuitem.Submenu = lmenu;*/
Gtk.MenuItem item = new Gtk.MenuItem (GetDescription (levent.InternalName));
item.Data [EventManager.EventIdKey] = levent.InternalName;
item.Data [EventManager.StateKey] = state;
item.ButtonPressEvent += handler;
retmenu.Add (item);
}
Gtk.MenuItem evmenuitem = new Gtk.MenuItem ("Events"); // Creation du sous menu "Events"
retmenu.Add (evmenuitem);
Gtk.Menu evmenu = new Gtk.Menu ();
evmenuitem.Submenu = evmenu;
List<string> sortedKeys = eventlist.Keys.ToList (); // On recupere des IDs
sortedKeys.Sort (); // et on les trie
foreach (string key in sortedKeys) {
internalEvent evt = eventlist [key];
Gtk.MenuItem item = new Gtk.MenuItem (GetDescription (evt.InternalName));
item.Data [EventManager.EventIdKey] = evt.InternalName;
item.Data [EventManager.StateKey] = state;
item.ButtonPressEvent += handler;
evmenu.Add (item);
}
return retmenu;
}
void IEventProvider.ProcessEvents (EventManagerCallback callback)
{
AlsaSeqLib.snd_seq_event_t evS;
uint evpage;
// Tant qu'il y des evenements midi en attente
while (AlsaSeqLib.GetEvent(out evS)) {
string id = null;
int value = 0;
byte channel = 255;
switch (evS.type) {
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_SUBSCRIBED: // Connection d'un périph midi
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
PortConnect (
evS.data_connect,
evS.type == AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_SUBSCRIBED
);
continue;
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CONTROLLER:
if (evS.data_ev_ctrl.param == 127 && evS.data_ev_ctrl.value > 0) {
CurrentPage++;
continue;
}
if (evS.data_ev_ctrl.param == 126 && evS.data_ev_ctrl.value > 0) {
CurrentPage--;
continue;
}
id = string.Format ("CTRL-C{0}P{1}", evS.data_ev_ctrl.channel, evS.data_ev_ctrl.param);
channel = evS.data_ev_ctrl.channel;
value = 255 * evS.data_ev_ctrl.value / 127; // Conversion {0,127} => {0,255}
break;
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_NOTEON:
id = string.Format ("NOTE-C{0}N{1}", evS.data_ev_note.channel, evS.data_ev_note.note);
channel = evS.data_ev_note.channel;
value = 255;
break;
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_NOTEOFF:
id = string.Format ("NOTE-C{0}N{1}", evS.data_ev_note.channel, evS.data_ev_note.note);
channel = evS.data_ev_note.channel;
value = 0;
break;
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PITCHBEND:
id = string.Format ("PB-C{0}", evS.data_ev_ctrl.channel);
channel = evS.data_ev_ctrl.channel;
value = ((evS.data_ev_ctrl.value + 7000) * 255 / 14000);
if (value < 0)
value = 0;
if (value > 255)
value = 255;
break;
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CLOCK:
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_SENSING:
continue;
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PGMCHANGE:
CurrentPage = (uint)evS.data_ev_ctrl.value;
continue;
case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_START:
PortDetected (
AlsaSeqLib.GetClientByID (evS.data_addr.client),
AlsaSeqLib.GetPortByIDs (evS.data_addr.client, evS.data_addr.port)
);
continue;
default:
id = null;
#if DEBUG
Console.WriteLine(string.Format ("event {0}", evS.type) );
Info.Publish(string.Format ("event {0}", evS.type) ); // On affiche les evenements inconnus
#endif
continue;
}
connected = true;
if (id != null) {
// Hashcode de l'ev Midi, non pagine
int evHC = id.GetHashCode ();
int srcid = evS.source.client << 8 + evS.source.port;
int lnvk = CombineHash (srcid, evHC);
if (channel == 255 || unpaginatedchannels.Contains (channel))
evpage = 0;
else
evpage = page;
// Construction de l'ID evenement
id = string.Format ("MIDI-PAGE{0}-{1}", evpage, id);
// Creation de l'objet interne si innexistant
if (!eventlist.ContainsKey (id))
eventlist.Add (id, new internalEvent (id, page, evHC));
levent = eventlist [id]; //Dernier Evenement recu conserve pour menu
if (!lastValueOfSrc.ContainsKey (lnvk))
lastValueOfSrc [lnvk] = (byte)value;
EventData evData = new EventData ();
evData.id = id;
evData.value = (byte)value;
evData.prev_value = lastValueOfSrc [lnvk];
/*if (evData.Equals (last))
continue; */
last = evData;
if (eventlist [id].Bound) {
callback (evData);
}
lastValueOfSrc [lnvk] = (byte)value;
eventlist [id].StoredEvent = evS;
eventlist [id].LastKnownValue = (byte)value;
}
}
}
string IEventProvider.MenuName {
get {
return "Midi";
}
}
static System.Text.RegularExpressions.Regex regexCtrlEventID = new System.Text.RegularExpressions.Regex (
@"MIDI-PAGE(?<page>\d+)-CTRL-C(?<chan>\d+)P(?<param>\d+)",
System.Text.RegularExpressions.RegexOptions.Compiled);
static System.Text.RegularExpressions.Regex regexPbEventID = new System.Text.RegularExpressions.Regex (
@"MIDI-PAGE(?<page>\d+)-PB-C(?<chan>\d+)",
System.Text.RegularExpressions.RegexOptions.Compiled);
static System.Text.RegularExpressions.Regex regexNoteEventID = new System.Text.RegularExpressions.Regex (
@"MIDI-PAGE(?<page>\d+)-NOTE-C(?<chan>\d+)N(?<note>\d+)",
System.Text.RegularExpressions.RegexOptions.Compiled);
string GetDescription (string eventId)
{
if (!eventlist.ContainsKey (eventId))
return null;
var res = regexCtrlEventID.Match (eventId);
if (res.Success) {
uint page = uint.Parse (res.Groups ["page"].Value);
byte chan = byte.Parse (res.Groups ["chan"].Value);
uint param = uint.Parse (res.Groups ["param"].Value);
return string.Format ("Page {2} => Control-Change C({0}) Param-{1}", chan + 1, param, page);
}
res = regexPbEventID.Match (eventId);
if (res.Success) {
uint page = uint.Parse (res.Groups ["page"].Value);
byte chan = byte.Parse (res.Groups ["chan"].Value);
return string.Format ("Page {1} => PitchBend C({0})", chan + 1, page);
}
res = regexNoteEventID.Match (eventId);
if (res.Success) {
uint page = uint.Parse (res.Groups ["page"].Value);
byte chan = byte.Parse (res.Groups ["chan"].Value);
byte note = byte.Parse (res.Groups ["note"].Value);
return string.Format ("Page {2} => Note C({0}) Note-{1}", chan + 1, note, page);
}
return eventId;
}
IFeedbackInfo IEventProvider.GetFeedbackInfo (string eventId)
{
if (!eventlist.ContainsKey (eventId))
return null;
var res = regexCtrlEventID.Match (eventId);
if (res.Success) {
byte chan = byte.Parse (res.Groups ["chan"].Value);
uint param = uint.Parse (res.Groups ["param"].Value);
return new ctrlfeedbackinfo (this, eventlist [eventId], chan, param);
}
res = regexPbEventID.Match (eventId);
if (res.Success) {
byte chan = byte.Parse (res.Groups ["chan"].Value);
return new pitchbendfeedbackinfo (this, eventlist [eventId], chan);
}
return null;
}
#endregion
#region IDisposable implementation
bool disposed = false;
~MidiEventProvider ()
{
Dispose ();
}
public void Dispose ()
{
if (disposed)
return;
disposed = true;
AlsaSeqLib.Close ();
}
#endregion
public void Save (System.Xml.XmlElement parent)
{
System.Xml.XmlElement el = parent.OwnerDocument.CreateElement ("Midi");
parent.AppendChild (el);
el.SetAttribute ("maxpage", maxpage.ToString ());
System.Xml.XmlElement xmlEl;
foreach (MidiDev dev in knowndevices.Values) {
el.AppendChild (xmlEl = parent.OwnerDocument.CreateElement ("MidiDev"));
xmlEl.SetAttribute ("name", dev.Name);
xmlEl.SetAttribute ("feedback", dev.HasFeedback.ToString ());
}
foreach (byte ch in unpaginatedchannels) {
el.AppendChild (xmlEl = parent.OwnerDocument.CreateElement ("UPC"));
xmlEl.SetAttribute ("ch", ch.ToString ());
}
}
public void Load (System.Xml.XmlElement el)
{
if (el == null)
return;
maxpage = uint.Parse (el.TryGetAttribute ("maxpage", "8"));
foreach (var xd in el.GetElementsByTagName("MidiDev")) {
System.Xml.XmlElement xdev = xd as System.Xml.XmlElement;
string name = xdev.GetAttribute ("name");
if (!knowndevices.ContainsKey (name))
knowndevices.Add (name, new MidiDev (name));
knowndevices [name].HasFeedback = bool.Parse (xdev.TryGetAttribute ("feedback", "false"));
}
unpaginatedchannels.Clear ();
foreach (var xu in el.GetElementsByTagName("UPC")) {
System.Xml.XmlElement xupc = xu as System.Xml.XmlElement;
unpaginatedchannels.Add (byte.Parse (xupc.GetAttribute ("ch")));
}
AutoConnect ();
}
}
}