Java MIDI Sequencer SyncMode - java

Hi: I'm trying to get an instance of the MIDI Sequencer sync to an external clock. I did:
S_p = MidiSystem.getSequencer(false);
D2 = MidiSystem.getMidiDevice(MidiSystem.getMidiDeviceInfo()[1]);
S_p.open();
D2.open();
R2=S_p.getReceiver();
T2=D2.getTransmitter();
T2.setReceiver(R2);
but
for(int i=0;i<S_p.getMasterSyncModes().length;i++)
{System.out.println("Available modes are "+i+ " "+S_p.getMasterSyncModes()[i].toString());}
returns
Available modes are 0 Internal Clock
which means this will be useless.
S_p.setMasterSyncMode(Sequencer.SyncMode.MIDI_SYNC);
What am I doing wrong ?
Of course I have confirmation of messages going out of D2 and into another receiver custom written to notify to system.out, and sequencer plays normally, it just appears that it doesn't support the SyncModes docs say it should. Specifically this phrase confuses me (from MIDI_SYNC: "This mode only applies as the master sync mode for sequencers that are also MIDI receivers."
What's the meaning of the sequencer BEING a receiver. I thought it should be enough with my approach of getReceiver()
Regards and thanks !

For the future generations:
Homebrew, works-for-me, MIDI-clocked-sequencer. (Guess it's now clear what it says about the sequencer BEING a receiver): (ASSUMES that receivers listen on different MIDI channels, and tracks are created accordingly. Sends all notes off at closing. Track_Master is from my implementation, keeps track of the channel the receiver is listening)
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
/**
* #author claudio
*
*/
public class MIDI_Clocked_Sequencer implements Receiver {
private Sequence S;
private int[] playheads;
private long counter=0;
private Receiver[] receivers;
private long counter_reset;
/**
* #throws MidiUnavailableException
*
*/
public MIDI_Clocked_Sequencer(Sequence S,long counter_reset,int[] where_to_get_receivers) throws MidiUnavailableException {
this.setSequence(S);
this.counter_reset=counter_reset;
playheads=new int[S.getTracks().length];
receivers=new Receiver[where_to_get_receivers.length];
for(int i=0;i<where_to_get_receivers.length;i++){
this.receivers[i]=MidiSystem.getMidiDevice(MidiSystem.getMidiDeviceInfo()[where_to_get_receivers[i]]).getReceiver();
MidiSystem.getMidiDevice(MidiSystem.getMidiDeviceInfo()[where_to_get_receivers[i]]).open();
}
}
#Override
public void close() {
for(int j=0;j<receivers.length;j++){
try {
receivers[j].send(new ShortMessage(ShortMessage.CONTROL_CHANGE+Track_Master.channel_map.get(Track_Master.gui_map.get(j)),123,0), 0);
} catch (InvalidMidiDataException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
receivers[j].close();}
}
#Override
public void send(MidiMessage msg, long arg1) {
Set<MidiMessage> message_buffer = new HashSet<MidiMessage>();
if(msg.getMessage()[0]==-8){
if(counter==counter_reset){counter=0;}
for(int j=0;j<S.getTracks().length;j++){
if(playheads[j]==S.getTracks()[j].size()){playheads[j]=0;};
while(playheads[j]<S.getTracks()[j].size() && S.getTracks()[j].get(playheads[j]).getTick()==counter){
message_buffer.add(S.getTracks()[j].get(playheads[j]).getMessage());
playheads[j]=playheads[j]+1;
}
}
for(Iterator<MidiMessage> it=message_buffer.iterator();it.hasNext();){
MidiMessage f=it.next();
for(int j=0;j<receivers.length;j++)
{
receivers[j].send(f, -1);}
}
counter=counter+1;
}
}
public Sequence getSequence() {
return S;
}
public void setSequence(Sequence s) {
this.S = s;
}
}

Many questions at once!
1) The most important information is that Sequencers in Java Sound are plugins (service providers, SPI). Java ships with at least one Sequencer, but there is no mention that it does support all features imaginable. In particular, the existence of the getMasterSyncModes() query is an indication for the fact that not every Sequencer implementation will support all sync modes. And as you have found out, the default Sequencer in Oracle's Java happens to not support any sync modes except Internal. To be clear about it: the Java specification does not say that a Sequencer must support any particular sync mode.
2) It is, however, possible to add an own implementation of Sequencer with external sync in form of a plugin.
What's the meaning of the sequencer BEING a receiver?
3) I assume what is meant is that in order to sync to another master sync provider, a Sequencer must be able to receive the MIDI Sync messages, i.e. also receive MIDI messages. I agree that the terminology of Receiver and Transmitter is indeed confusing in Java Sound.
What am I doing wrong?
4) You're not doing anything wrong. You can query the sync modes, and if the Sequencer supports the sync mode you're after, it'll work. But chances are, you won't find such a Sequencer implementation...
S_p = MidiSystem.getSequencer(false);
5) As said above, in Java Sound, Sequencers are plugins just like MIDI devices and Synthesizers. So your system might provide multiple Sequencers. You can query MidiSystem.getMidiDeviceInfo(), get the various devices, and see if they are instanceof Sequencer to find all available Sequencers.
But I think the main answer is: there is probably no implementation of Sequencer publicly available which supports external synchronization.
It is possible to get active and fix it in OpenJDK's RealTimeSequencer...

