/* 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 . */ 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 connected = new List (); public List ConnectedPorts { get{ return connected;} } public MidiDev (string _name) { name = _name; } } /// /// Etat interne des evenements midi paginés. /// readonly Dictionary eventlist = new Dictionary (); /// /// Liste des peripheriques connus (presents ou non) /// readonly Dictionary knowndevices = new Dictionary (); /// /// Liste des ports connectés avec feedback /// readonly List feedbacksources = new List (); //static readonly Dictionary srcidToDev = new Dictionary(); readonly List unpaginatedchannels = new List (); /// /// Derniere valeur connue pour une evenement sur source donnée : /// Soit recue, soit envoyée (feedback / changement de page) /// readonly Dictionary lastValueOfSrc = new Dictionary (); 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 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); // TODO: Deconnecter ici 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 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(?\d+)-(?.+)", 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 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; //TODO : Regarder si d'autres controles interessants. 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"; } } //TODO gerer pages et feeddback static System.Text.RegularExpressions.Regex regexCtrlEventID = new System.Text.RegularExpressions.Regex ( @"MIDI-PAGE(?\d+)-CTRL-C(?\d+)P(?\d+)", System.Text.RegularExpressions.RegexOptions.Compiled); static System.Text.RegularExpressions.Regex regexPbEventID = new System.Text.RegularExpressions.Regex ( @"MIDI-PAGE(?\d+)-PB-C(?\d+)", System.Text.RegularExpressions.RegexOptions.Compiled); static System.Text.RegularExpressions.Regex regexNoteEventID = new System.Text.RegularExpressions.Regex ( @"MIDI-PAGE(?\d+)-NOTE-C(?\d+)N(?\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 (); } } }