/* 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; using System.Text.RegularExpressions; namespace DMX2 { public class MidiEventProvider : IEventProvider, IDisposable { /// /// 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; internalEventDesc levent = null; bool guirefreshflag = false; uint page = 1; uint maxpage = 8; protected bool use14b = false; protected int max14bValue = 100; byte[] fbTmpData = new byte[512]; uint pageUpCC = 127; uint pageDownCC = 126; AlsaSeqLib.MidiPort midiport; public uint CurrentPage { get { return page; } set { if (value < 1 || value > maxpage) return; page = value; Refresh(); } } public uint PageUpCC { get { return pageUpCC; } set { pageUpCC = value; } } public uint PageDownCC { get { return pageDownCC; } set { pageDownCC = value; } } public uint Maxpage { get { return maxpage; } set { maxpage = value; } } public List UnpaginatedChannels { get { return unpaginatedchannels; } } public bool Use14bCC { get { return use14b; } set { use14b = value; } } public int Max14bValue { get { return max14bValue; } set { max14bValue = value; } } public bool GuiRefreshFlag { get { if (guirefreshflag) { guirefreshflag = false; return true; } return false; } } public void ConnectDevice(string name) { knowndevices.Add(name, new MidiControler(name)); AutoConnect(); } public void RefreshFeedback(string name) { foreach (AlsaSeqLib.Port 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.MidiControler dev) { if (!knowndevices.ContainsKey(dev.Name)) return; knowndevices.Remove(dev.Name); foreach (AlsaSeqLib.Port connectedport in dev.ConnectedPorts) { midiport.Deconnecte(connectedport); } } public bool IsKnownDevice(string name) { return knowndevices.ContainsKey(name); } public IEnumerable KnownDevices { get { return knowndevices.Values; } } public MidiEventProvider(EventManager manager) { #if DEBUG MidiControler dev = new MidiControler("VMPK Input:VMPK Input"); dev.HasFeedback = true; knowndevices.Add(dev.Name, dev); dev = new MidiControler("VMPK Output:VMPK Output"); dev.HasFeedback = true; knowndevices.Add(dev.Name, dev); #endif manager.RegisterProvider(this); //AlsaSeqLib.Init (); midiport = new AlsaSeqLib.MidiPort("event_prov_in_out"); //midiport.ConnectFrom(AlsaSeqLib.SND_SEQ_CLIENT_SYSTEM, AlsaSeqLib.SND_SEQ_PORT_SYSTEM_ANNOUNCE); AlsaSeqLib.Port systemport = AlsaSeqLib.GetPortByIDs(AlsaSeqLib.SND_SEQ_CLIENT_SYSTEM, AlsaSeqLib.SND_SEQ_PORT_SYSTEM_ANNOUNCE); midiport.ConnectFrom(systemport); 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(p)) return; midiport.ConnectFrom(p); midiport.ConnectTo(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; } AlsaSeqLib.Port p = AlsaSeqLib.GetPortByIDs(clientId, portId); if (connect) { AlsaSeqLib.Client c = AlsaSeqLib.GetClientByID(clientId); string fpname = c.Name + ":" + p.Name; if (!knowndevices.ContainsKey(fpname)) return; if (knowndevices[fpname].ConnectedPorts.Contains(p)) return; knowndevices[fpname].ConnectedPorts.Add(p); if (knowndevices[fpname].HasFeedback) feedbacksources.Add(p); guirefreshflag = true; //srcidToDev[srcid] = knowndevices [fpname]; return; } foreach (var dev in knowndevices.Values) { if (dev.ConnectedPorts.Contains(p)) { dev.ConnectedPorts.Remove(p); guirefreshflag = true; return; } } } static int CombineHash(int hash1, int hash2) { unchecked { return hash1 * 33 + hash2; } } protected bool HasFeedback(AlsaSeqLib.Port source) { return feedbacksources.Contains(source); } public void SendEvent(AlsaSeqLib.snd_seq_event_t ev) { midiport.SendEvent(ev); } public void Refresh() { foreach (var ievent in eventlist.Values) { if (ievent.Page == page) { ievent.SendFeedback(); foreach (AlsaSeqLib.Port src in feedbacksources) { int lnvk = CombineHash(src.SrcId, ievent.MidiEvCode); if (ievent.LastKnownValue != -1) lastValueOfSrc[lnvk] = (byte)ievent.LastKnownValue; } } } } #region IEventProvider implementation 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)) { Match res = regexEventID.Match(eventId); if (!res.Success) return false; uint _page = uint.Parse(res.Groups["page"].Value); int _evHC = res.Groups["id"].Value.GetHashCode(); midiFeedbackSender sender = null; res = regexCtrlEventID.Match(eventId); if (res.Success) { byte chan = byte.Parse(res.Groups["chan"].Value); uint param = uint.Parse(res.Groups["param"].Value); sender = new midiCCFbSender(this, chan, param); } else if ((res = regexPbEventID.Match(eventId)).Success) { byte chan = byte.Parse(res.Groups["chan"].Value); sender = new midiPBFbSender(this, chan); } eventlist.Add(eventId, new internalEventDesc(eventId, _page, _evHC, sender)); } 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) { 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) { internalEventDesc 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 (midiport.GetEvent(out evS)) { Console.WriteLine(string.Format("event {0}", evS.type)); 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 == pageUpCC && evS.data_ev_ctrl.value > 0) { CurrentPage++; continue; } if (evS.data_ev_ctrl.param == pageDownCC && evS.data_ev_ctrl.value > 0) { CurrentPage--; continue; } channel = evS.data_ev_ctrl.channel; if (use14b && evS.data_ev_ctrl.param < 64) { long msbAddr; if (evS.data_ev_ctrl.param < 32) { msbAddr = evS.data_ev_ctrl.channel * 32 + evS.data_ev_ctrl.param; fbTmpData[msbAddr] = (byte)evS.data_ev_ctrl.value; continue; } evS.data_ev_ctrl.param -= 32; msbAddr = evS.data_ev_ctrl.channel * 32 + evS.data_ev_ctrl.param; value = ((fbTmpData[msbAddr] << 7) ^ evS.data_ev_ctrl.value); value = 255 * value / max14bValue; if (value > 255) value = 255; } else { value = 255 * evS.data_ev_ctrl.value / 127; // Conversion {0,127} => {0,255} } id = string.Format("CTRL-C{0}P{1}", evS.data_ev_ctrl.channel, evS.data_ev_ctrl.param); 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_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; /*case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CLOCK: case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_SENSING: 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; } 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)) { switch (evS.type) { // TODO : Pitchbend feedback case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CONTROLLER: eventlist.Add(id, new internalEventDesc(id, evpage, evHC, new midiCCFbSender(this, channel, evS.data_ev_ctrl.param)) ); break; case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PITCHBEND: eventlist.Add(id, new internalEventDesc(id, evpage, evHC, new midiPBFbSender(this, channel)) ); break; default: eventlist.Add(id, new internalEventDesc(id, evpage, evHC, null)); break; } } levent = eventlist[id]; //Dernier Evenement recu conserve pour menu if (!lastValueOfSrc.ContainsKey(lnvk)) { lastValueOfSrc[lnvk] = (byte)value; } else if (lastValueOfSrc[lnvk] == (byte)value) continue; EventData evData = new EventData(); evData.id = id; evData.value = (byte)value; evData.prev_value = lastValueOfSrc[lnvk]; /*if (evData.Equals (last)) continue; */ //last = evData; eventlist[id].CallEvent(callback, evData); /*if (eventlist [id].Bound) { callback (evData); }*/ lastValueOfSrc[lnvk] = (byte)value; eventlist[id].LastKnownValue = (byte)value; } } } string IEventProvider.MenuName { get { return "Midi"; } } static System.Text.RegularExpressions.Regex regexEventID = new System.Text.RegularExpressions.Regex( @"MIDI-PAGE(?\d+)-(?.+)", System.Text.RegularExpressions.RegexOptions.Compiled); 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; return new midifeedbackinfo(this, eventlist[eventId]); } #endregion #region IDisposable implementation bool disposed = false; ~MidiEventProvider() { Dispose(); } public void Dispose() { if (disposed) return; disposed = true; //AlsaSeqLib.Close(); midiport.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()); el.SetAttribute("pageUpCC", pageUpCC.ToString()); el.SetAttribute("pageDownCC", pageDownCC.ToString()); el.SetAttribute("fourteenbits", use14b ? "true" : "false"); el.SetAttribute("max14b", max14bValue.ToString()); System.Xml.XmlElement xmlEl; foreach (MidiControler 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")); pageUpCC = uint.Parse(el.TryGetAttribute("pageUpCC", "127")); pageDownCC = uint.Parse(el.TryGetAttribute("pageDownCC", "126")); use14b = el.TryGetAttribute("fourteenbits", string.Empty).Equals("true"); max14bValue = int.Parse(el.TryGetAttribute("max14b", "255")); 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 MidiControler(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(); } class internalEventDesc { readonly string internalName; bool bound = false; readonly uint page; readonly int midiEvCode; readonly midiFeedbackSender fbSender; 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 internalEventDesc(string _id, uint _page, int _evHCode, midiFeedbackSender _evsender) { internalName = _id; page = _page; midiEvCode = _evHCode; fbSender = _evsender; } int lastknownvalue = -1; public int LastKnownValue { get { return lastknownvalue; } set { lastknownvalue = value; } } bool nofbflag = false; public void SendFeedback() { if (fbSender != null && !nofbflag) fbSender.SendFeedback(lastknownvalue); } public void CallEvent(EventManagerCallback callback, EventData evData) { nofbflag = true; callback(evData); nofbflag = false; } } abstract class midiFeedbackSender { public abstract void SendFeedback(int value); } class midiCCFbSender : midiFeedbackSender { readonly MidiEventProvider prov; AlsaSeqLib.snd_seq_event_t ev; AlsaSeqLib.snd_seq_event_t ev2; public midiCCFbSender(MidiEventProvider _prov, byte _chan, uint _param) { prov = _prov; ev = new AlsaSeqLib.snd_seq_event_t(); ev.type = AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CONTROLLER; ev.data_ev_ctrl.channel = _chan; ev.data_ev_ctrl.param = _param; ev2 = ev; ev2.data_ev_ctrl.param += 32; } public override void SendFeedback(int value) { if (prov.use14b && ev.data_ev_ctrl.param < 32) { value = value * prov.max14bValue / 255; ev.data_ev_ctrl.value = value >> 7; ev2.data_ev_ctrl.value = value & 0xFF; prov.SendEvent(ev); prov.SendEvent(ev2); } else { ev.data_ev_ctrl.value = value * 127 / 255; prov.SendEvent(ev); } } } class midiPBFbSender : midiFeedbackSender { readonly MidiEventProvider prov; AlsaSeqLib.snd_seq_event_t ev; public midiPBFbSender(MidiEventProvider _prov, byte _chan) { prov = _prov; ev = new AlsaSeqLib.snd_seq_event_t(); ev.type = AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PITCHBEND; ev.data_ev_ctrl.channel = _chan; } public override void SendFeedback(int value) { // value = ((evS.data_ev_ctrl.value + 7000) * 255 / 14000); ev.data_ev_ctrl.value = value * 14000 / 255 - 7000; prov.SendEvent(ev); } } class midifeedbackinfo : IFeedbackInfo { MidiEventProvider prov; readonly internalEventDesc iev; public midifeedbackinfo(MidiEventProvider _prov, internalEventDesc _iev) { prov = _prov; iev = _iev; } #region IFeedbackInfo implementation bool IFeedbackInfo.FeedBack(byte data) { iev.LastKnownValue = data; if (prov.CurrentPage == iev.Page || iev.Page == 0) { iev.SendFeedback(); foreach (AlsaSeqLib.Port src in prov.feedbacksources) { int lnvk = CombineHash(src.SrcId, iev.MidiEvCode); if (iev.LastKnownValue != -1) prov.lastValueOfSrc[lnvk] = (byte)iev.LastKnownValue; } } return true; } #endregion } public class MidiControler { 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 MidiControler(string _name) { name = _name; } } } }