-
Bug
-
Resolution: Unresolved
-
P3
-
8, 11, 14, 16, 17
-
generic
-
generic
ADDITIONAL SYSTEM INFORMATION :
OS : Windows 10 Pro x64 build version 1909
Java Runtime : 14
A DESCRIPTION OF THE PROBLEM :
From the docs
https://docs.oracle.com/en/java/javase/14/docs/api/java.desktop/javax/sound/midi/MidiChannel.html#programChange(int,int)
It says in order to verify the bank of the channel the formulae is
int bank = (getController(0) * 128) + getController(32);
but after calling programChange(bank,program) the instrument changes correctly to the specified bank and program but during verification only getProgram() return the correct program but the bank when obtained using the above formulae always returns 0
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Steps
1) Open an MidiSynthesizer
2) Call Midi.programChange(bank,program)
3 )Retrive the bank and program as follows
bank=(channel.getController(0)*128)+channel.getController(32);
program=channel.getProgram();
4) Upon printing the values program prints correctly but bank always prints 0
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The initial bank & program is 0 , 0 which is expected
after programChange(640,124) the new bank should be 640 and the new program should be 124 that is
expected output = 640,124
ACTUAL -
actual output = 0 ,124 the instrument at bank,program 640,124 plays correctly but retrieving the actual bank always output's zero
0/0 //Before Bank,Program Change
Channel=0
Note=65
Bank=640
Program=124
0/124 //After Bank,Program Change
---------- BEGIN SOURCE ----------
import java.util.Arrays;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.VoiceStatus;
public final class Test
{
public static void main(String[] args)throws Exception
{
try(Synthesizer sys=MidiSystem.getSynthesizer())
{
if(!sys.isOpen()){sys.open();}
MidiChannel channel=sys.getChannels()[0];
//Check current bank and program
System.out.println((channel.getController(0)*128)+(channel.getController(32))+"/"+channel.getProgram());//This calculation to compute bank is part of the documentation
channel.programChange(640,124);//Bank=640,Program=124 Plays Wind Chimes
channel.noteOn(65,127); //Play Note 65 At Volume 127 [Wind Chime Sounds Correctly]
Thread.sleep(100); //Give Some Time For Note To Turn On
checkVoice(sys); //Display Voice Status
Thread.sleep(3000); //Play note for a total of 3 seconds
channel.noteOff(80); //Note Off
//Now recheck bank and program
System.out.println((channel.getController(0)*128)+(channel.getController(32))+"/"+channel.getProgram());//This Displays Bank 0 with program 124 Which is actually Telephone
}
}
private static void checkVoice(Synthesizer syn)
{
//The bank changed successfully which is even verified in the active voice
VoiceStatus active=Arrays.stream(syn.getVoiceStatus())
.filter(voice->voice.active)
.findFirst()
.get();
System.out.println("Channel="+active.channel);
System.out.println("Note="+active.note);
System.out.println("Bank="+active.bank);
System.out.println("Program="+active.program);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
I tried to find the underlying channel returned by the default synthesizer by causing an intentional null pointer exception with the call synthesizer.loadAlInstruments(null). I then followed the stack trace which lead me to
->com.sun.media.sound.SoftSynthesizer
In it's declared variables i discovered it uses the
com.sun.media.sound.SoftChannel class which is wrapped in an SoftChannelProxy object and returned to the user.
In this class i discovered 2 variables of interest
private int bank
private int controller[]
Upon scrolling down and finding the programChange(bank,program) method i discovered an piece of code which could be the source of the potential bug
This code was taken directly from the class
@Override
public void programChange(int bank, int program) {
bank = restrict14Bit(bank);
program = restrict7Bit(program);
synchronized (control_mutex) {
mainmixer.activity();
if(this.bank != bank || this.program != program)
{
this.bank = bank;
this.program = program;
current_instrument = null;
}
}
}
Notice this method does not change any index value of the controller[] array. It instead just stores the bank and program inside private variables and although the getProgram() method returns the value of this private variable the bank calculation is of no use as the controllers are never changed here
@Override
public int getProgram() {
synchronized (control_mutex) {
return program;
}
}
the bank calculation which i stated above returns 0 for now obvious reasons
This bug again occurs inside the controlChange() method.If you scroll down to the end of the method you will find another bug
if (controller == 0x00) {
bank = /*(bank & 127) +*/ (value << 7);
return;
}
if (controller == 0x20) {
bank = (bank & (127 << 7)) + value;
return;
}
this.controller[controller] = value;
In the if statements for controllers 0x00(0) and 0x20(32) which is associated with bank the new value is again stored in the private variable bank and then the method RETURNS!!. not giving it a chance to be stored in the controller array and therefore this statement
this.controller[controller] = value;
never executes for bank change
Finally the getController() method
@Override
public int getController(int controller) {
synchronized (control_mutex) {
// Should only return lower 7 bits,
// even when controller is "boosted" higher.
return this.controller[controller] & 127;
}
}
will always return 0 for bank.
These classes and packages are not accessible in my netbeans 12.0 IDE directly which is why i had to follow the stack trace
The most easiest solution would be to create an public getBank() method but since that's not possible i had to use reflection to extract the bank variable
private static int getBank(MidiChannel channel)throws Exception
{
//Treat this as a special case
if(Class.forName("com.sun.media.sound.SoftChannelProxy").isInstance(channel))
{
Field channelField=channel.getClass().getDeclaredField("channel");
channelField.setAccessible(true);
Object theActualChannel=channelField.get(channel);
Field bankField=theActualChannel.getClass().getDeclaredField("bank");
bankField.setAccessible(true);
return bankField.getInt(theActualChannel); //Return the actual bank
}
//Hopefully will work in the future
return (channel.getController(0)*128)+(channel.getController(32));
}
This throws a lot of warning messages in my log files for obvious reasons , hopefully this easy fix will be resolved
FREQUENCY : always
OS : Windows 10 Pro x64 build version 1909
Java Runtime : 14
A DESCRIPTION OF THE PROBLEM :
From the docs
https://docs.oracle.com/en/java/javase/14/docs/api/java.desktop/javax/sound/midi/MidiChannel.html#programChange(int,int)
It says in order to verify the bank of the channel the formulae is
int bank = (getController(0) * 128) + getController(32);
but after calling programChange(bank,program) the instrument changes correctly to the specified bank and program but during verification only getProgram() return the correct program but the bank when obtained using the above formulae always returns 0
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Steps
1) Open an MidiSynthesizer
2) Call Midi.programChange(bank,program)
3 )Retrive the bank and program as follows
bank=(channel.getController(0)*128)+channel.getController(32);
program=channel.getProgram();
4) Upon printing the values program prints correctly but bank always prints 0
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The initial bank & program is 0 , 0 which is expected
after programChange(640,124) the new bank should be 640 and the new program should be 124 that is
expected output = 640,124
ACTUAL -
actual output = 0 ,124 the instrument at bank,program 640,124 plays correctly but retrieving the actual bank always output's zero
0/0 //Before Bank,Program Change
Channel=0
Note=65
Bank=640
Program=124
0/124 //After Bank,Program Change
---------- BEGIN SOURCE ----------
import java.util.Arrays;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.VoiceStatus;
public final class Test
{
public static void main(String[] args)throws Exception
{
try(Synthesizer sys=MidiSystem.getSynthesizer())
{
if(!sys.isOpen()){sys.open();}
MidiChannel channel=sys.getChannels()[0];
//Check current bank and program
System.out.println((channel.getController(0)*128)+(channel.getController(32))+"/"+channel.getProgram());//This calculation to compute bank is part of the documentation
channel.programChange(640,124);//Bank=640,Program=124 Plays Wind Chimes
channel.noteOn(65,127); //Play Note 65 At Volume 127 [Wind Chime Sounds Correctly]
Thread.sleep(100); //Give Some Time For Note To Turn On
checkVoice(sys); //Display Voice Status
Thread.sleep(3000); //Play note for a total of 3 seconds
channel.noteOff(80); //Note Off
//Now recheck bank and program
System.out.println((channel.getController(0)*128)+(channel.getController(32))+"/"+channel.getProgram());//This Displays Bank 0 with program 124 Which is actually Telephone
}
}
private static void checkVoice(Synthesizer syn)
{
//The bank changed successfully which is even verified in the active voice
VoiceStatus active=Arrays.stream(syn.getVoiceStatus())
.filter(voice->voice.active)
.findFirst()
.get();
System.out.println("Channel="+active.channel);
System.out.println("Note="+active.note);
System.out.println("Bank="+active.bank);
System.out.println("Program="+active.program);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
I tried to find the underlying channel returned by the default synthesizer by causing an intentional null pointer exception with the call synthesizer.loadAlInstruments(null). I then followed the stack trace which lead me to
->com.sun.media.sound.SoftSynthesizer
In it's declared variables i discovered it uses the
com.sun.media.sound.SoftChannel class which is wrapped in an SoftChannelProxy object and returned to the user.
In this class i discovered 2 variables of interest
private int bank
private int controller[]
Upon scrolling down and finding the programChange(bank,program) method i discovered an piece of code which could be the source of the potential bug
This code was taken directly from the class
@Override
public void programChange(int bank, int program) {
bank = restrict14Bit(bank);
program = restrict7Bit(program);
synchronized (control_mutex) {
mainmixer.activity();
if(this.bank != bank || this.program != program)
{
this.bank = bank;
this.program = program;
current_instrument = null;
}
}
}
Notice this method does not change any index value of the controller[] array. It instead just stores the bank and program inside private variables and although the getProgram() method returns the value of this private variable the bank calculation is of no use as the controllers are never changed here
@Override
public int getProgram() {
synchronized (control_mutex) {
return program;
}
}
the bank calculation which i stated above returns 0 for now obvious reasons
This bug again occurs inside the controlChange() method.If you scroll down to the end of the method you will find another bug
if (controller == 0x00) {
bank = /*(bank & 127) +*/ (value << 7);
return;
}
if (controller == 0x20) {
bank = (bank & (127 << 7)) + value;
return;
}
this.controller[controller] = value;
In the if statements for controllers 0x00(0) and 0x20(32) which is associated with bank the new value is again stored in the private variable bank and then the method RETURNS!!. not giving it a chance to be stored in the controller array and therefore this statement
this.controller[controller] = value;
never executes for bank change
Finally the getController() method
@Override
public int getController(int controller) {
synchronized (control_mutex) {
// Should only return lower 7 bits,
// even when controller is "boosted" higher.
return this.controller[controller] & 127;
}
}
will always return 0 for bank.
These classes and packages are not accessible in my netbeans 12.0 IDE directly which is why i had to follow the stack trace
The most easiest solution would be to create an public getBank() method but since that's not possible i had to use reflection to extract the bank variable
private static int getBank(MidiChannel channel)throws Exception
{
//Treat this as a special case
if(Class.forName("com.sun.media.sound.SoftChannelProxy").isInstance(channel))
{
Field channelField=channel.getClass().getDeclaredField("channel");
channelField.setAccessible(true);
Object theActualChannel=channelField.get(channel);
Field bankField=theActualChannel.getClass().getDeclaredField("bank");
bankField.setAccessible(true);
return bankField.getInt(theActualChannel); //Return the actual bank
}
//Hopefully will work in the future
return (channel.getController(0)*128)+(channel.getController(32));
}
This throws a lot of warning messages in my log files for obvious reasons , hopefully this easy fix will be resolved
FREQUENCY : always