内容


利用发布/订阅消息传递机制分发 MIDI 命令

Comments

概述

本文描述了如何在不同的应用程序间利用发布/订阅消息传递机制分发 MIDI 命令。本文首先简要描述了 MIDI 的定义和 Java™ 是如何支持 MIDI 的。 接下来介绍发布/订阅消息传递以及如何利用它将任意数量的应用程序连接在一起。文章最后描述了三个程序实例,MIDI 文件播放器,MIDI 输入和 MIDI 输出。

什么是 MIDI?

Musical Instrument Digital Interface (MIDI) 最早由 MIDI Manufacturers Association (MMA) 于 1983 年发布,此后 MMA 公司又发布了附加规范和增强版本。MIDI 不同于 WAV 或者 MP3 之类数字音频,它是一组用于控制例如合成器这样的电子音乐设备命令。音乐家可以使用 MIDI 控制器进行演奏,例如键盘,但是键盘产生的 MIDI 命令可以用来控制一个或更多其它的设备。一种称为音序器的设备可以自动控制这些设备, 这样就可以在现场演出期间对预先录制的回放程序进行准备和修改。

例如 Cakewalk 或 Sibelius 这些应用程序允许作曲家使用图形化的界面来创作音乐,然后可以通过软件生成的 MIDI 命令来播放这些音乐。MIDI 包含三种命令。一些简短的命令可以用于直接控制 MIDI 设备。 需要增加 Meta 命令来支持 MIDI 文件,并提供一些系统高级命令,增加厂商的特性,这些特性由他们的硬件系统实现。

MIDI 命令通常由 2 或 3 个字节组成,并通过特殊的操作方式编码。 这样的操作包括按键 (NoteOn),释放键件 (NoteOff),更换合成器程序或者修补齿轮的设置或者移动齿轮来改变音符的音调。 除了操作之外,这些命令还包括其他参数,例如频道数 (0-15),音符的音调 (0-127) 以及按键的速度(速率)。可以将不同的合成器置于不同的频道中以实现复杂的多音性能。

表 1. MIDI 命令
定义操作其他参数
MIDI 命令由 2 或 3 个字节组成,并通过特殊的操作方式编码。操作包括:
  • 按键 (NoteOn)
  • 释放按键 (NoteOff)
  • 更换合成器程序或者修补齿轮的设置或者移动齿轮来改变音符的音调。
其他参数包括:
  • 频道数 (0-15)
  • 音符的音调 (0-127)
  • 释放键的速度(速率)

MIDI 控制器与合成器通过电线相连接,利用异步串行接口可以在不同设备间传送命令。接口的波特率为 31.25K 波特,其中包括 1 个起始位, 8 个数据位以及 1 个停止位,每个串行比特周期为 320 微妙。 每个 MIDI 设备有三个连接器:MIDI In、MIDI Out 和 MIDI Thru,它们能将设备呈菊花链状连接起来。在这方面存在一些协议和标准,例如 Distributed MIDI 和 MidiWeb,他们能够通过 LAN 分发 MIDI。但是,为提高音乐性能,音乐家们主要还是使用最初的串行接口。

本文仅将 MIDI 命令作为在不同应用程序间分发的数据包实例,并不是要标准化另一种形式的 MIDI。

什么是发布/订阅消息传递?

消息传递的发布/订阅的设计模式自 19 世纪 80 年代开始使用,并且作为一种消息传递基础设施已被许多信息传递中间件厂商广泛实施和部署。 下图演示了发布/订阅系统的组件和用于 MIDI 命令的部分主题空间。

图 1. 发布/订阅消息传递
图 1. 发布/订阅消息传递流程
图 1. 发布/订阅消息传递流程

在发布/订阅系统中传播的事件包括两部分:消息内容和相关的主题。 这些题目可以看作是一个限制性的命名空间,该命名空间由代理管理。 代理是一个单独的逻辑元素,实施的时候可能会将其部署到网络内的多个处理器上。 在这种分布式的情况下,代理互相通信,从而共同管理主题和有关消息的发布和订阅。 上面演示的实例显示了 MIDI 命令的一部分主题空间。可以用主题 Midi/Short/NoteOn 给 MIDI NoteOn 命令作标记。信息提供者发布的所有 NoteOn 命令将会用此主题作标记。 消息的内容将是一些命令信息,包括哪个音符被按下以及什么时候按下的。 消息内容的格式必须得到消息发布者和订阅者的同意和理解。