Related

How to save game data to the cloud using gdx-gamesvcs?

I want to implement Google Play Games Services in my game on the libgdx engine. I tried using gdx-gamesvcs for this. But I am having trouble saving data. I understood from the example that one value is being saved, not the entire state of the game. So I decided to check it out: save and load one value using gsClient.loadGameState and gsClient.saveGameState. I deliberately deleted the game data from the device. But as a result, not only the test value changed, but many others as well. I thought that the state of the entire game is being saved, but the values ​​obtained do not fit into the logic of the game and could not be obtained in it.
How should I use this tool and is it worth it at all, or is it better to use what libgdx itself offers?
Here is a piece of code:
if (gsClient.isSessionActive()) {
try {
gsClient.saveGameState("data", intToByteArray(testValue), 0, null);
} catch (UnsupportedOperationException unsupportedOperationException) {
}
if (gsClient.isSessionActive()) {
try {
gsClient.loadGameState("data", new ILoadGameStateResponseListener() {
#Override
public void gsGameStateLoaded(byte[] gameState) {
if (gameState != null) {
setTestValue(bytesToInt(gameState));
}
}
});
} catch (UnsupportedOperationException unsupportedOperationException) {
}
}
UPD
Yes, saving occurs both to the cloud and to the device, for saving to the device I use Preferences. I have a Google account login button in the game, it works, I have repeatedly seen this standard bar of my account level, which appears at the top when I log in. Everything is set up in the developer console too, I have an id for achievements and leaderboards. In code, I work with the client like this (In the create() method):
public IGameServiceClient gsClient;
if (gsClient == null) {
gsClient = new MockGameServiceClient(1) {
#Override
protected Array<ILeaderBoardEntry> getLeaderboardEntries() {
return null;
}
#Override
protected Array<String> getGameStates() {
return null;
}
#Override
protected byte[] getGameState() {
return new byte[0];
}
#Override
protected Array<IAchievement> getAchievements() {
return null;
}
#Override
protected String getPlayerName() {
return null;
}
};
}
gsClient.setListener(this);
gsClient.resumeSession();
Next is loading.
The exception is not caught, I removed it and everything works as before.
Well, libgdx offers no built-in cloud-save, it is hard to use it for that. :-)
You should in any case save to local AND to cloud, as the cloud is not very fast to load its state.
I can see no problem in your code besides the fact that you swallow an UnsupportedOperationException that is thrown if you did not activate cloud save feature. So the interesting question is: what happens if you don't swallow the exception, and did you intialize GpgsClient with cloud save enabled? Are you really logged in to Gpgs, and is the feature also activated in your developer console?
The main problem was that gameState was null, this arose due to the fact that you had to wait 24 hours after enabling the save function in the developer console, and the advice on clearing the memory of google play games on the test device did not help. After a while gameState began to pass the existing values, but I started having problems with the graphics flow, probably due to the asynchronous loading.

