package Audio;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Stream;

import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.Native;

import Audio.Bass.BASS_DEVICEINFO;
import org.pmw.tinylog.Logger;


/**
 * SIP Audio Converter
 * @author rdkartono
 *
 */
@SuppressWarnings({"unused", "ResultOfMethodCallIgnored"})
public class JSIPAudio {
	private final Bass BASS = Bass.Instance;
	private final BassMix BASSMIX = BassMix.Instance;
	private final BassEnc BASSENC = BassEnc.Instance;
	private final BassEncAAC BASSENC_AAC = BassEncAAC.Instance;
	private final String currentfolder ;

	private boolean inited = false;

	private final ExecutorService exec = Executors.newCachedThreadPool();
	private final Set<Integer> inited_playback ;
	private final Set<Integer> inited_recorder ;
	private final Set<Integer> loaded_plugin;

	public ULaw ULaw;
	public PCM PCM16;
	public AAC AAC;

	public JSIPAudio(AudioEvent1 ulaw_event, AudioEvent1 pcm16_event, AudioEvent1 aac_event) {
		currentfolder = new File("").getAbsolutePath();
		
		inited_playback = new HashSet<>();
		inited_recorder = new HashSet<>();
		loaded_plugin = new HashSet<>();

		extract_libraries();

		ULaw = new ULaw(16,1,ulaw_event);
		PCM16 = new PCM(16,1, pcm16_event);
		AAC = new AAC(16,1, aac_event);

		Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            Logger.info("JSIPAudio ShutdownHook called");
            inited_recorder.forEach(recdev->{
                if (BASS.BASS_RecordSetDevice(recdev)) {
                    if (BASS.BASS_RecordFree()) {
                        Logger.info("Recorder Device="+recdev+" freed");
                    } else Logger.error(BASS.GetBassError("BASS_RecordFree"));
                } else Logger.error(BASS.GetBassError("BASS_RecordSetDevice"));

            });
            inited_recorder.clear();

            inited_playback.forEach(playdev ->{
                if (BASS.BASS_SetDevice(playdev)) {
                    if (BASS.BASS_Free()) {
                        Logger.info("Playback Device="+playdev+" freed");
                    } else Logger.error(BASS.GetBassError("BASS_Free"));
                } else Logger.error(BASS.GetBassError("BASS_SetDevice"));

            });
            inited_playback.clear();

            loaded_plugin.forEach(ph ->{
                if (BASS.BASS_PluginFree(ph)) {
                    Logger.info("Plugin freed");
                } else Logger.error(BASS.GetBassError("BASS_PluginFree"));
            });
            loaded_plugin.clear();
        }));
		inited = true;
	}

	private String extract_library(String folder, String libname){
		try {
			File existinglib = new File(folder, System.mapLibraryName(libname));
			File xx = Native.extractFromResourcePath(libname);

			if (existinglib.exists() && existinglib.isFile() && existinglib.length()==xx.length()) {
				xx.delete();	// gak perlu
				return existinglib.getAbsolutePath();
			} else {
				if (xx.renameTo(existinglib)) {
					return existinglib.getAbsolutePath();
				} else return xx.getAbsolutePath();

			}
		} catch (IOException e) {
			return MessageFormat.format("IOException Libname={0} Message={1}", libname, e.getMessage());
		}
	}

	private String ToHexString(int value) {
		return "0x"+Integer.toHexString(value).toUpperCase();
	}

	
	private void extract_libraries() {
		raise_log(extract_library(currentfolder, "bass"));
		raise_log(extract_library(currentfolder, "bassmix"));
		raise_log(extract_library(currentfolder, "bass_aac"));
		raise_log(extract_library(currentfolder, "bassenc"));
		raise_log(extract_library(currentfolder, "bassenc_aac"));

		raise_log("Bass Version = "+ToHexString(BASS.BASS_GetVersion()));
		raise_log("BassMix Version = "+ToHexString(BASSMIX.BASS_Mixer_GetVersion()));
		raise_log("BassEnc Version = "+ToHexString(BASSENC.BASS_Encode_GetVersion()));
		raise_log("BassEnc_AAC Version = "+ToHexString(BASSENC_AAC.BASS_Encode_AAC_GetVersion()));
	
		int pluginhandle = BASS.BASS_PluginLoad("bass_aac", 0);
		if (pluginhandle!=0) {
			loaded_plugin.add(pluginhandle);
			raise_log("Plugin AAC Loaded");
		} else raise_log(BASS.GetBassError("BASS_PluginLoad"));
	}
	
	
	
	/**
	 * Set Channel Volume
	 * @param handle handle value
	 * @param value 0 - 100
	 */
	public void SetChannelVolume(int handle, int value) {
		if (value<0) value = 0;
		if (value>100) value = 100;
		if (handle!=0) {
			if (!BASS.BASS_ChannelSetAttribute(handle, Bass.BASS_ATTRIB_VOL, value/100.0f)) {
				raise_log(BASS.GetBassError("BASS_ChannelSetAttribute"));
			}
			
		}
	}
	
	/**
	 * Get Channel Volume
	 * @param handle handle value
	 * @return -1 if failed, 0 - 100 if success
	 */
	public int GetChannelVolume(int handle) {
		if (handle!=0) {
			Bass.FloatValue val = new Bass.FloatValue();
			if (BASS.BASS_ChannelGetAttribute(handle, Bass.BASS_ATTRIB_VOL, val)) {
				int intval = (int)(val.value*100);
				if (intval<0) intval = 0;
				if (intval>100) intval = 100;
				return intval;
			}
		}
		return -1;
	}
	
	/**
	 * Set Master Volume for specific Playback Device
	 * @param devid start from 1. No-sound cant be controlled
	 * @param value 0 - 100
	 * @return true if SetVolume success
	 */
	public boolean SetMasterVolume(int devid, int value) {
		if (value<0) value = 0;
		if (value>100) value = 100;
		if (devid>0) {
			if (BASS.BASS_SetDevice(devid)) {
				if (BASS.BASS_SetVolume(value/100.0f)) {
					return true;
				} else raise_log(BASS.GetBassError("BASS_SetVolume"));
			} else raise_log(BASS.GetBassError("BASS_SetDevice"));
		}
		return false;
	}
	
	/**
	 * Get Master Volume for specific Playback Device
	 * @param devid start from 1
	 * @return -1 if GetVolume failed
	 */
	public int GetMasterVolume(int devid) {
		if (devid>0) {
			if (BASS.BASS_SetDevice(devid)) {
				float val = BASS.BASS_GetVolume();
				if (val>=0) {
					int result = (int)(val * 100);
					if (result>100) result = 100;
					return result;
				} else raise_log(BASS.GetBassError("BASS_GetVolume"));
			} else raise_log(BASS.GetBassError("BASS_SetDevice"));
		}
		return -1;
	}
	
	/**
	 * Set Record Volume
	 * @param devid start from 0
	 * @param value 0 - 100
	 * @return true if success
	 */
	public boolean SetRecordVolume(int devid, int value) {
		// value negative = leave current
		// value = 0 = silent
		// value = 100 = max
		if (value<-1) value = -1;	
		if (value>100) value = 100;
		if (BASS.BASS_RecordSetInput(devid, Bass.BASS_INPUT_ON, value/100.0f)) {
			return true;
		} else {
			raise_log(BASS.GetBassError("BASS_RecordSetInput"));
			return false;
		}
	}
	
	/**
	 * Get Record Volume
	 * @param devid start from 0
	 * @return -1 if failed, 0 - 100 if success
	 */
	public int GetRecordVolume(int devid) {
		Bass.FloatValue val = new Bass.FloatValue();
		int result = BASS.BASS_RecordGetInput(devid, val);
		if (result==-1) {
			// error
			raise_log(BASS.GetBassError("BASS_RecordGetInput"));
			return -1;
		} else {
            return (int)(val.value * 100);
		}
	}
	
	/**
	 * Check if already initialized
	 * @return true if initialized
	 */
	public boolean IsInitialized() {
		return inited;
	}
	
	/**
	 * Get all Playback Devices
	 * @return array of BASS_DEVICEINFO
	 */
	public BASS_DEVICEINFO[] GetPlaybackDevices() {
		int ii = 0;
		boolean success;
		List<BASS_DEVICEINFO> result = new ArrayList<>();
		do {
			BASS_DEVICEINFO dev = new BASS_DEVICEINFO();
			success = BASS.BASS_GetDeviceInfo(ii, dev);
			if (success) {
				ii++;
				result.add(dev);
			}
		} while(success);
		return result.toArray(new BASS_DEVICEINFO[0]);
	}
	
	/**
	 * Get All Recorder Devices
	 * @return array of BASS_DEVICEINFO
	 */
	public BASS_DEVICEINFO[] GetRecorderDevices() {
		int ii = 0;
		boolean success;
		List<BASS_DEVICEINFO> result = new ArrayList<>();
		do {
			BASS_DEVICEINFO dev = new BASS_DEVICEINFO();
			success = BASS.BASS_RecordGetDeviceInfo(ii, dev);
			if (success) {
				ii++;
				result.add(dev);
			}
			
		} while(success);
		return result.toArray(new BASS_DEVICEINFO[0]);
	}
	
	/**
	 * Get Playback Device Info
	 * @param devid 0 = no sound, 1 = first device
	 * @return BASS_DEVICEINFO object, or null if failed
	 */
	public BASS_DEVICEINFO GetPlaybackDeviceInformation(int devid) {
		BASS_DEVICEINFO dev = new BASS_DEVICEINFO();
		if (BASS.BASS_GetDeviceInfo(devid, dev)) {
			return dev;
		} else raise_log(BASS.GetBassError("BASS_GetDeviceInfo"));
		return null;
	}
	
	/**
	 * Get Recorder Device Info
	 * @param devid 0 = first device
	 * @return BASS_DEVICEINFO object, or null if failed
	 */
	public BASS_DEVICEINFO GetRecorderDeviceInformation(int devid) {
		BASS_DEVICEINFO dev = new BASS_DEVICEINFO();
		if (BASS.BASS_RecordGetDeviceInfo(devid, dev)) {
			return dev;
		} else raise_log(BASS.GetBassError("BASS_RecordGetDeviceInfo"));
		return null;
	}
	
	private void raise_log(String msg) {
		Logger.info(msg);
	}
	
	@SuppressWarnings("BusyWait")
    public class ULaw extends basicfunctions{
		public final int[] samplingrate = {8000};
		public final int channel;
		public final int bits;
		public final AudioEvent1 event;
		public ULaw(int bits, int channel, AudioEvent1 event) {
			super(48000, Bass.BASS_DEVICE_MONO | Bass.BASS_DEVICE_FREQ);
			this.channel = channel;
			this.bits = bits;
			this.event = event;
		}

		/**
		 * Convert PCM 16bit Bytes to ULaw 
		 * @param pcm PCM Data
		 * @return ULaw data
		 */
		private byte[] ConvertPCMToULaw(byte[] pcm) {
			//TODO check di sini, harusnya readcount = pcm.length / 2
			// karena PCM 16 bit, ULaw = 8 bit
			// tapi gak tau juga, belum cek
			int readcount = pcm.length;
			byte[] cc = new byte[readcount];
			
			ShortBuffer sb = ByteBuffer.wrap(pcm).asShortBuffer();
			ShortBuffer sc = ByteBuffer.wrap(cc).asShortBuffer();
			
			// sampe sini dapat PCM nya , convert ke ULaw
			
			// convert dari PCM to ULaw
			while(sb.hasRemaining()) {
				sc.put((short)G711.linear2ulaw(sb.get()));
			}
			return cc;
		}
		
		/**
		 * Convert file from source to target, in G711 U-Law format
		 * will raise event ulaw_fileconvertfinish(sourcefile , targetfile) when finished
		 * @param sourcefilename Source File
		 * @param targetsamplingrate target sampling rate, only 8000 is supported
		 * @param targetfilename Target File
		 * @return true if convesion can be started
		 */
		@Override
		public boolean FileConvert(final String sourcefilename, int targetsamplingrate, final String targetfilename) {
			targetsamplingrate = samplingrate[0];
			OpenFile op = new OpenFile(sourcefilename, 0, targetsamplingrate, true);
			
			if (op.isSuccess()) {
				// sampe sini sudah plugged ke Mixer
				inited_playback.add(0);
				
				try(FileOutputStream fos = new FileOutputStream(targetfilename)){
					exec.submit(()->{
						long timestamp = System.currentTimeMillis();
						int totalread = 0;
						int readcount;
						
						try {
							do {
								Pointer ptr = new Memory(1024);
								readcount = BASS.BASS_ChannelGetData(op.getMixerhandle(), ptr, 1024);
								if (readcount>0) {
									byte[] bb = ptr.getByteArray(0, readcount);
									byte[] cc = ConvertPCMToULaw(bb);
									
									totalread+=readcount;
									fos.write(cc);
									
								}
							} while(readcount>0);
							
							fos.close();
						} catch(IOException e) {
							raise_log(MessageFormat.format("FileOutputStream {0} IOException={1}", targetfilename, e.getMessage()));
						}
						
						// sampe sini selesai
						long delta = System.currentTimeMillis() - timestamp;
						raise_log(MessageFormat.format("ULaw FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta));
						if (event!=null) event.fileconvertfinish(sourcefilename, targetfilename);
					});
					return true;
				} catch(IOException e) {
					raise_log("FileOutputStream create exception="+e.getMessage());
					return false;
				} finally {
					String[] closeerror = op.CloseHandles();
					Stream.of(closeerror).forEach(JSIPAudio.this::raise_log);
				}
				
				
			} else {
				Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
				return false;
			}
			
			
			
			
		}
		
		/**
		 * Open File for streaming
		 * will raise event ulaw_filestreaming
		 * @param playdevice 0 = no sound, 1 = first audio
		 * @param sourcefilename Source File to stream
		 * @param targetsamplingrate target sampling rate, only 8000 is supported
		 * @return 0 if failed, or other value if success
		 */
		@Override
		public int OpenFileForStreaming(final int playdevice, final String sourcefilename, int targetsamplingrate) {
			targetsamplingrate = samplingrate[0];
			
			OpenFile op = new OpenFile(sourcefilename, playdevice, targetsamplingrate);
			if (op.isSuccess()) {
				inited_playback.add(playdevice);
				// sampe sini , channel sudah di add
				if (op.StartPlayback()) {
					exec.submit(()->{
						
						int dsphandle = BASS.BASS_ChannelSetDSP(op.getMixerhandle(), (dsp_handle, channel_handle, buffer, length, user)->{
							// di sini dapat PCM dari Mixer
							if (length>0) {
								byte[] data = buffer.getByteArray(0, length);
								byte[] ulaw = ConvertPCMToULaw(data);
								if (event!=null) event.filestreaming(sourcefilename, op.getFilehandle(), ulaw);
							}
							
						}, null, 0);
						
						if (dsphandle!=0) {
							// sampe sini , DSP sudah created
							long timestamp = System.currentTimeMillis();
							raise_log("Mixer Playback for "+sourcefilename+" started");
							// tinggal loop aja sampe selesai
							do {
								try {
									Thread.sleep(100);
									//Thread.yield();
								} catch (InterruptedException e) {
									raise_log("Interrupt Exception="+e.getMessage());
								}
							} while(BASS.BASS_ChannelIsActive(op.getFilehandle())== Bass.BASS_ACTIVE_PLAYING);
							
							// finish playback !!
							long delta = System.currentTimeMillis() - timestamp;
							raise_log("Mixer Playback for "+sourcefilename+" finished, duration="+delta+" ms");
						} else raise_log(BASS.GetBassError("BASS_ChannelSetDSP"));
						
						// sampe sini selesai semua
						Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
					});
					
					return op.getFilehandle();
				} else {
					raise_log(BASS.GetBassError("BASS_ChannelStart"));
					Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
					return 0;
				}
				
			} else {
				Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
				return 0;
			}
			
			
			
			
			
		}

		/**
		 * Open Input for streaming
		 * will raise event ulaw_inputstreaming
		 * @param recdevice 0 = first recording device
		 * @param targetsamplingrate target sampling rate, only 8000 is supported
		 * @return 0 if failed, other value if success
		 */
		@Override
		public int OpenInputForStreaming(final int recdevice, int targetsamplingrate) {
			// default ke 8000
			targetsamplingrate = samplingrate[0];
				
			OpenInput oi = new OpenInput(recdevice, targetsamplingrate, (handle, buffer, length, user) -> {

                byte[] data = buffer.getByteArray(0,length);
                byte[] ulaw = ConvertPCMToULaw(data);
                // inputname ada di Pointer user
                if (event!=null) event.inputstreaming(user==null ? "": user.getString(0), handle, ulaw);

                return true;	// terusin RecordProc
            });
			
			if (oi.isSuccess()) {
				inited_recorder.add(oi.getRecorderdev());
				String[] errorstart = oi.StartRecord();
				if (errorstart.length>0) {
					// ada error
					Stream.of(oi.getErrorlist()).forEach(JSIPAudio.this::raise_log);
					Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log);
					return 0;
				} else return oi.getRecordhandle();	// aman
			} else {
				Stream.of(oi.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log);
				return 0;
			}
			
			
			
		}
		
	}
	
	public class PCM extends basicfunctions{
		public PCM(int bits, int channel, AudioEvent1 event) {
			super(48000, Bass.BASS_DEVICE_MONO | Bass.BASS_DEVICE_FREQ);
			
			this.channel = channel;
			this.bits = bits;
			this.MIME  = bits==24 ? "audio/L24" : "audio/L16";
			this.event = event;

		}
		
		public final int[] samplingrate = {16000, 32000, 44100, 48000};
		public final int channel;
		public final int bits;
		public final String MIME;
		public final AudioEvent1 event;
		
		private byte[] Convert_PCM16_to_PCM24(byte[] data) {
			int length16 = data.length;	// length data dalam 16 bit
			int length = length16 / 2;	// jumlah samples nya, yaitu 16bit dibagi 2
			int length24 = length * 3;	// jumlah data dengan samples yang sama, tapi 24 bit
			
			byte[] result = new byte[length24];
			int jj = 0;
			for (int ii=0;ii<length;ii++) {
				result[jj++] = data[2*ii];	// byte pertama
				result[jj++] = data[(2*ii)+1];  // byte kedua
				result[jj++] = 0;			// byte ketiga di skip
				
			}
			
			return result;
		}
		
		@Override
		public boolean FileConvert(String sourcefilename, final int targetsamplingrate, String targetfilename) {
			
			final int samplingfreq = Arrays.stream(samplingrate).filter(xx -> xx==targetsamplingrate).findAny().orElse(samplingrate[0]);
			
			OpenFile op = new OpenFile(sourcefilename, 0, samplingfreq, true);
			if (op.isSuccess()) {
				inited_playback.add(0);
				// sampe sini sudah plugged ke Mixer
				
				try(FileOutputStream fos = new FileOutputStream(targetfilename)){
					
					exec.submit(()->{
						long timestamp = System.currentTimeMillis();
						int totalread = 0;
						int readcount;
						do {
							Pointer ptr = new Memory(1024);
							readcount = BASS.BASS_ChannelGetData(op.getMixerhandle(), ptr, 1024);
							if (readcount>0) {
								// ada data
								byte[] bb = ptr.getByteArray(0, readcount);
								
								// tulis ke FileOutputStream
								try {
									if (bits==24) {
										byte[] cc = Convert_PCM16_to_PCM24(bb);
										fos.write(cc);
										totalread+=cc.length;
									} else {
										fos.write(bb);
										totalread+=bb.length;
									}
									
								} catch (IOException e) {
									raise_log("FileOutputStream write exception="+e.getMessage());
									break;
								}
							}
						} while(readcount>0);
						
						
						// close FileOutputStream
						try {
							fos.close();
						} catch (IOException e) {
							raise_log("FileOutputStream close exception="+e.getMessage());
						}
						
						Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
						
						long delta = System.currentTimeMillis() - timestamp;
						raise_log(MessageFormat.format("FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta));
						if (event!=null) event.fileconvertfinish(sourcefilename, targetfilename);
					});
					return true;
					
					
				} catch(IOException e) {
					raise_log(MessageFormat.format("FileOutputStream {0} Exception={1}", targetfilename, e.getMessage()));
					return false;
				} finally {
					Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
				}
			} else {
				Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
				return false;
			}
			
		}

		@Override
		public int OpenFileForStreaming(int playdevice, final String sourcefilename, int targetsamplingrate) {
			final int samplingfreq = Arrays.stream(samplingrate).filter(xx -> xx==targetsamplingrate).findAny().orElse(samplingrate[0]);
			
			OpenFile op = new OpenFile(sourcefilename, playdevice, samplingfreq);
			if (op.isSuccess()) {
				inited_playback.add(playdevice);
				if (op.StartPlayback()) {
					final int dsphandle = BASS.BASS_ChannelSetDSP(op.getMixerhandle(), (dsp_h, ch_h, buffer,length, user)->{
						// dsp nya disini
						byte[] data = buffer.getByteArray(0, length);
						byte[] cc = Convert_PCM16_to_PCM24(data);
						if (event!=null) event.filestreaming(sourcefilename, op.getFilehandle(),cc);
					}, null, 0);
					
					if (dsphandle==0) {
						raise_log(BASS.GetBassError("BASS_ChannelSetDSP"));
						Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
						return 0;
					}
					
					// sampe sini berhasil
					return op.getFilehandle();
				}
				else {
					raise_log(BASS.GetBassError("BASS_ChannelStart"));
					Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
					return 0;
				}
				
			} else {
				Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
				return 0;
			}
		}

		@Override
		public int OpenInputForStreaming(final int recdevice, final int targetsamplingrate) {
			final int samplingfreq = Arrays.stream(samplingrate).filter(xx -> xx==targetsamplingrate).findAny().orElse(samplingrate[0]);
			
			OpenInput oi = new OpenInput(recdevice, samplingfreq, (handle, buffer, length, user) -> {
                byte[] data = buffer.getByteArray(0, length);
                String recdevname = user==null ? "" : user.getString(0);
                if (bits==24) {
                    if (event!=null) event.inputstreaming(recdevname, handle, Convert_PCM16_to_PCM24(data));
                } else {
                    if (event!=null) event.inputstreaming(recdevname, handle, data);
                }
                return true;
            });
			
			if (oi.isSuccess()) {
				inited_recorder.add(oi.getRecorderdev());
				return oi.getRecordhandle();
			} else {
				Stream.of(oi.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log);
				return 0;
			}	
			
		}
		
	}
	
	@SuppressWarnings("BusyWait")
    public class AAC extends basicfunctions{
		public AAC(int bits, int channel, AudioEvent1 event) {
			super(48000, Bass.BASS_DEVICE_MONO | Bass.BASS_DEVICE_FREQ);
			
			this.bits = bits;
			this.channel = channel;

			this.event = event;
		}
		public final int[] samplingrate = {8000, 16000, 32000, 44100, 48000};
		public final int bits;
		public final int channel;
		public final String MIME = "audio/mpeg4-generic";
		private final AudioEvent1 event;

		
		@Override
		public boolean FileConvert(String sourcefilename, final int targetsamplingrate, String targetfilename) {
			final int samplingfreq = Arrays.stream(samplingrate).filter(xx->xx==targetsamplingrate).findAny().orElse(samplingrate[0]);
			OpenFile op = new OpenFile(sourcefilename, 0, samplingfreq);
			if (op.isSuccess()) {
				inited_playback.add(0);
				// sampe sini sudah plugged ke Mixer
				
				// create encoder handle dari mixer handle
				final int encodehandle = BASSENC_AAC.BASS_Encode_AAC_StartFile(op.getMixerhandle(), null, 0, targetfilename);
				if (encodehandle==0) {
					// gak bisa create encoder
					raise_log(BASS.GetBassError("BASS_Encode_AAC_StartFile"));
					// free handle
					Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
					return false;
				}
				
				// sampe sini encoder jalan
				exec.submit(()->{
					do {
						try {
							Thread.sleep(100);
							//Thread.yield();
						} catch (InterruptedException e) {
							raise_log("InterruptException");
						}
					} while (BASS.BASS_ChannelIsActive(op.getMixerhandle())== Bass.BASS_ACTIVE_PLAYING);
					
					// free encoder
					BASSENC.BASS_Encode_Stop(encodehandle);
					// free handle
					Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
					
					if (event!=null) event.fileconvertfinish(sourcefilename, targetfilename);
					
				});
				return true;
			} else {
				Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
				return false;
			}
			
		}
		
		

		@Override
		public int OpenFileForStreaming(int playdevice, final String sourcefilename, int targetsamplingrate) {
			// cari sampling rate yang supported. kalau gak ketemu, pilih default
			final int samplingfreq = Arrays.stream(samplingrate)
					.filter(xx -> xx == targetsamplingrate)
					.findFirst()
					.orElse(samplingrate[0]);

			OpenFile op = new OpenFile(sourcefilename, playdevice, samplingfreq);
			if (op.isSuccess()) {
				inited_playback.add(playdevice);
				// sampe sini sudah plugged ke Mixer
				if (op.StartPlayback()) {
					
					final int encodehandle = BASSENC_AAC.BASS_Encode_AAC_Start(op.getMixerhandle(), null, 0,
							(encode_handle, source_handle, buffer, length, offset, user)->{
								if (event!=null) event.filestreaming(sourcefilename, op.getFilehandle(), buffer.getByteArray(0,length));
							}
							, null);
					
					if (encodehandle==0) {
						raise_log(BASS.GetBassError("BASS_Encode_AAC_Start"));
						
						// free handle
						Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
						return 0;
					}
					
					exec.submit(()->{
						do {
							try {
								Thread.sleep(100);
							} catch (InterruptedException e) {
								raise_log("InterruptException");
							}
						} while(BASS.BASS_ChannelIsActive(op.getMixerhandle())== Bass.BASS_ACTIVE_PLAYING);
						
						BASSENC.BASS_Encode_Stop(encodehandle);
						
						// free handle
						Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
						
					});
					
					return op.getFilehandle();
				} else {
					raise_log(BASS.GetBassError("BASS_ChannelStart"));
					Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
					return 0;
				}
				
				
			} else {
				Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log);
				return 0;
			}
			
			
		}

		@Override
		public int OpenInputForStreaming(int recdevice, final int targetsamplingrate) {
			final int samplingfreq = Arrays.stream(samplingrate).filter(xx -> xx==targetsamplingrate).findAny().orElse(samplingrate[0]);
			
			OpenInput oi = new OpenInput(recdevice, samplingfreq,null);
			if (oi.isSuccess()) {
				inited_recorder.add(oi.getRecorderdev());
				// sampe sini bisa recording
				final int encodehandle = BASSENC_AAC.BASS_Encode_AAC_Start(oi.getRecordhandle(), null, 0, (encode_handle, source_handle, buffer, length, offset, user)->{
					byte[] data = buffer.getByteArray(0, length);
					if (event!=null) event.inputstreaming(oi.getDeviceName(), source_handle, data);
				}, null);
				
				if (encodehandle!=0) {
					return oi.getRecordhandle();
				} else {
					raise_log(BASS.GetBassError("BASS_Encode_AAC_Start"));
					Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log);
				}
				
			} else {
				Stream.of(oi.getErrorlist()).forEach(JSIPAudio.this::raise_log);
				Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log);
				
			}
			
			return 0;
			
			
		}
		
	}
}