对特殊消息感兴趣的信息消费者可以使用代理通过订阅一系列的主题登记注册他们所感兴趣的内容。 他们的订阅可以用通配符主题表示,例如 Midi/Short/NoteOn, 或者是主题表达方式,例如所有的 MIDI 事件 Midi/# 或所有的 MIDI Meta 命令 Midi/Meta/+。 一旦完成注册,代理将会所有发布的消息中与他们的订阅主题相匹配的消息传送给他们。 有些代理能够使用一些优先传输性能(例如 IP 的多点传送)来优化对消息的传递。

信息生产者和消费者都是匿名的。消费者不会知道消息是从何处产生的,除非明确地将该消息设计到消息内容计划或着主题空间中。同样,生产者也不会知道他们发布的消息被发往何处。 除此之外,这种体系结构还是动态变化着的,生产者和消费者随时都会启动或终止,而不会影响代理的运行。一些编程接口,例如 Java 消息传递服务 (Java Message Service,JMS) 能利用这种体系结构支持消息的传递和接收。

因此如果我们采用这种发布/订阅消息传递机制来分发 MIDI 命令,就可以开发一种与 MIDI 设备进行交互的灵活方式,这种方式不再受限于当前使用的串行协议,因为这些协议比较僵硬,不够灵活。实际上,我们可以几乎实时地将命令分发给通过网络连接在一起的任何应用程序。

MIDI 应用程序

本文提供的演示代码显示了三个应用程序。MIDI 文件播放器应用程序充当了一个信息提供者,它对 MIDI 文件进行排序,并将命令发布给代理。MIDI 输出应用程序通过订阅 MIDI 短命令充当了信息消费者,收到这些命令后将他们传送给所选的 MIDI 设备,这些设备可以是内部软件或外部硬件。最后,MIDI 输入应用程序通过捕捉 MIDI 命令并将他们发布给代理充当了信息提供者,这些 MIDI 命令是通过键盘这样的控制器输入的。接下来的部分将描述他们是如何运行的,并通过实例代码演示了一些特殊的设计点。

MIDI 文件播放器

MIDI 文件播放应用程序如下所示。与所有的演示应用程序一样,它由三部分组成:

  1. 与发布/订阅代理的连接
  2. 用来显示已经发布的或者接收到的文本区
  3. 一系列控制按钮
图 2. MIDI 文件播放器
MIDI 文件播放器
MIDI 文件播放器

应用程序启动时,启用代理 Connect 按钮,设置 LED 为红色,表明当前没有连接。可以通过代理的 TCP/IP 地址和端口号来访问代理,端口号缺省值为 1883。对消息产生者来说, 代理通过这个端口监听发布的消息。演示表明代理和客户端应用程序运行在同一台机器上,这是因为使用了本地回环地址。选择 Connect 按钮将会启动与代理的连接。进行连接的同时,LED 将会变为黄色,一旦连接成功,会显示绿色。

可以使用 Select 按钮来选择要播放的 MIDI 文件,该按钮会弹出一个文件选择对话框。选择 MIDI 文件将会启动 Play 按钮,它是用来排序的。

MIDI 文件开始播放时,会在发布 MIDI 命令的文本区显示这些命令。显示消息的格式如下:

  1. 消息主题
  2. MIDI 状态
  3. MIDI 第一个数据字节(对于 NoteOn 和 NoteOff 按钮,表示音符号码)
  4. MIDI 第二个数据位(对于 NoteOn 和 NoteOff 按钮,表示速率。 许多厂商习惯使用零速率的 NoteOn 来表示 NoteOff)
  5. 数字时间信息列,从序列的开始进行量化得到的

MIDI 输出和输入

MIDI 输出和输入应用程序看上去是类似的。同 MIDI 文件播放器一样,它们都有 GUI 组件用于连接发布/订阅代理。 MIDI 输出应用程序显示了它接收到的和要发送给所选的 MIDI 设备的消息。MIDI 输入应用程序显示了它从相连控制器发布的消息。

图 3. MIDI 输出
MIDI 输出
MIDI 输出

选择框表明 MIDI 输入设备或输出可用于应用程序。对于 MIDI 输入应用程序, 它表示当前可用的 MIDI 输入端口,对于 MIDI 输出应用程序,它表示当前可用的输出端口。

MIDI 输出应用程序上的 Subscribe 按钮使用代理为所有 MIDI 短消息注册了订阅。 这种订阅机制必须与代理进行连接。一旦接受了该订阅,代理将会将消息传送给输出应用程序。

在 MIDI 输入应用程序中,Attach 和 Detach 按钮负责连接或断开所选 MIDI 设备的应用程序。 连接到应用程序后,它会发布从控制器接收到的 MIDI 消息。