Android: How to retrieve MIDI events at the right time?

I am trying to retrieve all MIDI events at the right time from a MIDI file, within an Android app.
The following code works on a standard JVM (on my computer), using the javax.sound.midi API.
Sequencer sequencer = MidiSystem.getSequencer();
Sequence sequence = MidiSystem.getSequence(new File(FILENAME));
sequencer.setSequence(sequence);
sequencer.open();
sequencer.getTransmitter().setReceiver(new Receiver()
{
#Override
public void send(MidiMessage message, long timeStamp)
{
System.out.println(Arrays.toString(message.getMessage()));
}
#Override
public void close()
{
}
});
sequencer.start();
Unfortunatelly, javax.sound.* package is not available on Android. A porting for Android is available on Github (https://github.com/kshoji/javax.sound.midi-for-Android) but my code sample above doesn't work (sequencer.getTransmitter() returns null).
Does anyone know how to do that? I didn't found any interesting library (http://www.midi.org/aboutmidi/android.php) for what I want to do.
Thank you.

Android MediaPlayer reset freezes UI

I have a problem with the Android MediaPlayer when changing the dataSource of the player. According the specification of the MediaPlayer (http://developer.android.com/reference/android/media/MediaPlayer.html) I have to reset the player when changing the dataSource. This works fine, but as soon as the channelChanged method is called twice in quick succession the MediaPlayer.reset freezes the UI. I profile the code as seen here:
public void channelChanged(String streamingUrl)
{
long m1 = System.currentTimeMillis();
mMediaPlayer.reset();
long m2 = System.currentTimeMillis();
try
{
mMediaPlayer.setDataSource(streamingUrl);
}
catch (IOException e)
{
e.printStackTrace();
}
long m3 = System.currentTimeMillis();
mMediaPlayer.prepareAsync();
long m4 = System.currentTimeMillis();
Log.d("MEDIAPLAYER", "reset: " + (m2 - m1));
Log.d("MEDIAPLAYER", "setDataSource: " + (m3 - m2));
Log.d("MEDIAPLAYER", "preparing: " + (m4 - m3));
}
reset: 3
setDataSource: 1
preparing: 0
reset: 3119
setDataSource: 2
preparing: 1
So apparently the reset is blocked by the asynchronous preparing of the first call (when I wait until the first stream starts and then call channelChanged() again, everything is fine).
Any ideas how to solve the problems? Should I execute the whole method in a separate thread? Basically I want to avoid that, because it seems not to be a good coding style and can possibly cause some further issues, e.g. when the user tries to start the player again, but the player is still in the reset method, which on the other hand seems to wait for the asyncPrepare method. It is not clear how the player would behave...
Is there any other good solution?
MediaPlayer is a tricky bastard. I recommend you take a look at the sample app where the MediaPlayer bad design is made evident by looking at the mess of code you have to write around it to have a consistent media playback experience.
If anything, after looking at the sample, you see that when they want to skip a track, they essentially reset and release…
mPlayer.reset();
mPlayer.release();
…and later when they are ready to load a new track…
try {
mPlayer.reset();
mPlayer.setDataSource(someUrl);
mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
//bam!
}
});
mPlayer.prepareAsync();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
I have added the try/catch because on some devices/OS versions, the MediaPlayer is worse than others and sometimes it just does weird stuff. You should have an Interface/Listener that is capable of reacting to these situations
UPDATE:
This is a method I use when I stop (or pause) my Music Playback (mostly taken from the sample app, this is running in a service and it has been modified to suit my own app but still).
The first method is used by both stop and pause, the former passes true, the later false
/**
* Releases resources used by the service for playback. This includes the "foreground service"
* status and notification, the wake locks and possibly the MediaPlayer.
*
* #param releaseMediaPlayer Indicates whether the Media Player should also be released or not
*/
void relaxResources(boolean releaseMediaPlayer) {
stopForeground(true);
stopMonitoringPlaybackProgress();
// stop and release the Media Player, if it's available
if (releaseMediaPlayer && mPlayer != null) {
mPlayer.reset();
mPlayer.release();
mPlayer = null;
}
// we can also release the Wifi lock, if we're holding it
if (mWifiLock.isHeld()) {
mWifiLock.release();
}
}
This is part of the processPauseRequest():
if (mState == State.Playing) {
// Pause media player and cancel the 'foreground service' state.
mState = State.Paused;
mPlayer.pause();
dispatchBroadcastEvent(ServiceConstants.EVENT_AUDIO_PAUSE);//notify broadcast receivers
relaxResources(false); // while paused, we always retain the mp and notification
And this is part of the processStopRequest() (simplified):
void processStopRequest(boolean force, final boolean stopSelf) {
if (mState == State.Playing || mState == State.Paused || force) {
mState = State.Stopped;
// let go of all resources...
relaxResources(true);
currentTrackNotification = null;
giveUpAudioFocus();
}
}
Now the core part is the next/skip…
This is what I do…
void processNextRequest(final boolean isSkipping) {
processStopRequest(true, false); // THIS IS IMPORTANT, WE RELEASE THE MP HERE
mState = State.Retrieving;
dispatchBroadcastEvent(ServiceConstants.EVENT_TRACK_INFO_LOAD_START);
// snipped but here you retrieve your next track and when it's ready…
// you just processPlayRequest() and "start from scratch"
This is how the MediaPlayer sample does it (found in the samples folder) and I haven't had problems with it.
That being said, i know what you mean when you say you get the whole thing blocked, I've seen it and it's the MP buggyness. If you get an ANR I'd like to see the log for it.
For the record here's how I "begin playing" (a lot of custom code has been omited but you get to see the MP stuff):"
/**
* Starts playing the next song.
*/
void beginPlaying(Track track) {
mState = State.Stopped;
relaxResources(false); // release everything except MediaPlayer
try {
if (track != null) {
createMediaPlayerIfNeeded();
mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mPlayer.setDataSource(track.audioUrl);
} else {
processStopRequest(true, false); // stop everything!
return;
}
mState = State.Preparing;
setUpAsForeground(); //service
/* STRIPPED ALL CODE FROM REMOTECONTROLCLIENT, AS IT ADDS A LOT OF NOISE :) */
// starts preparing the media player in the background. When it's done, it will call
// our OnPreparedListener (that is, the onPrepared() method on this class, since we set
// the listener to 'this').
// Until the media player is prepared, we *cannot* call start() on it!
mPlayer.prepareAsync();
// We are streaming from the internet, we want to hold a Wifi lock, which prevents
// the Wifi radio from going to sleep while the song is playing.
if (!mWifiLock.isHeld()) {
mWifiLock.acquire();
}
} catch (IOException ex) {
Log.e("MusicService", "IOException playing next song: " + ex.getMessage());
ex.printStackTrace();
}
}
As a final note, I've noticed that the "media player blocking everything" happens when the audio stream or source is unavailable or unreliable.
Good luck! Let me know if there's anything specific you'd like to see.
The newest phones and Android API works much butter, reset method takes only 5-20 ms when fast switching between songs (next or prev)
So there is no solution for older phones, it just how it works

Set UsbDeviceListener javax.usb/usb4java

I tried to set the UsbDeviceListener but nothing happens when I disconnect the device nor when I create a new file on the device.
Can someone tell me where is the problem in my code?
public static void listPeripherique(UsbHub hub) {
List perepheriques = hub.getAttachedUsbDevices();
Iterator iterator = perepheriques.iterator();
while (iterator.hasNext()) {
UsbDevice perepherique = (UsbDevice) iterator.next();
perepherique.addUsbDeviceListener(new UsbDeviceListener() {
#Override
public void usbDeviceDetached(UsbDeviceEvent arg0) {
System.out.println("connect " + arg0);
}
#Override
public void errorEventOccurred(UsbDeviceErrorEvent arg0) {
System.out.println("disconect " + arg0);
}
#Override
public void dataEventOccurred(UsbDeviceDataEvent arg0) {
System.out.println("new data on " + arg0);
}
});
}
if (perepherique.isUsbHub()) {
listPeripherique((UsbHub) perepherique);
}
}
Edit: It's work only for usb Detached.
Do you mind giving use more details on your environment (Linux, Win, OSX) and what kind of USB device (Stick, Gamepad, DVD-Burner, Dongle, iP(a|o)d, iPhone, Smartphone)? However, if you use Windows, it seems the driver is out-of-date: JSR80: This is an ABANDONED Windows implementation. it does not work, it needs a kernel driver.
Keep us posted on your project and progress.
Edit
I tried it with my Logitech Wingman controller and I can confirm that only usbDeviceDetached events are raised. I think the driver is really outdated :(
Sounds like it is working correctly.
Your code iterates once over all currently connected devices and registers a device listener on it. When the device is disconnected then the usbDeviceDetached event is triggered. The other two events are triggered after data is transfered from or to the device by your own program or when an error occurred when your program tried to communicate with the device.
javax-usb can't monitor data transfers made by other programs. Maybe this is what you expected? For this you need a special USB monitor tool like usbmon on Linux.

How to detect keypress

I am using java and trying to write a simple sudo mouse recorder that will store the mouse position when the space key is pressed. The only key I need to detect is the space key (Although if another key is easier, that will work).
I need the whole mouse recorder to be written in java, but it is OK if the function for the keypress is in another language. And I will be runing this in Windows 7.
What is the simplest way to do what has been described? All of the methods I have found require at minimum 20 lines of code. This is OK, except I don't understand how to implement them. I have an example source below:
import java.awt.MouseInfo;
import javax.swing.JOptionPane;
public class MouseRecorder {
public static void main (String[] args)
{
int slot = 0;
int xLoc[10];
int yLoc[10];
while (slot <= 10)
{
if (keyPressed(KEY_SPACE)) //<<<<This obviously won't work, but I'm looking for the simplest way to code this
{
xLoc[slot] = MouseInfo.getPointerInfo().getLocation().x;
yLoc[slot] = MouseInfo.getPointerInfo().getLocation().y;
slot++;
}
}
//The mouse information can now be utilized
}
}
Again your question is not clear since you have not addressed my comment:
It smells like to me that you might want a system-wide key logger that only responds to one key press, and that doesn't need a GUI. If this is so, then your best solution is to not use Java to create your application but to use a tool that can get closer to the OS. AutoIt comes to mind if this is for a Windows project. Please define your needs better.
and this forces us to guess at the problem and its solution.
If you are interested in creating a Swing GUI, having it take focus, and have it listen to key events, then the solution is to do this:
Create your Swing GUI and display it, leaving it as the active windowed application while your application is running.
Using Key Bindings have it listen for space bar presses
And then log the mouse location when the space bar is pressed.
As noted, this will not be achievable in 5 lines of code, so put that idea to the side.
If on the other hand your desire is to not have a GUI but rather listen for hot-key presses while any and all applications are running, then
The possible platforms used will be critical since your solution will require OS-specific code since creating a hot-key means having to create a keyboard handler routine, often in C, and doing this for each platform, and then linking it in to Java using JNI or JNA.
Or as noted another way is to link your Java program with an OS specific utility program or script such as AutoIt.
If you need more specific help, then please clarify your question.
Edit
You state:
Thank you for the answer, but as I have described above: "Changing languages is out of the question although it seems like that might be easier." and "I dont want a gui, if I can avoid it"
Then my second answer is what you're looking for. How adept are you at C/C++, JNI or JNA, and how good is your knowledge of operating system libraries? If you want a Java only solution, I would consider your requirements far above beginner or intermediate Java and into the realm of advanced -- or at least beyond my abilities at the moment, although I am sure that I could come up with some solutions after several days to a week or two of study.
... or consider getting rid of your "changing languages" requirement and instead allow at least meshing Java together with a scripting utility, like AutoIt. That could allow creation of solutions in a shorter period of time, at least for me. A limitation though is that these would be platform specific solutions. What is your purpose behind all of this? Could this be an XY problem in disguise?
Edit 2
I decided to try to solve this with a small AutoIt utility that I meshed with Java, and this is what I came up with.
My AutoIt program is called CaptureHotKey.au3, and it is compiled to an exe before use:
$key = $CmdLine[1]
HotKeySet($key, "hotKeyFunction")
While 1
Sleep(100)
WEnd
Func hotKeyFunction()
ConsoleWrite(#CRLF)
EndFunc
There's not much to it. All it does is set a hot-key from the first command line parameter, provides an endless while loop so that it will continue to run, and a hotkey function that is quite simple and only sends a carriage-return/line-feed to the console (which will be the standard output).
Then a Java class to help interact with this. It uses SwingPropertyLanguageSupport to allow addition of PropertyChangeListeners that respond on the Swing thread (in case I want to use this with a GUI).
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import javax.swing.event.SwingPropertyChangeSupport;
public class CaptureHotKey implements Runnable {
public static final String HOT_KEY = "hot key";
private String hotKey;
private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(
this);
private Scanner scanner;
private CaptureHotKeyFromAutoIt capture;
public CaptureHotKey(final String hotKey) throws IOException {
this.hotKey = hotKey;
capture = new CaptureHotKeyFromAutoIt(hotKey);
scanner = new Scanner(capture.getReadable());
}
public void startCapturing() {
new Thread(this).start();
}
public void exit() {
if (capture != null) {
capture.exit();
}
if (scanner != null) {
scanner.close();
}
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(listener);
}
public String getHotKey() {
return hotKey;
}
#Override
public void run() {
while (scanner != null && scanner.hasNextLine()) {
scanner.nextLine();
pcSupport.firePropertyChange(HOT_KEY, true, false);
}
}
private static class CaptureHotKeyFromAutoIt {
public static final String AUTO_IT_APP_PATH = "CaptureHotKey.exe";
private Process process = null;
private ProcessBuilder pb;
public CaptureHotKeyFromAutoIt(String hotKey) throws IOException {
List<String> cmdList = new ArrayList<>();
cmdList.add(AUTO_IT_APP_PATH);
cmdList.add(hotKey);
pb = new ProcessBuilder(cmdList);
pb.redirectErrorStream(true);
process = pb.start();
}
public void exit() {
if (process != null) {
process.destroy();
}
}
public Readable getReadable() {
if (process != null) {
return new InputStreamReader(process.getInputStream());
}
return null;
}
}
}
Finally a Java class to test this set up:;
This adds a PropertyChangeListener to the class above to allow it to be notified if the hot-key is pressed:
import java.awt.MouseInfo;
import java.awt.PointerInfo;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.Scanner;
public class CaptureHotKeyTest {
public static final String CTRL_R = "^r"; // "{SPACE}" works for spacebar
private static final String EXIT = "exit";
private CaptureHotKey capture;
public CaptureHotKeyTest() {
try {
capture = new CaptureHotKey(CTRL_R);
capture.addPropertyChangeListener(new HotKeyPropertyChngListener());
capture.startCapturing();
Scanner scan = new Scanner(System.in);
System.out.println("Press control-r to get mouse position.");
System.out.println("Type \"exit\" to exit program");
while (scan.hasNextLine()) {
String line = scan.nextLine();
if (line.equalsIgnoreCase(EXIT)) {
scan.close();
capture.exit();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private class HotKeyPropertyChngListener implements PropertyChangeListener {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(CaptureHotKey.HOT_KEY)) {
System.out.println("hot key pressed");
PointerInfo pointerInfo = MouseInfo.getPointerInfo();
System.out.println("Mouse: " + pointerInfo.getLocation());
}
}
}
public static void main(String[] args) {
new CaptureHotKeyTest();
}
}
You should use KeyListener: http://docs.oracle.com/javase/tutorial/uiswing/events/keylistener.html
It works fine with Swing

Categories

Resources