Files
jBass/src/aas/AAS_Receiver_Multichannel.java
2024-11-28 13:06:18 +07:00

790 lines
24 KiB
Java

package aas;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.un4seen.bass.BASS;
import com.un4seen.bass.BASS.BASS_DEVICEINFO;
import com.un4seen.bass.BASS.BASS_INFO;
import com.un4seen.bass.BASS.IDSPPROC;
import com.un4seen.bass.BASS.SYNCPROC;
import com.un4seen.bass.BASS.bassconstant;
import com.un4seen.bass.BASSmix;
import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.Bit;
import jbass.Bass_DeviceInfo;
@BA.Events(values = {
"log(msg as string)",
"openudpstreamchannel(success as boolean, localip as string, localport as int, streamhandle as int, channelnumber as int)",
"playbackdevicefailed(deviceid as int)",
"streamingstatus(channel as int, vu as int, bufferspace as int)",
"playbackstatus(channel as int, value as string)"
})
@BA.ShortName("AAS_Receiver_Multichannel")
/**
* AAS Receiver multichannel with 7.1 soundcard
* @author rdkartono
*/
public class AAS_Receiver_Multichannel {
private BA ba;
private String event;
private Object Me = this;
private BASS bass;
private BASSmix bassmix;
private boolean inited = false;
private int deviceid = -1;
private int mixerhandle = 0 ;
private boolean need_log_event = false;
private boolean need_openudpstreamchannel_event = false;
private boolean need_playbackdevicefailed_event = false;
private boolean need_streamingstatus_event = false;
private boolean need_playbackstatus_event = false;
ExecutorService exec = null;
private class streamstatusclass {
public int vu = 0;
public int handle = 0;
public final Integer chnumber;
public DatagramSocket theudp = null;
private ByteBuffer buffer;
public final IDSPPROC channeldsp;
public final SYNCPROC channelstalled;
public final SYNCPROC channelend;
public final SYNCPROC mixerchannelstalled;
public final SYNCPROC mixerchannelend;
public int relaystatus = -1;
public streamstatusclass(int chnumber) {
this.chnumber = chnumber;
buffer = ByteBuffer.allocate(32*1000);
channeldsp = new IDSPPROC() {
@Override
public void DSPPROC(int handle, int channel, Pointer buffer, int length, Pointer user) {
if (buffer!=null) {
if (length>0) {
ByteBuffer bb = buffer.getByteBuffer(0, length);
ShortBuffer sb = bb.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short curvalue=0, maxvalue=0;
while(sb.hasRemaining()) {
curvalue = sb.get();
if (curvalue>maxvalue) maxvalue = curvalue;
}
vu = (int)( (maxvalue * 100.0)/Short.MAX_VALUE );
}
}
}
};
channelstalled = new SYNCPROC() {
@Override
public void SYNCPROC(int handle, int channel, int data, Pointer user) {
switch(data) {
case 0 :
raise_log_event("BASS_ChannelSetSync Channel="+chnumber+" is stalled");
break;
case 1 :
raise_log_event("BASS_ChannelSetSync Channel="+chnumber+" is resumed");
break;
default :
raise_log_event("BASS_ChannelSetSync Channel="+chnumber+" is unknown stall, data="+data);
break;
}
}
};
mixerchannelstalled = new SYNCPROC() {
@Override
public void SYNCPROC(int handle, int channel, int data, Pointer user) {
switch(data) {
case 0 :
raise_log_event("BASS_Mixer_ChannelSetSync Channel="+chnumber+" is stalled");
break;
case 1 :
raise_log_event("BASS_Mixer_ChannelSetSync Channel="+chnumber+" is resumed");
break;
default :
raise_log_event("BASS_Mixer_ChannelSetSync Channel="+chnumber+" is unknown stall, data="+data);
break;
}
}
};
channelend = new SYNCPROC() {
@Override
public void SYNCPROC(int handle, int channel, int data, Pointer user) {
switch(data) {
case 0 :
raise_log_event("BASS_ChannelSetSync channel="+chnumber+" is normal end position");
break;
case 1 :
raise_log_event("BASS_ChannelSetSync channel="+chnumber+" is backward jump in MOD music");
break;
case 2 :
raise_log_event("BASS_ChannelSetSync channel="+chnumber+" is BASS_POS_END position");
break;
case 3 :
raise_log_event("BASS_ChannelSetSync channel="+chnumber+" is end of tail (BASS_ATTRIB_TAIL)");
break;
default :
raise_log_event("BASS_ChannelSetSync channel="+chnumber+" is unknown end, data="+data);
break;
}
}
};
mixerchannelend = new SYNCPROC() {
@Override
public void SYNCPROC(int handle, int channel, int data, Pointer user) {
switch(data) {
case 0 :
raise_log_event("BASS_Mixer_ChannelSetSync channel="+chnumber+" is normal end position");
break;
case 1 :
raise_log_event("BASS_Mixer_ChannelSetSync channel="+chnumber+" is backward jump in MOD music");
break;
case 2 :
raise_log_event("BASS_Mixer_ChannelSetSync channel="+chnumber+" is BASS_POS_END position");
break;
case 3 :
raise_log_event("BASS_Mixer_ChannelSetSync channel="+chnumber+" is end of tail (BASS_ATTRIB_TAIL)");
break;
default :
raise_log_event("BASS_Mixer_ChannelSetSync channel="+chnumber+" is unknown end, data="+data);
break;
}
}
};
}
public int BufferRemaining() {
return buffer.remaining();
}
public void PushData(byte[] bb, int bblen) {
buffer.put(bb,0,bblen);
}
@SuppressWarnings("unused")
public void PushData(byte[] bb) {
buffer.put(bb);
}
@SuppressWarnings("unused")
public byte[] PullData(int length) {
byte[] readdata = new byte[length];
buffer.flip();
buffer.get(readdata);
buffer.compact();
return readdata;
}
public byte[] PullAllData() {
if (BufferPosition()>0) {
buffer.flip();
byte[] readdata = new byte[buffer.remaining()];
buffer.get(readdata);
buffer.compact();
return readdata;
} else return null;
}
public int BufferPosition() {
return buffer.position();
}
@SuppressWarnings("unused")
public boolean Buffer_inWriteMode() {
return buffer.limit()==buffer.capacity() ? true : false;
}
}
private volatile streamstatusclass[] streamingstatus = null;
public AAS_Receiver_Multichannel() {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
BA.Log("AAS_Receiver_Multichannel ShutdownHook");
if (exec!=null) {
exec.shutdown();
try {
exec.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
BA.Log("ExecutorService awaitTermination failed, exception="+e.getMessage());
}
}
CloseDevice();
}
});
}
/**
* Initialize AAS Receiver Multichannel
* @param event eventname
*/
public void Initialize(BA ba, String event) {
this.ba = ba;
this.event = event;
check_events();
bass = new BASS();
bassmix = new BASSmix();
int bassversion = bass.BASS_GetVersion();
int bassmixversion = bassmix.BASS_Mixer_GetVersion();
if (bassversion!=0) {
if (bassmixversion!=0) {
BA.Log("BASS Version="+Bit.ToHexString(bassversion)+", Mixer Version="+Bit.ToHexString(bassmixversion));
inited = true;
exec = Executors.newFixedThreadPool(20);
}
}
}
/**
* Check if already initialized and BASS can be loaded
* @return
*/
public boolean IsInitialized() {
return inited;
}
/**
* Open Playback device
* @param devid device id, 0 = no sound, 1 = first real device, 2 = ....
* @param samplingrate samplingrate
* @return true if can be opened
*/
public boolean OpenDevice(final int devid, final int samplingrate) {
deviceid = -1;
if (inited) {
BASS_DEVICEINFO dvi = new BASS_DEVICEINFO();
if (bass.BASS_GetDeviceInfo(devid, dvi)) {
dvi.read();
Bass_DeviceInfo dv = new Bass_DeviceInfo(devid, dvi);
if (dv.isvalid) {
if (dv.IsEnabled()) {
if (dv.IsInited()) {
// sudah initialized, free dulu
bass.BASS_SetDevice(devid);
bass.BASS_Free();
}
int flags = bassconstant.BASS_DEVICE_16BITS | bassconstant.BASS_DEVICE_SPEAKERS | bassconstant.BASS_DEVICE_FREQ;
if (bass.BASS_Init(devid, samplingrate, flags)) {
this.deviceid = devid;
BASS_INFO bi = new BASS_INFO();
if (bass.BASS_GetInfo(bi)) {
bi.read();
streamingstatus = new streamstatusclass[bi.speakers];
for(int ii=0;ii<streamingstatus.length;ii++) streamingstatus[ii] = new streamstatusclass(ii);
} else raise_log_event("BASS_GetInfo failed, error="+bass.GetBassErrorString());
// bassmix.BASS_MIXER_NONSTOP
int mixerflag = 0;
int mh = bassmix.BASS_Mixer_StreamCreate(samplingrate, 8, mixerflag);
if (mh!=0) {
if (bass.BASS_ChannelPlay(mh, true)) {
this.mixerhandle = mh;
bass.BASS_ChannelSetSync(mh, bassconstant.BASS_SYNC_DEV_FAIL, 0, mixerdevfail, null);
bass.BASS_ChannelSetSync(mh, bassconstant.BASS_SYNC_STALL, 0, mixerstalled, null);
bass.BASS_ChannelSetSync(mh, bassconstant.BASS_SYNC_END, 0, mixerend, null);
// pancingan
exec.submit(new raisestreamingstatusrunnable());
return true;
} else raise_log_event("BASS_ChannelPlay mixerhandle failed, error="+bass.GetBassErrorString());
} else raise_log_event("BASS_Mixer_StreamCreate failed, error="+bass.GetBassErrorString());
} else raise_log_event("BASS_Init device="+devid+" failed, error="+bass.GetBassErrorString());
} else raise_log_event("OpenDevice device="+devid+" failed, Device is disabled");
} else raise_log_event("DeviceInfo for device="+devid+" is invalid");
} else raise_log_event("BASS_GetDeviceInfo device="+devid+" failed, error="+bass.GetBassErrorString());
} else raise_log_event("Call Initialize first");
return false;
}
private SYNCPROC mixerdevfail = new SYNCPROC() {
@Override
public void SYNCPROC(int handle, int channel, int data, Pointer user) {
raise_playbackdevicefailed_event(deviceid);
}
};
private SYNCPROC mixerend = new SYNCPROC() {
@Override
public void SYNCPROC(int handle, int channel, int data, Pointer user) {
raise_log_event("Mixer reached END");
}
};
private SYNCPROC mixerstalled = new SYNCPROC() {
@Override
public void SYNCPROC(int handle, int channel, int data, Pointer user) {
switch(data) {
case 0 :
raise_playbackstatus_event(100,"stalled");
break;
case 1 :
raise_playbackstatus_event(100,"resumed");
break;
}
}
};
/**
* Get Opened Playback Device ID
* @return -1 if not opened
*/
public int getDeviceID() {
return deviceid;
}
/**
* Close Playback Device
*/
public void CloseDevice() {
if (deviceid!=-1) {
if (inited) {
if (mixerhandle!=0) {
bass.BASS_StreamFree(mixerhandle);
}
mixerhandle = 0;
bass.BASS_SetDevice(deviceid);
bass.BASS_Free();
}
}
deviceid = -1;
}
/**
* Close UDP Streaming for channel
* @param channelnumber 0 - 7
* @return true if success
*/
public boolean Close_UDP_StreamChannel(int channelnumber) {
if (inited) {
if (deviceid!=-1) {
streamstatusclass sc = GetStreamingStatusMember(channelnumber);
if (sc!=null) {
if (sc.theudp!=null) {
if (sc.theudp.isClosed()==false) {
sc.theudp.close();
raise_log_event("Close_UDP_StreamChannel, closing UDP for channel="+sc.chnumber);
}
sc.theudp = null;
}
if (sc.handle!=0) {
if (!bassmix.BASS_Mixer_ChannelRemove(sc.handle)) {
raise_log_event("Close_UDP_StreamChannel BASS_Mixer_ChannelRemove failed for channel="+sc.chnumber+", error="+bass.GetBassErrorString());
}
if (!bass.BASS_ChannelStop(sc.handle)) {
raise_log_event("Close_UDP_StreamChannel BASS_ChannelStop failed for channel="+sc.chnumber+", error="+bass.GetBassErrorString());
}
if (!bass.BASS_StreamFree(sc.handle)) {
raise_log_event("Close_UDP_StreamChannel BASS_StreamFree failed for channel="+sc.chnumber+", error="+bass.GetBassErrorString());
}
sc.handle = 0;
}
return true;
}
}
}
return false;
}
/**
* Open UDP Streaming , linked with Soundcard channel
* will raise event openudpstreamchannel(success as boolean, localip as string, localport as int, streamhandle as int, channelnumber as int)
* @param samplingrate samplingrate
* @param localip valid IP address to bind
* @param localport valid port
* @param channelnumber 0 - (detectedspeakers-1)
* @return true if DatagramSocket can be created
*/
public boolean Open_UDP_StreamChannel(int samplingrate, String localip, int localport, int channelnumber) {
if (inited) {
if (deviceid!=-1) {
streamstatusclass sc = GetStreamingStatusMember(channelnumber);
if (sc!=null) {
DatagramSocket udp = null;
// coba open UDP listen
try {
udp = new DatagramSocket(new InetSocketAddress(localip, localport));
} catch (SocketException e) {
raise_log_event("Unable to create DatagramSocket, exception="+e.getMessage());
}
if (udp != null) {
// berhasil UDP listen
// mesti bikin final supaya bisa masuk ba.submitrunnable
sc.theudp = udp;
// runnable untuk ambil data dari udp;
exec.submit(new udprunnable(channelnumber));
int flag = bassconstant.BASS_STREAM_DECODE;
int handle = bass.BASS_StreamCreate(samplingrate, 1, flag, bassconstant.STREAMPROC_PUSH, null);
if (handle!=0) {
sc.handle = handle;
bass.BASS_ChannelSetAttribute(handle,bassconstant.BASS_ATTRIB_PUSH_LIMIT , 0);
bass.BASS_ChannelSetDSP(handle, sc.channeldsp, null, 0);
bass.BASS_ChannelSetSync(handle, bassconstant.BASS_SYNC_END, 0, sc.channelend, null);
bass.BASS_ChannelSetSync(handle, bassconstant.BASS_SYNC_STALL, 0, sc.channelstalled, null);
raise_openudpstreamchannel_event(true, sc.theudp.getLocalAddress().getHostAddress(), sc.theudp.getLocalPort(), sc.handle, sc.chnumber);
exec.submit(new streamputdatarunnable(channelnumber));
}else {
// gagal buka bass stream
raise_log_event("BASS_StreamCreate failed, error="+bass.GetBassErrorString());
if (sc.theudp != null) {
raise_openudpstreamchannel_event(false, sc.theudp.getLocalAddress().getHostAddress(), sc.theudp.getLocalPort(),0, sc.chnumber);
if (sc.theudp.isClosed()==false) {
sc.theudp.close();
}
sc.theudp = null;
}
}
return true;
}
}
} else raise_log_event("Call OpenDevice first");
} else raise_log_event("Call Initialize first");
return false;
}
private streamstatusclass GetStreamingStatusMember(int index) {
if (streamingstatus != null) {
if (index>=0) {
if (index<streamingstatus.length) {
return streamingstatus[index];
}
}
}
return null;
}
// Runnable untuk raise_streamingstatus_event periodik
private class raisestreamingstatusrunnable implements Runnable{
boolean keeprunning = true;
public raisestreamingstatusrunnable() {
}
@Override
public void run() {
keeprunning = true;
raise_log_event("raisestreamingstatusrunnable started");
while(keeprunning) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
keeprunning = false;
}
for(streamstatusclass ssc : streamingstatus) {
raise_streamingstatus_event(ssc.chnumber, ssc.vu, ssc.BufferRemaining());
}
}
raise_log_event("raisestreamingstatusrunnable finished");
}
}
private class streamputdatarunnable implements Runnable{
private final int channelnumber;
private final streamstatusclass ssc;
private int mixchanflag = 0;
private boolean keeprunning = false;
public streamputdatarunnable(int chnum) {
channelnumber = chnum;
ssc = GetStreamingStatusMember(channelnumber);
}
@Override
public void run() {
if (ssc!=null) {
final long delay = bass.BASS_ChannelSeconds2Bytes(ssc.handle, 2);
raise_log_event("Runnable StreamPutData started for channel="+ssc.chnumber+", will delay "+delay+" bytes");
keeprunning = true;
while(keeprunning) {
synchronized(ssc) {
if (ssc.handle==0) {
raise_log_event("ssc.handle=0 for channel="+ssc.chnumber+", breaking now");
keeprunning = false;
}
if (ssc.BufferPosition()==0) {
try {
ssc.wait(3000);
} catch (InterruptedException e) {
raise_log_event("InterruptException on buffer waiting for channel="+ssc.chnumber);
keeprunning = false;
}
}
// sampe sini , harusnya ada data, kalau gak ada, selesai aja
byte[] dataread = ssc.PullAllData();
if (dataread!=null) {
int rem = dataread.length;
Pointer bb = new Memory(rem);
bb.write(0, dataread, 0, rem);
int qq = bass.BASS_StreamPutData(ssc.handle, bb, rem);
if (ssc.relaystatus!=1) {
ssc.relaystatus = 1;
raise_playbackstatus_event(ssc.chnumber, "resumed");
}
mixchanflag = bassmix.BASS_Mixer_ChannelFlags(ssc.handle, 0, 0);
if ((mixchanflag & bassmix.BASS_MIXER_CHAN_PAUSE)>0) {
if (qq>delay) {
raise_log_event("Resuming mixer for channel="+ssc.chnumber);
bassmix.BASS_Mixer_ChannelFlags(ssc.handle, 0, bassmix.BASS_MIXER_CHAN_PAUSE);
}
}
} else {
ssc.vu = 0;
if (ssc.relaystatus!=0) {
ssc.relaystatus = 0;
raise_playbackstatus_event(ssc.chnumber,"stalled");
//bass.BASS_StreamPutData(ssc.handle, null, bassconstant.BASS_STREAMPROC_END); // bikin gak bisa streaming
raise_log_event("Pausing mixer for channel="+ssc.chnumber);
bassmix.BASS_Mixer_ChannelFlags(ssc.handle, bassmix.BASS_MIXER_CHAN_PAUSE, bassmix.BASS_MIXER_CHAN_PAUSE);
}
}
}
}
if (ssc!=null) {
raise_log_event("Runnable StreamPutData finished for channel="+ssc.chnumber);
}
}
}
}
private class udprunnable implements Runnable{
private final int channelnumber;
private final streamstatusclass ssc;
private boolean keeprunning = false;
public udprunnable(int chnum) {
channelnumber = chnum;
ssc = GetStreamingStatusMember(channelnumber);
}
@Override
public void run() {
if (ssc!=null) {
raise_log_event("Runnable UDP Receive started for channel="+ssc.chnumber);
keeprunning = true;
while(keeprunning) {
if (ssc.theudp!=null && ssc.theudp.isClosed()==false) {
try {
// ngeblok di sini sampe dapat paket
DatagramPacket pkg = new DatagramPacket(new byte[1500],1500);
ssc.theudp.receive(pkg);
if (pkg!=null) {
int length = pkg.getLength();
byte[] data = pkg.getData();
if (ssc!=null) {
synchronized(ssc) {
ssc.PushData(data, length);
ssc.notify();
}
};
}
} catch (IOException e) {
raise_log_event("IOException dari theudp, exception="+e.getMessage());
ssc.theudp.close();
ssc.theudp = null;
keeprunning = false;
}
} else {
raise_log_event("theudp Runnable must break, theudp isclosed");
keeprunning = false;
}
}
raise_log_event("Runnable UDP Receive finished for channel="+ssc.chnumber);
}
}
}
/**
* Play handle
* Obtain handle from event openudpstreamchannel
* @param handle
* @param channelnumber 0 - 7
* @return true if can be played
*/
public boolean PlayHandle(int handle, int channelnumber) {
if (inited) {
if (mixerhandle!=0) {
if (handle != 0) {
int flag = bassmix.BASS_MIXER_CHAN_PAUSE;
switch(channelnumber) {
case 1 :
flag |= bassconstant.BASS_SPEAKER_FRONTRIGHT;
break;
case 2:
flag |= bassconstant.BASS_SPEAKER_REARLEFT;
break;
case 3:
flag |= bassconstant.BASS_SPEAKER_REARRIGHT;
break;
case 4:
flag |= bassconstant.BASS_SPEAKER_REAR2LEFT;
break;
case 5:
flag |= bassconstant.BASS_SPEAKER_REAR2RIGHT;
break;
case 6:
flag |= bassconstant.BASS_SPEAKER_CENTER;
break;
case 7:
flag |= bassconstant.BASS_SPEAKER_LFE;
break;
default:
flag |= bassconstant.BASS_SPEAKER_FRONTLEFT;
break;
}
if (bassmix.BASS_Mixer_StreamAddChannel(mixerhandle, handle, flag)) {
streamstatusclass ssc = GetStreamingStatusMember(channelnumber);
if (ssc!=null) {
bassmix.BASS_Mixer_ChannelSetSync(handle, bassconstant.BASS_SYNC_STALL, 0, ssc.mixerchannelstalled, null);
bassmix.BASS_Mixer_ChannelSetSync(handle, bassconstant.BASS_SYNC_END, 0, ssc.mixerchannelend, null);
}
return true;
} else raise_log_event("BASS_Mixer_StreamAddChannel failed , error="+bass.GetBassErrorString());
} else raise_log_event("Invalid handle, obtain handle from event openudpstreamchannel");
} else raise_log_event("Call OpenDevice first");
} else raise_log_event("Call Initialize first");
return false;
}
/**
* Stop playing handle, and remove it
* @param handle
* @return true if can be done
*/
public boolean StopHandle(int handle) {
if (inited) {
if (mixerhandle!=0) {
if (handle!=0) {
if (bassmix.BASS_Mixer_ChannelRemove(handle)) {
if (bass.BASS_StreamFree(handle)) {
raise_log_event("StopHandle handle="+handle+" succcess");
return true;
} else raise_log_event("BASS_StreamFree failed, error="+bass.GetBassErrorString());
} else raise_log_event("BASS_Mixer_ChannelRemove failed, error="+bass.GetBassErrorString());
} else raise_log_event("Invalid handle, obtain handle from event openudpstreamchannel");
} else raise_log_event("Call OpenDevice first");
} else raise_log_event("Call Initialize first");
return false;
}
private void check_events() {
need_log_event = ba.subExists(event+"_log");
need_openudpstreamchannel_event = ba.subExists(event+"_openudpstreamchannel");
need_playbackdevicefailed_event = ba.subExists(event+"_playbackdevicefailed");
need_streamingstatus_event = ba.subExists(event+"_streamingstatus");
need_playbackstatus_event = ba.subExists(event+"_playbackstatus");
}
private void raise_log_event(String msg) {
if (need_log_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
}
private void raise_openudpstreamchannel_event(boolean success, String localip, int localport, int streamhandle, int channelnumber) {
if (need_openudpstreamchannel_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_openudpstreamchannel", false, new Object[] {success, localip, localport, streamhandle, channelnumber});
}
private void raise_playbackdevicefailed_event(int deviceid) {
if (need_playbackdevicefailed_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_playbackdevicefailed", false, new Object[] {deviceid});
}
private void raise_streamingstatus_event(int channelnumber, int vu, int buffferspace) {
if (need_streamingstatus_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_streamingstatus", false, new Object[] {channelnumber, vu, buffferspace});
}
private void raise_playbackstatus_event(int channelnumber, String value) {
if (need_playbackstatus_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_playbackstatus", false, new Object[] {channelnumber, value});
}
}