应用程序代码

本文中使用的发布/订阅 API 函数为 WebSphere® MQ Telemetry Transport API (WMQTT)。 该协议和 API 是为带宽有限的消息传递应用程序而开发的,例如远程的自动测量记录,这里的消息通常很简短,并且网络连接器的费用非常昂贵(例如卫星上行链路)。MIDI 消息很短,因而根据消息大小和 API 复杂度,附加消息头不合适。有关 WMQTT 的详细信息,请参阅本文最后的参考资料部分。

下面我们来看一下代码中的重要部分。

与代理建立连接

BrokerConnection 类实现了 Runnable 接口,并且管理客户端应用程序与代理的连接。每个客户端必须有单独的标识符串,将该标识符串作为参数传送给类构造函数。确保此标识符对于客户端是唯一的,因为如果与其他用户共享同一个代理,当试图用同一个已连接的标识符去连接客户端时,会使得已连接的客户端断开连接。

BrokerConnection.connect() 方法用于检测当前是否存在连接。如果存在,停止该连接,并创建新的客户端连接对象。 BrokerConnection.connect() 方法由 BrokerConnection 线程调用。如果连接成功,LED 就变为绿色。

清单 1. 与代理的客户端连接
	private MqttClient mqttClient = null;
	public BrokerConnection(String c, MidiMessageHandler m) {
		// Save the clientId
		clientId = c;
		// Save the Midi Message Handler
		mMH = m;
	}
	private void connect(String ipAddr, int port) throws Exception {
		String connStr;
		if (ipAddr.indexOf("://") < 0) {
			connStr = MqttClient.TCP_ID + ipAddr + "@" + port;
		} else
			connStr = ipAddr;
		if ((mqttClient != null)
				&& (!connStr.equals(mqttClient.getConnection()))) {
			mqttClient.terminate();
			mqttClient = null;
		}
		// Connect to the broker
		mqttClient = new MqttClient(connStr);
		// Set the retry interval for the connection
		mqttClient.setRetry(retryInterval);
		// Register the handler
		mqttClient.registerSimpleHandler(this);
		// Connect to the Broker
		mqttClient.connect(clientId, true, (short) 30);
	}
	public void run() {
		int rc = -1;
		// Connect to the broker
		try {
			System.out.println(clientId + " connecting to " + brokerAddress
					+ " port " + brokerPort);
			connect(brokerAddress, Integer.parseInt(brokerPort));
			// Successful connect(no exception), set LED to green
			connected = true;
			if (led != null) {
				led.setGreen();
			}
		} catch (IOException ioe) {
			System.out.println("WMQtt connect failed !");
		} catch (NumberFormatException nfe) {
			System.out.println("Invalid port number: MQIsdp Connect Exception");
		} catch (Exception ex) {
			System.out.println("Unable to create Broker connection !");
			ex.printStackTrace();
		}
		// Set the LED to red if not connected
		if (!connected) {
			if (led != null) {
				led.setRed();
			}
		}
	}

BrokerConnection 类实现了 MqttSimpleCallback 接口。它必须实现 publishArrived() 方法,但是它还包括其他的方法,这些方法用于发布,订阅,报告从代理丢失的连接以及断开信息。

向代理发布信息

与代理连接成功后,就可以发布消息了。这是通过使用 MidiEventPublisher 类完成的,该类实现了 javax.sound.midi.Receiver 接口。该接口连同 javax.sound.midi.Transmiter 接口一起允许 MIDI 命令以消息的形式在 Java 对象之间传送。该接口包括接收 MIDI 信息的 send(MidiMessage message, long lTimeStamp) 方法,并且对一些设备而言,该方法是信息时间列。 通过实现该接口, MidiEventPublisher 看上去像将另一个到音序器的 MIDI 设备,从文件来调度 MIDI 命令,并将他们传送到发布者。

MidiEventPublisher.send() 方法如下所示。

