package bass;
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 anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.Bit;
import anywheresoftware.b4a.keywords.DateTime;
import bass.BassLibrary.BASS_DEVICEINFO;
import bass.BassLibrary.FloatValue;
import bass.BassLibrary.RECORDPROC;
import code.common;



/**
 * SIP Audio Converter
 * @author rdkartono
 *
 */
@BA.ShortName("JSIPAudio")
@BA.Events(values= {
		"log(msg as string)",
		"fileconvertfinish(sourcefile as string, targetfile as string)",
		"filestreaming(filename as string, handle as int,  data() as byte)",
		"inputstreaming(inputname as string, handle as int, data() as byte)"
		
})
public class JSIPAudio {
	
	public JSIPAudio() {
		Me = this;
		currentfolder = new File("").getAbsolutePath();
		
		inited_playback = new HashSet<Integer>();
		inited_recorder = new HashSet<Integer>();
		loaded_plugin = new HashSet<Integer>();
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				System.out.println("JSIPAudio ShutdownHook called");
				inited_recorder.forEach(recdev->{
					if (BassLibrary.BASS.BASS_RecordSetDevice(recdev)) {
						if (BassLibrary.BASS.BASS_RecordFree()) {
							System.out.println("Recorder Device="+recdev+" freed");
						} else System.out.println(BassLibrary.BASS.GetBassError("BASS_RecordFree"));
					} else System.out.println(BassLibrary.BASS.GetBassError("BASS_RecordSetDevice"));
					
				});
				inited_recorder.clear();
				
				inited_playback.forEach(playdev ->{
					if (BassLibrary.BASS.BASS_SetDevice(playdev)) {
						if (BassLibrary.BASS.BASS_Free()) {
							System.out.println("Playback Device="+playdev+" freed");
						} else System.out.println(BassLibrary.BASS.GetBassError("BASS_Free"));
					} else System.out.println(BassLibrary.BASS.GetBassError("BASS_SetDevice"));
					
				});
				inited_playback.clear();
				
				loaded_plugin.forEach(ph ->{
					if (BassLibrary.BASS.BASS_PluginFree(ph)) {
						System.out.println("Plugin freed");
					} else System.out.println(BassLibrary.BASS.GetBassError("BASS_PluginFree"));
				});
				loaded_plugin.clear();
			}
		});
	}
	private final String currentfolder ;
	
	private BA ba;
	private String event;
	private final Object Me;
	private boolean need_log_event = false;
	
	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 PCM LPCM24;
	public AAC AAC;
	
		
	/**
	 * Initialize JSIPAudio
	 * @param eventname Event Name
	 */
	public void Initialize(BA ba, String eventname) {
		this.ba = ba;
		this.event = eventname;
		check_b4x_events();
		extract_libraries();
		
		ULaw = new ULaw(16,1);
		PCM16 = new PCM(16,1);
		AAC = new AAC(16,1);
		LPCM24 = new PCM(24,1);
		
		inited = true;
		
	}
	
	/**
	 * Check for B4X events requirement
	 */
	private void check_b4x_events() {
		if (this.ba!=null) {
			if (common.ValidString(this.event)) {
				need_log_event = this.ba.subExists(event+"_log");
				
			}
		}	
	}
	
	private void extract_libraries() {
		raise_log(common.extract_library(currentfolder, "bass"));
		raise_log(common.extract_library(currentfolder, "bassmix"));
		raise_log(common.extract_library(currentfolder, "bass_aac"));
		raise_log(common.extract_library(currentfolder, "bassenc"));
		raise_log(common.extract_library(currentfolder, "bassenc_aac"));
		
		raise_log("Bass Version = "+Bit.ToHexString(BassLibrary.BASS.BASS_GetVersion()));
		raise_log("BassMix Version = "+Bit.ToHexString(BassMixLibrary.BASSMIX.BASS_Mixer_GetVersion()));
		raise_log("BassEnc Version = "+Bit.ToHexString(BassEncLibrary.BASSENC.BASS_Encode_GetVersion()));
		raise_log("BassEnc_AAC Version = "+Bit.ToHexString(BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_GetVersion()));
	
		int pluginhandle = BassLibrary.BASS.BASS_PluginLoad("bass_aac", 0);
		if (pluginhandle!=0) {
			loaded_plugin.add(pluginhandle);
			raise_log("Plugin AAC Loaded");
		} else raise_log(BassLibrary.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 (!BassLibrary.BASS.BASS_ChannelSetAttribute(handle, BassLibrary.Constant.BASS_ATTRIB_VOL, value/100.0f)) {
				raise_log(BassLibrary.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) {
			FloatValue val = new FloatValue();
			if (BassLibrary.BASS.BASS_ChannelGetAttribute(handle, BassLibrary.Constant.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 (BassLibrary.BASS.BASS_SetDevice(devid)) {
				if (BassLibrary.BASS.BASS_SetVolume(value/100.0f)) {
					return true;
				} else raise_log(BassLibrary.BASS.GetBassError("BASS_SetVolume"));
			} else raise_log(BassLibrary.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 (BassLibrary.BASS.BASS_SetDevice(devid)) {
				float val = BassLibrary.BASS.BASS_GetVolume();
				if (val>=0) {
					int result = (int)(val * 100);
					if (result>100) result = 100;
					return result;
				} else raise_log(BassLibrary.BASS.GetBassError("BASS_GetVolume"));
			} else raise_log(BassLibrary.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 (BassLibrary.BASS.BASS_RecordSetInput(devid, BassLibrary.Constant.BASS_INPUT_ON, value/100.0f)) {
			return true;
		} else {
			raise_log(BassLibrary.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) {
		FloatValue val = new FloatValue();
		int result = BassLibrary.BASS.BASS_RecordGetInput(devid, val);
		if (result==-1) {
			// error
			raise_log(BassLibrary.BASS.GetBassError("BASS_RecordGetInput"));
			return -1;
		} else {
			int intval = (int)(val.value * 100);
			return intval;
		}
	}
	
	/**
	 * 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 = true;
		List<BASS_DEVICEINFO> result = new ArrayList<BASS_DEVICEINFO>();
		do {
			BASS_DEVICEINFO dev = new BASS_DEVICEINFO();
			success = BassLibrary.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 = true;
		List<BASS_DEVICEINFO> result = new ArrayList<BASS_DEVICEINFO>();
		do {
			BASS_DEVICEINFO dev = new BASS_DEVICEINFO();
			success = BassLibrary.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 (BassLibrary.BASS.BASS_GetDeviceInfo(devid, dev)) {
			return dev;
		} else raise_log(BassLibrary.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 (BassLibrary.BASS.BASS_RecordGetDeviceInfo(devid, dev)) {
			return dev;
		} else raise_log(BassLibrary.BASS.GetBassError("BASS_RecordGetDeviceInfo"));
		return null;
	}
	
	private void raise_log(String msg) {
		if (need_log_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
		} else {
			System.out.println(msg);
		}
	}
	
	public class ULaw extends basicfunctions{
		public ULaw(int bits, int channel) {
			super(48000, BassLibrary.Constant.BASS_DEVICE_MONO | BassLibrary.Constant.BASS_DEVICE_FREQ);
			this.channel = channel;
			this.bits = bits;
			this.need_fileconvert_event = (ba!=null) ? ba.subExists(fileconvert_event) : false;
			this.need_filestreaming_event = (ba!=null) ? ba.subExists(filestreaming_event) : false;
			this.need_inputstreaming_event = (ba!=null) ? ba.subExists(inputstreaming_event) : false;
		}
		public final int[] samplingrate = {8000};
		public final int channel;;
		public final int bits;
		public final String MIME = "audio/basic";
		
		private final boolean need_fileconvert_event;
		private final boolean need_filestreaming_event;
		private final boolean need_inputstreaming_event;
		
		private final String fileconvert_event = "ulaw_fileconvertfinish";
		private final String filestreaming_event = "ulaw_filestreaming";
		private final String inputstreaming_event = "ulaw_inputstreaming";
		
		private void raise_fileconvert(String sourcefile, String targetfile) {
			if (need_fileconvert_event) ba.raiseEventFromDifferentThread(Me, null, 0, fileconvert_event, false, new Object[] {sourcefile, targetfile});
		}
		
		private void raise_filestreaming(String filename, int handle, byte[] data) {
			if (need_filestreaming_event) ba.raiseEventFromDifferentThread(Me,null,0,filestreaming_event, false, new Object[] {filename, handle, data});
		}
		
		private void raise_inputstreaming(String inputname, int handle, byte[] data) {
			if (need_inputstreaming_event) ba.raiseEventFromDifferentThread(Me, null, 0, inputstreaming_event, false, new Object[] {inputname, handle, data});
		}
		
		/**
		 * 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 = DateTime.getNow();
						int totalread = 0;
						int readcount = 0;
						
						try {
							do {
								Pointer ptr = new Memory(1024);
								readcount = BassLibrary.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 = DateTime.getNow() - timestamp;
						raise_log(MessageFormat.format("ULaw FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta));
						raise_fileconvert(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(err -> raise_log(err));
				}
				
				
			} else {
				Stream.of(op.getErrorlist()).forEach(err -> raise_log(err));
				Stream.of(op.CloseHandles()).forEach(err-> raise_log(err));
				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 = BassLibrary.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);
								raise_filestreaming(sourcefilename, op.getFilehandle(), ulaw);
							}
							
						}, null, 0);
						
						if (dsphandle!=0) {
							// sampe sini , DSP sudah created
							long timestamp = DateTime.getNow();
							raise_log("Mixer Playback for "+sourcefilename+" started");
							// tinggal loop aja sampe selesai
							do {
								try {
									Thread.sleep(100);
								} catch (InterruptedException e) {
									raise_log("Interrupt Exception="+e.getMessage());
								}
							} while(BassLibrary.BASS.BASS_ChannelIsActive(op.getFilehandle())==BassLibrary.Constant.BASS_ACTIVE_PLAYING);
							
							// finish playback !!
							long delta = DateTime.getNow() - timestamp;
							raise_log("Mixer Playback for "+sourcefilename+" finished, duration="+delta+" ms");
						} else raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelSetDSP"));
						
						// sampe sini selesai semua
						Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
					});
					
					return op.getFilehandle();
				} else {
					raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelStart"));
					Stream.of(op.CloseHandles()).forEach(err-> raise_log(err));
					return 0;
				}
				
			} else {
				Stream.of(op.getErrorlist()).forEach(err-> raise_log(err));
				Stream.of(op.CloseHandles()).forEach(err-> raise_log(err));
				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, new RECORDPROC() {

				@Override
				public boolean RECORDPROC(int handle, Pointer buffer, int length, Pointer user) {
					
					byte[] data = buffer.getByteArray(0,length);
					byte[] ulaw = ConvertPCMToULaw(data);
					// inputname ada di Pointer user
					raise_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(err->raise_log(err));
					Stream.of(oi.StopRecord()).forEach(err->raise_log(err));
					return 0;
				} else return oi.getRecordhandle();	// aman
			} else {
				Stream.of(oi.getErrorlist()).forEach(err->raise_log(err));
				Stream.of(oi.StopRecord()).forEach(err->raise_log(err));
				return 0;
			}
			
			
			
		}
		
	}
	
	public class PCM extends basicfunctions{
		public PCM(int bits, int channel) {
			super(48000, BassLibrary.Constant.BASS_DEVICE_MONO | BassLibrary.Constant.BASS_DEVICE_FREQ);
			
			this.channel = channel;
			this.bits = bits;
			this.MIME  = bits==24 ? "audio/L24" : "audio/L16";
			this.fileconvert_event = (bits==24 ? "pcm24" : "pcm16")+"_fileconvertfinish";
			this.filestreaming_event = (bits==24 ? "pcm24" : "pcm16")+"_filestreaming";
			this.inputstreaming_event = (bits==24 ? "pcm24" : "pcm16")+"_inputstreaming";
			this.need_fileconvert_event = (ba!=null) ? ba.subExists(fileconvert_event) : false;
			this.need_filestreaming_event = (ba!=null) ? ba.subExists(filestreaming_event) : false;
			this.need_inputstreaming_event = (ba!=null) ? ba.subExists(inputstreaming_event) : false;
			
			
		}
		
		public final int[] samplingrate = {16000, 32000, 44100, 48000};
		public final int channel;
		public final int bits;
		public final String MIME;
		
		private final boolean need_fileconvert_event;
		private final boolean need_filestreaming_event;
		private final boolean need_inputstreaming_event;
		
		private final String fileconvert_event;
		private final String filestreaming_event;
		private final String inputstreaming_event;
		
		private void raise_fileconvert(String sourcefile, String targetfile) {
			if (need_fileconvert_event) ba.raiseEventFromDifferentThread(Me, null, 0, fileconvert_event, false, new Object[] {sourcefile, targetfile});
		}
		
		private void raise_filestreaming(String filename, int handle, byte[] data) {
			if (need_filestreaming_event) ba.raiseEventFromDifferentThread(Me,null,0,filestreaming_event, false, new Object[] {filename, handle, data});
		}
		
		private void raise_inputstreaming(String inputname, int handle, byte[] data) {
			if (need_inputstreaming_event) ba.raiseEventFromDifferentThread(Me, null, 0, inputstreaming_event, false, new Object[] {inputname, handle, data});
		}
		
		private byte[] Convert_PCM16_to_PCM24(byte[] data) {
			// TODO belum pernah dicek
			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 = DateTime.getNow();
						int totalread = 0;
						int readcount = 0;
						do {
							Pointer ptr = new Memory(1024);
							readcount = BassLibrary.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(err-> raise_log(err));
						
						long delta = DateTime.getNow() - timestamp;
						raise_log(MessageFormat.format("FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta));
						raise_fileconvert(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(err->raise_log(err));
				}
			} else {
				Stream.of(op.getErrorlist()).forEach(err->raise_log(err));
				Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
				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 = BassLibrary.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);
						raise_filestreaming(sourcefilename, op.getFilehandle(),cc);
					}, null, 0);
					
					if (dsphandle==0) {
						raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelSetDSP"));
						Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
						return 0;
					}
					
					// sampe sini berhasil
					return op.getFilehandle();
				}
				else {
					raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelStart"));
					Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
					return 0;
				}
				
			} else {
				Stream.of(op.getErrorlist()).forEach(err->raise_log(err));
				Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
				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, new RECORDPROC() {

				@Override
				public boolean RECORDPROC(int handle, Pointer buffer, int length, Pointer user) {
					byte[] data = buffer.getByteArray(0, length);
					String recdevname = user==null ? "" : user.getString(0);
					if (bits==24) {
						raise_inputstreaming(recdevname, handle, Convert_PCM16_to_PCM24(data));
					} else {
						raise_inputstreaming(recdevname, handle, data);
					}
					return true;
				}
				
			});
			
			if (oi.isSuccess()) {
				inited_recorder.add(oi.getRecorderdev());
				return oi.getRecordhandle();
			} else {
				Stream.of(oi.getErrorlist()).forEach(err->raise_log(err));
				Stream.of(oi.StopRecord()).forEach(err->raise_log(err));
				return 0;
			}	
			
		}
		
	}
	
	public class AAC extends basicfunctions{
		public AAC(int bits, int channel) {
			super(48000, BassLibrary.Constant.BASS_DEVICE_MONO | BassLibrary.Constant.BASS_DEVICE_FREQ);
			
			this.bits = bits;
			this.channel = channel;
			this.need_fileconvert_event = (ba!=null) ? ba.subExists(fileconvert_event) : false;
			this.need_filestreaming_event = (ba!=null) ? ba.subExists(filestreaming_event) : false;
			this.need_inputstreaming_event = (ba!=null) ? ba.subExists(inputstreaming_event) : false;
			
		}
		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 boolean need_fileconvert_event;
		private final boolean need_filestreaming_event;
		private final boolean need_inputstreaming_event;
		
		private final String fileconvert_event = "aac_fileconvertfinish";
		private final String filestreaming_event = "aac_filestreaming";
		private final String inputstreaming_event = "aac_inputstreaming";
		
		private void raise_fileconvert(String sourcefile, String targetfile) {
			if (need_fileconvert_event) ba.raiseEventFromDifferentThread(Me, null, 0, fileconvert_event, false, new Object[] {sourcefile, targetfile});
		}
		
		private void raise_filestreaming(String filename, int handle, byte[] data) {
			if (need_filestreaming_event) ba.raiseEventFromDifferentThread(Me,null,0,filestreaming_event, false, new Object[] {filename, handle, data});
		}
		
		private void raise_inputstreaming(String inputname, int handle, byte[] data) {
			if (need_inputstreaming_event) ba.raiseEventFromDifferentThread(Me, null, 0, inputstreaming_event, false, new Object[] {inputname, handle, data});
		}
		
		@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 = BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_StartFile(op.getMixerhandle(), null, 0, targetfilename);
				if (encodehandle==0) {
					// gak bisa create encoder
					raise_log(BassLibrary.BASS.GetBassError("BASS_Encode_AAC_StartFile"));
					// free handle
					Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
					return false;
				}
				
				// sampe sini encoder jalan
				exec.submit(()->{
					do {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							raise_log("InterruptException");
						}
					} while (BassLibrary.BASS.BASS_ChannelIsActive(op.getMixerhandle())==BassLibrary.Constant.BASS_ACTIVE_PLAYING);
					
					// free encoder
					BassEncLibrary.BASSENC.BASS_Encode_Stop(encodehandle);
					// free handle
					Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
					
					
					
					raise_fileconvert(sourcefilename, targetfilename);
				});
				return true;
			} else {
				Stream.of(op.getErrorlist()).forEach(err->raise_log(err));
				Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
				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 = BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_Start(op.getMixerhandle(), null, 0, 
							(encode_handle, source_handle, buffer, length, offset, user)->{
								raise_filestreaming(sourcefilename, op.getFilehandle(), buffer.getByteArray(0,length));
							}
							, null);
					
					if (encodehandle==0) {
						raise_log(BassLibrary.BASS.GetBassError("BASS_Encode_AAC_Start"));
						
						// free handle
						Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
						return 0;
					}
					
					exec.submit(()->{
						do {
							try {
								Thread.sleep(100);
							} catch (InterruptedException e) {
								raise_log("InterruptException");
							}
						} while(BassLibrary.BASS.BASS_ChannelIsActive(op.getMixerhandle())==BassLibrary.Constant.BASS_ACTIVE_PLAYING);
						
						BassEncLibrary.BASSENC.BASS_Encode_Stop(encodehandle);
						
						// free handle
						Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
						
					});
					
					return op.getFilehandle();
				} else {
					raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelStart"));
					Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
					return 0;
				}
				
				
			} else {
				Stream.of(op.getErrorlist()).forEach(err->raise_log(err));
				Stream.of(op.CloseHandles()).forEach(err->raise_log(err));
				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 = BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_Start(oi.getRecordhandle(), null, 0, (encode_handle, source_handle, buffer, length, offset, user)->{
					byte[] data = buffer.getByteArray(0, length);
					raise_inputstreaming(oi.getDeviceName(), source_handle, data);
				}, null);
				
				if (encodehandle!=0) {
					return oi.getRecordhandle();
				} else {
					raise_log(BassLibrary.BASS.GetBassError("BASS_Encode_AAC_Start"));
					Stream.of(oi.StopRecord()).forEach(err->raise_log(err));
				}
				
			} else {
				Stream.of(oi.getErrorlist()).forEach(err->raise_log(err));
				Stream.of(oi.StopRecord()).forEach(err->raise_log(err));
				
			}
			
			return 0;
			
			
		}
		
	}
}
