1 /* 2 * Copyright (c) 1999, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.media.sound; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 32 import javax.sound.midi.InvalidMidiDataException; 33 import javax.sound.midi.MidiDevice; 34 import javax.sound.midi.MidiDeviceReceiver; 35 import javax.sound.midi.MidiDeviceTransmitter; 36 import javax.sound.midi.MidiMessage; 37 import javax.sound.midi.MidiUnavailableException; 38 import javax.sound.midi.Receiver; 39 import javax.sound.midi.Transmitter; 40 41 42 /** 43 * Abstract AbstractMidiDevice class representing functionality shared by 44 * MidiInDevice and MidiOutDevice objects. 45 * 46 * @author David Rivas 47 * @author Kara Kytle 48 * @author Matthias Pfisterer 49 * @author Florian Bomers 50 */ 51 abstract class AbstractMidiDevice implements MidiDevice, ReferenceCountingDevice { 52 53 private static final boolean TRACE_TRANSMITTER = false; 54 55 private ArrayList<Receiver> receiverList; 56 57 private TransmitterList transmitterList; 58 59 // lock to protect receiverList and transmitterList 60 // from simultaneous creation and destruction 61 // reduces possibility of deadlock, compared to 62 // synchronizing to the class instance 63 private final Object traRecLock = new Object(); 64 65 // DEVICE ATTRIBUTES 66 67 private final MidiDevice.Info info; 68 69 // DEVICE STATE 70 71 private volatile boolean open; 72 private int openRefCount; 73 74 /** List of Receivers and Transmitters that opened the device implicitely. 75 */ 76 private List<Object> openKeepingObjects; 77 78 /** 79 * This is the device handle returned from native code. 80 */ 81 protected volatile long id; 82 83 /** 84 * Constructs an AbstractMidiDevice with the specified info object. 85 * @param info the description of the device 86 */ 87 /* 88 * The initial mode and only supported mode default to OMNI_ON_POLY. 89 */ AbstractMidiDevice(MidiDevice.Info info)90 protected AbstractMidiDevice(MidiDevice.Info info) { 91 92 if(Printer.trace) Printer.trace(">> AbstractMidiDevice CONSTRUCTOR"); 93 94 this.info = info; 95 openRefCount = 0; 96 97 if(Printer.trace) Printer.trace("<< AbstractMidiDevice CONSTRUCTOR completed"); 98 } 99 100 // MIDI DEVICE METHODS 101 102 @Override getDeviceInfo()103 public final MidiDevice.Info getDeviceInfo() { 104 return info; 105 } 106 107 /** Open the device from an application program. 108 * Setting the open reference count to -1 here prevents Transmitters and Receivers that 109 * opened the device implicitly from closing it. The only way to close the device after 110 * this call is a call to close(). 111 */ 112 @Override open()113 public final void open() throws MidiUnavailableException { 114 if (Printer.trace) Printer.trace("> AbstractMidiDevice: open()"); 115 synchronized(this) { 116 openRefCount = -1; 117 doOpen(); 118 } 119 if (Printer.trace) Printer.trace("< AbstractMidiDevice: open() completed"); 120 } 121 122 /** Open the device implicitly. 123 * This method is intended to be used by AbstractReceiver 124 * and BasicTransmitter. Actually, it is called by getReceiverReferenceCounting() and 125 * getTransmitterReferenceCounting(). These, in turn, are called by MidiSytem on calls to 126 * getReceiver() and getTransmitter(). The former methods should pass the Receiver or 127 * Transmitter just created as the object parameter to this method. Storing references to 128 * these objects is necessary to be able to decide later (when it comes to closing) if 129 * R/T's are ones that opened the device implicitly. 130 * 131 * @object The Receiver or Transmitter instance that triggered this implicit open. 132 */ openInternal(Object object)133 private void openInternal(Object object) throws MidiUnavailableException { 134 if (Printer.trace) Printer.trace("> AbstractMidiDevice: openInternal()"); 135 synchronized(this) { 136 if (openRefCount != -1) { 137 openRefCount++; 138 getOpenKeepingObjects().add(object); 139 } 140 // double calls to doOpens() will be catched by the open flag. 141 doOpen(); 142 } 143 if (Printer.trace) Printer.trace("< AbstractMidiDevice: openInternal() completed"); 144 } 145 doOpen()146 private void doOpen() throws MidiUnavailableException { 147 if (Printer.trace) Printer.trace("> AbstractMidiDevice: doOpen()"); 148 synchronized(this) { 149 if (! isOpen()) { 150 implOpen(); 151 open = true; 152 } 153 } 154 if (Printer.trace) Printer.trace("< AbstractMidiDevice: doOpen() completed"); 155 } 156 157 @Override close()158 public final void close() { 159 if (Printer.trace) Printer.trace("> AbstractMidiDevice: close()"); 160 synchronized (this) { 161 doClose(); 162 openRefCount = 0; 163 } 164 if (Printer.trace) Printer.trace("< AbstractMidiDevice: close() completed"); 165 } 166 167 /** Close the device for an object that implicitely opened it. 168 * This method is intended to be used by Transmitter.close() and Receiver.close(). 169 * Those methods should pass this for the object parameter. Since Transmitters or Receivers 170 * do not know if their device has been opened implicitely because of them, they call this 171 * method in any case. This method now is able to seperate Receivers/Transmitters that opened 172 * the device implicitely from those that didn't by looking up the R/T in the 173 * openKeepingObjects list. Only if the R/T is contained there, the reference count is 174 * reduced. 175 * 176 * @param object The object that might have been opening the device implicitely (for now, 177 * this may be a Transmitter or receiver). 178 */ closeInternal(Object object)179 public final void closeInternal(Object object) { 180 if (Printer.trace) Printer.trace("> AbstractMidiDevice: closeInternal()"); 181 synchronized(this) { 182 if (getOpenKeepingObjects().remove(object)) { 183 if (openRefCount > 0) { 184 openRefCount--; 185 if (openRefCount == 0) { 186 doClose(); 187 } 188 } 189 } 190 } 191 if (Printer.trace) Printer.trace("< AbstractMidiDevice: closeInternal() completed"); 192 } 193 doClose()194 public final void doClose() { 195 if (Printer.trace) Printer.trace("> AbstractMidiDevice: doClose()"); 196 synchronized(this) { 197 if (isOpen()) { 198 implClose(); 199 open = false; 200 } 201 } 202 if (Printer.trace) Printer.trace("< AbstractMidiDevice: doClose() completed"); 203 } 204 205 @Override isOpen()206 public final boolean isOpen() { 207 return open; 208 } 209 implClose()210 protected void implClose() { 211 synchronized (traRecLock) { 212 if (receiverList != null) { 213 // close all receivers 214 for(int i = 0; i < receiverList.size(); i++) { 215 receiverList.get(i).close(); 216 } 217 receiverList.clear(); 218 } 219 if (transmitterList != null) { 220 // close all transmitters 221 transmitterList.close(); 222 } 223 } 224 } 225 226 /** 227 * This implementation always returns -1. 228 * Devices that actually provide this should over-ride 229 * this method. 230 */ 231 @Override getMicrosecondPosition()232 public long getMicrosecondPosition() { 233 return -1; 234 } 235 236 /** Return the maximum number of Receivers supported by this device. 237 Depending on the return value of hasReceivers(), this method returns either 0 or -1. 238 Subclasses should rather override hasReceivers() than override this method. 239 */ 240 @Override getMaxReceivers()241 public final int getMaxReceivers() { 242 if (hasReceivers()) { 243 return -1; 244 } else { 245 return 0; 246 } 247 } 248 249 /** Return the maximum number of Transmitters supported by this device. 250 Depending on the return value of hasTransmitters(), this method returns either 0 or -1. 251 Subclasses should override hasTransmitters(). 252 */ 253 @Override getMaxTransmitters()254 public final int getMaxTransmitters() { 255 if (hasTransmitters()) { 256 return -1; 257 } else { 258 return 0; 259 } 260 } 261 262 /** Retrieve a Receiver for this device. 263 This method returns the value returned by createReceiver(), if it doesn't throw 264 an exception. Subclasses should rather override createReceiver() than override 265 this method. 266 If createReceiver returns a Receiver, it is added to the internal list 267 of Receivers (see getReceiversList) 268 */ 269 @Override getReceiver()270 public final Receiver getReceiver() throws MidiUnavailableException { 271 Receiver receiver; 272 synchronized (traRecLock) { 273 receiver = createReceiver(); // may throw MidiUnavailableException 274 getReceiverList().add(receiver); 275 } 276 return receiver; 277 } 278 279 @Override 280 @SuppressWarnings("unchecked") // Cast of result of clone getReceivers()281 public final List<Receiver> getReceivers() { 282 List<Receiver> recs; 283 synchronized (traRecLock) { 284 if (receiverList == null) { 285 recs = Collections.unmodifiableList(new ArrayList<Receiver>(0)); 286 } else { 287 recs = Collections.unmodifiableList 288 ((List<Receiver>) (receiverList.clone())); 289 } 290 } 291 return recs; 292 } 293 294 /** 295 * This implementation uses createTransmitter, which may throw an exception. 296 * If a transmitter is returned in createTransmitter, it is added to the internal 297 * TransmitterList 298 */ 299 @Override getTransmitter()300 public final Transmitter getTransmitter() throws MidiUnavailableException { 301 Transmitter transmitter; 302 synchronized (traRecLock) { 303 transmitter = createTransmitter(); // may throw MidiUnavailableException 304 getTransmitterList().add(transmitter); 305 } 306 return transmitter; 307 } 308 309 @Override 310 @SuppressWarnings("unchecked") // Cast of result of clone getTransmitters()311 public final List<Transmitter> getTransmitters() { 312 List<Transmitter> tras; 313 synchronized (traRecLock) { 314 if (transmitterList == null 315 || transmitterList.transmitters.size() == 0) { 316 tras = Collections.unmodifiableList(new ArrayList<Transmitter>(0)); 317 } else { 318 tras = Collections.unmodifiableList((List<Transmitter>) (transmitterList.transmitters.clone())); 319 } 320 } 321 return tras; 322 } 323 getId()324 final long getId() { 325 return id; 326 } 327 328 // REFERENCE COUNTING 329 330 /** Retrieve a Receiver and open the device implicitly. 331 This method is called by MidiSystem.getReceiver(). 332 */ 333 @Override getReceiverReferenceCounting()334 public final Receiver getReceiverReferenceCounting() 335 throws MidiUnavailableException { 336 /* Keep this order of commands! If getReceiver() throws an exception, 337 openInternal() should not be called! 338 */ 339 Receiver receiver; 340 synchronized (traRecLock) { 341 receiver = getReceiver(); 342 AbstractMidiDevice.this.openInternal(receiver); 343 } 344 return receiver; 345 } 346 347 /** Retrieve a Transmitter and open the device implicitly. 348 This method is called by MidiSystem.getTransmitter(). 349 */ 350 @Override getTransmitterReferenceCounting()351 public final Transmitter getTransmitterReferenceCounting() 352 throws MidiUnavailableException { 353 /* Keep this order of commands! If getTransmitter() throws an exception, 354 openInternal() should not be called! 355 */ 356 Transmitter transmitter; 357 synchronized (traRecLock) { 358 transmitter = getTransmitter(); 359 AbstractMidiDevice.this.openInternal(transmitter); 360 } 361 return transmitter; 362 } 363 364 /** Return the list of objects that have opened the device implicitely. 365 */ getOpenKeepingObjects()366 private synchronized List<Object> getOpenKeepingObjects() { 367 if (openKeepingObjects == null) { 368 openKeepingObjects = new ArrayList<>(); 369 } 370 return openKeepingObjects; 371 } 372 373 // RECEIVER HANDLING METHODS 374 375 /** Return the internal list of Receivers, possibly creating it first. 376 */ getReceiverList()377 private List<Receiver> getReceiverList() { 378 synchronized (traRecLock) { 379 if (receiverList == null) { 380 receiverList = new ArrayList<>(); 381 } 382 } 383 return receiverList; 384 } 385 386 /** Returns if this device supports Receivers. 387 Subclasses that use Receivers should override this method to 388 return true. They also should override createReceiver(). 389 390 @return true, if the device supports Receivers, false otherwise. 391 */ hasReceivers()392 protected boolean hasReceivers() { 393 return false; 394 } 395 396 /** Create a Receiver object. 397 throwing an exception here means that Receivers aren't enabled. 398 Subclasses that use Receivers should override this method with 399 one that returns objects implementing Receiver. 400 Classes overriding this method should also override hasReceivers() 401 to return true. 402 */ createReceiver()403 protected Receiver createReceiver() throws MidiUnavailableException { 404 throw new MidiUnavailableException("MIDI IN receiver not available"); 405 } 406 407 // TRANSMITTER HANDLING 408 409 /** Return the internal list of Transmitters, possibly creating it first. 410 */ getTransmitterList()411 final TransmitterList getTransmitterList() { 412 synchronized (traRecLock) { 413 if (transmitterList == null) { 414 transmitterList = new TransmitterList(); 415 } 416 } 417 return transmitterList; 418 } 419 420 /** Returns if this device supports Transmitters. 421 Subclasses that use Transmitters should override this method to 422 return true. They also should override createTransmitter(). 423 424 @return true, if the device supports Transmitters, false otherwise. 425 */ hasTransmitters()426 protected boolean hasTransmitters() { 427 return false; 428 } 429 430 /** Create a Transmitter object. 431 throwing an exception here means that Transmitters aren't enabled. 432 Subclasses that use Transmitters should override this method with 433 one that returns objects implementing Transmitters. 434 Classes overriding this method should also override hasTransmitters() 435 to return true. 436 */ createTransmitter()437 protected Transmitter createTransmitter() throws MidiUnavailableException { 438 throw new MidiUnavailableException("MIDI OUT transmitter not available"); 439 } 440 implOpen()441 protected abstract void implOpen() throws MidiUnavailableException; 442 443 /** 444 * close this device if discarded by the garbage collector. 445 */ 446 @Override 447 @SuppressWarnings("deprecation") finalize()448 protected final void finalize() { 449 close(); 450 } 451 452 /** Base class for Receivers. 453 Subclasses that use Receivers must use this base class, since it 454 contains magic necessary to manage implicit closing the device. 455 This is necessary for Receivers retrieved via MidiSystem.getReceiver() 456 (which opens the device implicitely). 457 */ 458 abstract class AbstractReceiver implements MidiDeviceReceiver { 459 private volatile boolean open = true; 460 461 462 /** Deliver a MidiMessage. 463 This method contains magic related to the closed state of a 464 Receiver. Therefore, subclasses should not override this method. 465 Instead, they should implement implSend(). 466 */ 467 @Override send(final MidiMessage message, final long timeStamp)468 public final synchronized void send(final MidiMessage message, 469 final long timeStamp) { 470 if (!open) { 471 throw new IllegalStateException("Receiver is not open"); 472 } 473 implSend(message, timeStamp); 474 } 475 implSend(MidiMessage message, long timeStamp)476 abstract void implSend(MidiMessage message, long timeStamp); 477 478 /** Close the Receiver. 479 * Here, the call to the magic method closeInternal() takes place. 480 * Therefore, subclasses that override this method must call 481 * 'super.close()'. 482 */ 483 @Override close()484 public final void close() { 485 open = false; 486 synchronized (AbstractMidiDevice.this.traRecLock) { 487 AbstractMidiDevice.this.getReceiverList().remove(this); 488 } 489 AbstractMidiDevice.this.closeInternal(this); 490 } 491 492 @Override getMidiDevice()493 public final MidiDevice getMidiDevice() { 494 return AbstractMidiDevice.this; 495 } 496 isOpen()497 final boolean isOpen() { 498 return open; 499 } 500 501 //$$fb is that a good idea? 502 //protected void finalize() { 503 // close(); 504 //} 505 506 } // class AbstractReceiver 507 508 509 /** 510 * Transmitter base class. 511 * This class especially makes sure the device is closed if it 512 * has been opened implicitly by a call to MidiSystem.getTransmitter(). 513 * The logic of doing so is actually in closeInternal(). 514 * 515 * Also, it has some optimizations regarding sending to the Receivers, 516 * for known Receivers, and managing itself in the TransmitterList. 517 */ 518 class BasicTransmitter implements MidiDeviceTransmitter { 519 520 private Receiver receiver = null; 521 TransmitterList tlist = null; 522 BasicTransmitter()523 protected BasicTransmitter() { 524 } 525 setTransmitterList(TransmitterList tlist)526 private void setTransmitterList(TransmitterList tlist) { 527 this.tlist = tlist; 528 } 529 530 @Override setReceiver(Receiver receiver)531 public final void setReceiver(Receiver receiver) { 532 if (tlist != null && this.receiver != receiver) { 533 if (Printer.debug) Printer.debug("Transmitter "+toString()+": set receiver "+receiver); 534 tlist.receiverChanged(this, this.receiver, receiver); 535 this.receiver = receiver; 536 } 537 } 538 539 @Override getReceiver()540 public final Receiver getReceiver() { 541 return receiver; 542 } 543 544 /** Close the Transmitter. 545 * Here, the call to the magic method closeInternal() takes place. 546 * Therefore, subclasses that override this method must call 547 * 'super.close()'. 548 */ 549 @Override close()550 public final void close() { 551 AbstractMidiDevice.this.closeInternal(this); 552 if (tlist != null) { 553 tlist.receiverChanged(this, this.receiver, null); 554 tlist.remove(this); 555 tlist = null; 556 } 557 } 558 559 @Override getMidiDevice()560 public final MidiDevice getMidiDevice() { 561 return AbstractMidiDevice.this; 562 } 563 564 } // class BasicTransmitter 565 566 /** 567 * a class to manage a list of transmitters. 568 */ 569 final class TransmitterList { 570 571 private final ArrayList<Transmitter> transmitters = new ArrayList<>(); 572 private MidiOutDevice.MidiOutReceiver midiOutReceiver; 573 574 // how many transmitters must be present for optimized 575 // handling 576 private int optimizedReceiverCount = 0; 577 578 add(Transmitter t)579 private void add(Transmitter t) { 580 synchronized(transmitters) { 581 transmitters.add(t); 582 } 583 if (t instanceof BasicTransmitter) { 584 ((BasicTransmitter) t).setTransmitterList(this); 585 } 586 if (Printer.debug) Printer.debug("--added transmitter "+t); 587 } 588 remove(Transmitter t)589 private void remove(Transmitter t) { 590 synchronized(transmitters) { 591 int index = transmitters.indexOf(t); 592 if (index >= 0) { 593 transmitters.remove(index); 594 if (Printer.debug) Printer.debug("--removed transmitter "+t); 595 } 596 } 597 } 598 receiverChanged(BasicTransmitter t, Receiver oldR, Receiver newR)599 private void receiverChanged(BasicTransmitter t, 600 Receiver oldR, 601 Receiver newR) { 602 synchronized(transmitters) { 603 // some optimization 604 if (midiOutReceiver == oldR) { 605 midiOutReceiver = null; 606 } 607 if (newR != null) { 608 if ((newR instanceof MidiOutDevice.MidiOutReceiver) 609 && (midiOutReceiver == null)) { 610 midiOutReceiver = ((MidiOutDevice.MidiOutReceiver) newR); 611 } 612 } 613 optimizedReceiverCount = 614 ((midiOutReceiver!=null)?1:0); 615 } 616 // more potential for optimization here 617 } 618 619 620 /** closes all transmitters and empties the list */ close()621 void close() { 622 synchronized (transmitters) { 623 for(int i = 0; i < transmitters.size(); i++) { 624 transmitters.get(i).close(); 625 } 626 transmitters.clear(); 627 } 628 if (Printer.trace) Printer.trace("TransmitterList.close() succeeded"); 629 } 630 631 632 633 /** 634 * Send this message to all receivers 635 * status = packedMessage & 0xFF 636 * data1 = (packedMessage & 0xFF00) >> 8; 637 * data1 = (packedMessage & 0xFF0000) >> 16; 638 */ sendMessage(int packedMessage, long timeStamp)639 void sendMessage(int packedMessage, long timeStamp) { 640 try { 641 synchronized(transmitters) { 642 int size = transmitters.size(); 643 if (optimizedReceiverCount == size) { 644 if (midiOutReceiver != null) { 645 if (TRACE_TRANSMITTER) Printer.println("Sending packed message to MidiOutReceiver"); 646 midiOutReceiver.sendPackedMidiMessage(packedMessage, timeStamp); 647 } 648 } else { 649 if (TRACE_TRANSMITTER) Printer.println("Sending packed message to "+size+" transmitter's receivers"); 650 for (int i = 0; i < size; i++) { 651 Receiver receiver = transmitters.get(i).getReceiver(); 652 if (receiver != null) { 653 if (optimizedReceiverCount > 0) { 654 if (receiver instanceof MidiOutDevice.MidiOutReceiver) { 655 ((MidiOutDevice.MidiOutReceiver) receiver).sendPackedMidiMessage(packedMessage, timeStamp); 656 } else { 657 receiver.send(new FastShortMessage(packedMessage), timeStamp); 658 } 659 } else { 660 receiver.send(new FastShortMessage(packedMessage), timeStamp); 661 } 662 } 663 } 664 } 665 } 666 } catch (InvalidMidiDataException e) { 667 // this happens when invalid data comes over the wire. Ignore it. 668 } 669 } 670 sendMessage(byte[] data, long timeStamp)671 void sendMessage(byte[] data, long timeStamp) { 672 try { 673 synchronized(transmitters) { 674 int size = transmitters.size(); 675 if (TRACE_TRANSMITTER) Printer.println("Sending long message to "+size+" transmitter's receivers"); 676 for (int i = 0; i < size; i++) { 677 Receiver receiver = transmitters.get(i).getReceiver(); 678 if (receiver != null) { 679 //$$fb 2002-04-02: SysexMessages are mutable, so 680 // an application could change the contents of this object, 681 // or try to use the object later. So we can't get around object creation 682 // But the array need not be unique for each FastSysexMessage object, 683 // because it cannot be modified. 684 receiver.send(new FastSysexMessage(data), timeStamp); 685 } 686 } 687 } 688 } catch (InvalidMidiDataException e) { 689 // this happens when invalid data comes over the wire. Ignore it. 690 return; 691 } 692 } 693 694 /** 695 * Send this message to all transmitters. 696 */ sendMessage(MidiMessage message, long timeStamp)697 void sendMessage(MidiMessage message, long timeStamp) { 698 if (message instanceof FastShortMessage) { 699 sendMessage(((FastShortMessage) message).getPackedMsg(), timeStamp); 700 return; 701 } 702 synchronized(transmitters) { 703 int size = transmitters.size(); 704 if (optimizedReceiverCount == size) { 705 if (midiOutReceiver != null) { 706 if (TRACE_TRANSMITTER) Printer.println("Sending MIDI message to MidiOutReceiver"); 707 midiOutReceiver.send(message, timeStamp); 708 } 709 } else { 710 if (TRACE_TRANSMITTER) Printer.println("Sending MIDI message to "+size+" transmitter's receivers"); 711 for (int i = 0; i < size; i++) { 712 Receiver receiver = transmitters.get(i).getReceiver(); 713 if (receiver != null) { 714 //$$fb 2002-04-02: ShortMessages are mutable, so 715 // an application could change the contents of this object, 716 // or try to use the object later. 717 // We violate this spec here, to avoid costly (and gc-intensive) 718 // object creation for potentially hundred of messages per second. 719 // The spec should be changed to allow Immutable MidiMessages 720 // (i.e. throws InvalidStateException or so in setMessage) 721 receiver.send(message, timeStamp); 722 } 723 } 724 } 725 } 726 } 727 } // TransmitterList 728 } 729