清单 2. 发布 MIDI 消息
public void send(MidiMessage message, long lTimeStamp) {
  if (message instanceof ShortMessage) {
	ShortMessage sM = (ShortMessage) message;
	// Get command and channel data
	int command = sM.getCommand();
	// We can publish if it's one of these
	if ((command == ShortMessage.NOTE_ON)
			|| (command == ShortMessage.NOTE_OFF)
			|| (command == ShortMessage.CHANNEL_PRESSURE)
			|| (command == ShortMessage.CONTROL_CHANGE)
			|| (command == ShortMessage.PITCH_BEND)
			|| (command == ShortMessage.PROGRAM_CHANGE)
			|| (command == ShortMessage.POLY_PRESSURE)) {
		// If the ShortMessage is a NoteOn orNoteOff, then we need an
		// accurate TimeStamp based on 0 for the first note.
		// Any other commands, set to -1
		if ((command == ShortMessage.NOTE_ON)
			|| (command == ShortMessage.NOTE_OFF)) {
		 long time = System.currentTimeMillis();
		sME = new ShortMessageEvent(sM, timeStamp
				.getQuantisedTime(time));
		} else {
		sME = new ShortMessageEvent(sM, -1);
		}
		// Publish
	    Exception pubExp = null;
		try {
			brokerConnection.publish(sME.getTopic(), 
			   sME.getBytes(), 0, false);
		} catch (Exception ex) {
		 // Publish failed
		System.out.println("Error in publishing");
		ex.printStackTrace();
		pubExp = ex;
	   }
	// Print out a message to the publish panel
	msgString = sME.getTopic() + " " + sME.printEvent() + "\n";
			}
		} else if (message instanceof SysexMessage) {
			.....
		} else if (message instanceof MetaMessage) {
			.....
		}
		// Append to the text area
		PubSubPanel.pubData.append(msgString);
	}

由于 MidiMessage 类型既可以是 MIDI Short,Meta 又可以作为系统高级消息,所以通常这种方法结构用于测试 MIDI 消息属于何种类型,并为要发布的事件构建一个实例。生成字符串作为 GUI 的输出,然后发布 MIDI 事件。 MidiEventPublisher 类还包括关闭该设备的方法。

BrokerConnection.publish() 方法封装了对 WMQTT API 函数的低级调用。它首先查看是否已经与代理建立连接。然后连同主题一起发布事件,并将服务质量设置为 0(不存在发送并忘记 (fire-and-forget))。

清单 3. BrokerConnection.publish()
	public void publish(String topic, byte[] message, int qos, boolean retained)
			throws Exception {
		if (connected) {
			try {
				mqttClient.publish(topic, message, qos, retained);
			} catch (Exception ex) {
				System.out.println("Producer publish exception caught !");
				throw ex;
			}
		} else {
			throw new Exception("Producer client not connected");
		}
	}

向代理订阅消息

那些想从接收来自代理的消息的应用程序通过订阅一系列主题来完成订阅操作。消息被发布给与应用程序的订约要求相匹配的代理后,代理将他们传送给各订阅客户端。 BrokerConnection.subscription() 方法用于向代理订阅消息。 MidiOut() 客户端仅对接收 MIDI 短消息感兴趣,因此客户端只订阅 Midi/Short/+ 这些主题方面的消息。最后的通配符表明所有以 Midi/Short/ 开始的主题都是需要订阅的。观察演示代码中的通配符,客户端还订阅了额外的关闭事件,该事件是由 MIDI 文件播放器在文件的末尾发布的。

BrokerConnection 类实现了 WMQTT API 的 MqttSimpleCallback 接口。该接口注册了一种回调方法,当订阅到达订阅客户端时,就会调用该方法。在该实例代码中, MidiMessageHandler 类中定义了回调功能。查看 MidiMessageHandlerBrokerConnection 类看他们是如何相互结合的。接下来的部分将讨论订阅客户端是如何接收消息的。

接收来自代理的消息

发布的消息到达目的订阅客户端后,调用 BrokerConnection.publishArrived() 方法。该方法是 MqttSimpleCallback 接口的一部分。这种方法依次调用 MidiMessageHandler.handleMidiMessage() 方法,对接收到的消息做了很多实时处理。这种方法如下面的清单所示。

记住 MidiOut 客户端只订阅了 MIDI 短消息和特别的结束事件,因此可以忽略其他的 MIDI 事件,例如 Meta 或者 System-Exclusive。 我们需要做的就是创建 ShortMessageEvent 并且将其发送给 MIDI 接收器,该接收器封装了所选的 MIDI 输出端口。在构建 MidiMessageHandler 对象的时候已经通过 MidiOut 接口选择和创建了输出端口。

清单 4. 接收消息事件
	public void handleMidiMessage(String topic, byte[] msg) {
		byte[] buffer;
		int length;
		Event mE = null;
		ShortMessageEvent sME = null;
		if (topic.compareTo(EventConstants.closingTopic) == 0) {
			closing = true;
			mE = new ControlMessageEvent(topic, msg, -1);
			// Build the message string .....
			msgString = topic + "\n";
			// ..... and exit
			System.exit(0);
		} else {
			// Create the ShortMessageEvent
			mE = new ShortMessageEvent(topic, msg);
			// Build the message string
			sME = (ShortMessageEvent) mE;
			msgString = sME.getTopic() + " " + sME.printEvent() + "\n";
		}
		// Append to the text area
		PubSubPanel.subData.append(msgString);
		// Output the Midi message to the port
		sME.sendToMidiReceiver(outReceiver);
	}

MIDI 事件类

该应用程序实例使用一系列类来代表发布的 MIDI 消息。Java 包含了一组类,例如用 javax.sound.midi.ShortMessage 来代表原始的 MIDI 消息。通过 Java API 可以使用多种方法来获得和设置 MIDI 信息的端口。要发布消息,需要另外一个类 ShortMessageEvent,该类可以使我们获得原始消息,将其串行化用于发布,增加主题,反序列化以便于接收和添加各种其他的实用方法。为便于使用,我们定义了一个 EventConstants 类来声明一些有用的常量,例如主题字符串。

下面的清单显示了 ShortMessageEvent 类的结构。其中有两个构造器:一个用于将 MIDI ShortMessage 对象串行输入到字节阵列,而第二个用来将字节阵列和主题反序列化到 MIDI ShortMessage中。要更好的理解该类,请详细研究一下示例代码。

清单 5. ShortMessageEvent 类
public class ShortMessageEvent  extends Event{
	
    public ShortMessageEvent(ShortMessage sm, int ts) {
      // Builds the ShortMessage Event given a javax.sound.midi.ShortMessage 
      //and timestamp
      // A serialized byte array is built using BuildByteArray()
            .....
        }
     private void BuildByteArray() {
       // Create the byte[] representation
        ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
         DataOutputStream dos = new DataOutputStream(bos);
         try {
           dos.writeInt(status);
           dos.writeInt(data1);
           dos.writeInt(data2);
             dos.writeInt(timeStamp);
             dos.close();
             bytes = bos.toByteArray();
          } catch (IOException e) {
             e.printStackTrace();
          }
        }
	
     public ShortMessageEvent (String t, byte[] b) {
            // Builds the ShortMessage Event given a topic string and 
            //serialized byte array
            // This de-serializes the published event
            .....
        }
	
        public String setTopic() {
            // Sets the event topic for a given MIDI command
            .....
        }
	
        public void sendToMidiReceiver(Receiver r) {
            // Outputs the event to a MIDI Receiver
            .....
        }
	
        public String printEvent() {
            // Build a String describing the event
        }
	
        // Various Getters and Setters
    }

获取运行代码

Java 中用于 MIDI 支持的规范说明已经存在很长时间了,但不幸的是仍然没有完成最后的实现。 Java 1.5 的第二个版本已经准备公开,用户可以从 Sun Web 站点下载该版本来运行实例代码。

用户下一步需要完成的事情就是 WMQTT 客户端类的实现。IBM® 的 Supportpac number IA92 是可用的,用户可以使用本文参考资料部分的链接下载一个副本。最后用户需要访问代理。如果用户有 IBM 的 WebSphere Business Integration Event Broker,那么就可以访问代理了。或者,也可以使用 Web 站点上可用的代理来测试代码。 IBM 公司在 realtime.ngi.ibm.com 上有可用的代理供测试,用户可以访问它,因此将它作为实例应用程序中的代理 TCP/IP 地址,使用端口 1883。该代理是测试用的,其可用性得不到保证,所以用户需要耐心些。

文章最后是一些警告信息。如果使用共享的代理,牢记订阅应用程序将从代理接收来具有特定主题的所有消息。实例代码中的主题已在 EvenConstants.java 文件中定义,用户应该对这些字符做一些特殊修改,例如 "JohnIbbotson/Midi/Short/NoteOn" -- 否则,用户不仅会收到来自用户本身的应用程序发布的 MIDI 信息, 而且还会收到同时运行该实例的其他应用程序发布的信息。结果将很有趣 -- 一种新型的在线干扰形成了。

结束语

本文介绍了发布/订阅消息传递技术,并显示了如何利用 IBM 的 WebSphere Business Integration Event Broker 将该技术用于连接 MIDI 兼容的电子音乐设备。利用这种技术,用户可以创建互连网络,比 MIDI 标准支持的串行接口更具灵活性。

请欣赏!

致谢

非常感谢 Andy Stanford-Clark 先生和他的小组对我的建议,帮助和讨论。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere
ArticleID=57840
ArticleTitle=利用发布/订阅消息传递机制分发 MIDI 命令
publish-date=12012004