From 611745439f2243878095ae08922c6c86a4166253 Mon Sep 17 00:00:00 2001 From: rdkartono Date: Mon, 6 Oct 2025 13:50:00 +0700 Subject: [PATCH] commit 06/10/2025 --- .gitignore | 7 ++- src/Main.kt | 53 ++++++++++++++++++++ src/MainExtension01.kt | 29 +++++------ src/audio/AudioPlayer.kt | 93 +++++++++++++++++++++++++++--------- src/audio/Bass.java | 4 +- src/barix/BarixConnection.kt | 1 + src/codes/Somecodes.kt | 45 +++++++++++++++++ 7 files changed, 190 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 3ddbf4c..5c642fb 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,9 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +## Soundbank directories ## +/PagingResult/ +/SoundBanks/ +/SoundbankResult/ \ No newline at end of file diff --git a/src/Main.kt b/src/Main.kt index 1bfab73..203b51f 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,7 +1,9 @@ +import audio.AudioFileInfo import audio.AudioPlayer import audio.UDPReceiver import barix.BarixConnection import barix.TCP_Barix_Command_Server +import codes.Somecodes import com.sun.jna.Platform import commandServer.TCP_Android_Command_Server import content.Language @@ -16,7 +18,9 @@ import kotlinx.coroutines.launch import org.tinylog.Logger import oshi.util.GlobalConfig import web.WebApp +import java.nio.file.Files import kotlin.concurrent.fixedRateTimer +import kotlin.io.path.absolutePathString lateinit var db: MariaDB lateinit var audioPlayer: AudioPlayer @@ -39,6 +43,49 @@ val urutan_bahasa = listOf( Language.ARABIC.name ) +/** + * Common audio files, seperti chimeup, chimedown, silence1s, silencehalf + */ +val commonAudio : MutableMap = HashMap() + +/** + * Create necessary folders if not exist + */ +fun folder_preparation(){ + Files.createDirectories(Somecodes.SoundbankResult_directory) + Files.createDirectories(Somecodes.PagingResult_directory) + Files.createDirectories(Somecodes.Soundbank_directory) + Somecodes.Soundbank_Languages_directory.forEach { + Files.createDirectories(it) + } + +} + +/** + * Extract necessary wav files from classpath to soundbank directory + * and Load them + */ +fun files_preparation(){ + val list = listOf("chimeup.wav", "chimedown.wav", "silence1s.wav", "silencehalf.wav") + list.forEach { + Somecodes.ExtractFilesFromClassPath("/$it", Somecodes.Soundbank_directory) + val pp = Somecodes.Soundbank_directory.resolve(it) + if (Files.isRegularFile(pp)){ + val afi = audioPlayer.LoadAudioFile(pp.absolutePathString()) + if (afi.isValid()){ + Logger.info { "Common audio $it loaded from ${pp.toAbsolutePath()}" } + val key = it.substring(0, it.length - 4) // buang .wav + commonAudio[key] = afi + } else { + Logger.error { "Failed to load common audio $it from ${pp.toAbsolutePath()}" } + } + } else { + Logger.error { "Common audio $it not found at ${pp.toAbsolutePath()}" } + } + } + +} + // Application start here fun main() { if (Platform.isWindows()) { @@ -46,8 +93,14 @@ fun main() { GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true) } Logger.info { "Starting AAS New Generation version $version" } + + folder_preparation() + audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate audioPlayer.InitAudio(1) + + files_preparation() + db = MariaDB() val subcode01 = MainExtension01() diff --git a/src/MainExtension01.kt b/src/MainExtension01.kt index dc69652..a687a5a 100644 --- a/src/MainExtension01.kt +++ b/src/MainExtension01.kt @@ -580,7 +580,7 @@ class MainExtension01 { ).toString() audioPlayer.WavWriter( listafi, - targetfile + targetfile, true, ) { success, message -> db.Add_Log("AAS", message) if (success) { @@ -662,11 +662,8 @@ class MainExtension01 { val zz = qa.BroadcastZones.split(";") if (AllBroadcastZonesValid(zz)) { - println("All broadcast zones valid") val ips = BroadcastZones_to_SoundChannel_IP(zz) - println("Broadcast zones $zz converted to SoundChannel IPs: $ips") if (AllStreamerOutputIdle(ips)) { - println("All broadcast zones idle") if (qa.Type == "SOUNDBANK") { val variables = Get_Soundbank_Data(qa.SB_TAGS) val languages = qa.Language.split(";") @@ -708,17 +705,14 @@ class MainExtension01 { val listafi = mutableListOf() mblist.forEach { mb -> - println("Getting soundbank files for messagebank id ${mb.ANN_ID} voice=${mb.Voice_Type} lang=${mb.Language}, variables=$variables") Get_Soundbank_Files(mb, variables ?: emptyMap(), { listfile -> - println("Got soundbank files: $listfile") listfile.forEach { filenya -> val afi = audioPlayer.LoadAudioFile(filenya) if (afi.isValid()) { listafi.add(afi) } } - println("Loaded AudioFileInfo list: $listafi") }, { err -> @@ -728,27 +722,30 @@ class MainExtension01 { } if (listafi.isNotEmpty()){ + db.queuetableDB.DeleteByIndex(qa.index.toInt()) + val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Soundbank","")).toString() println("Writing to target WAV file: $targetfile") - audioPlayer.WavWriter(listafi, targetfile + audioPlayer.WavWriter(listafi, targetfile, true, ) { success, message -> if (success) { // file siap broadcast println("Successfully wrote WAV file: $targetfile") val targetafi = audioPlayer.LoadAudioFile(targetfile) if (targetafi.isValid()) { - zz.forEach { z1 -> - StreamerOutputs.values.find { it.channel == z1 } - ?.SendData( - targetafi.bytes, - { db.Add_Log("AAS", it) }, - { db.Add_Log("AAS", it) }) + ips.forEach { ip -> + StreamerOutputs[ip].let{ sc -> + sc?.SendData(targetafi.bytes, + { db.Add_Log("AAS", it) }, + { db.Add_Log("AAS", it) } ) + } } + val logmsg = "Broadcast started SOUNDBANK message with generated file '$targetfile' to zones: ${qa.BroadcastZones}" Logger.info { logmsg } db.Add_Log("AAS", logmsg) - db.queuetableDB.DeleteByIndex(qa.index.toInt()) + } } else { @@ -783,7 +780,7 @@ class MainExtension01 { } } val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Timer","")).toString() - audioPlayer.WavWriter(listafi, targetfile + audioPlayer.WavWriter(listafi, targetfile, true, ) { success, message -> if (success) { // file siap broadcast diff --git a/src/audio/AudioPlayer.kt b/src/audio/AudioPlayer.kt index 13adaf8..aeee691 100644 --- a/src/audio/AudioPlayer.kt +++ b/src/audio/AudioPlayer.kt @@ -11,9 +11,13 @@ import audio.BassEnc.BASS_ENCODE_PCM import codes.Somecodes.Companion.ValidFile import codes.Somecodes.Companion.ValidString import com.sun.jna.Memory +import com.sun.jna.Pointer +import commonAudio import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.tinylog.Logger import java.util.function.BiConsumer @@ -126,21 +130,28 @@ class AudioPlayer (var samplingrate: Int) { return result } + /** + * Writes the audio data from a byte array to a WAV file. + * @param data The byte array containing the audio data. + * @param target The target file name for the WAV file. + * @param callback A BiConsumer that accepts a Boolean indicating success or failure and a String message. + */ fun WavWriter(data: ByteArray, target: String, callback: BiConsumer) { val source = AudioFileInfo() source.bytes = data source.fileName = "In-Memory Data" val sources = listOf(source) - WavWriter(sources, target, callback) + WavWriter(sources, target, false, callback) } /** * Writes the audio data from the sources to a WAV file. * @param sources List of AudioFileInfo objects containing the audio data to write. * @param target The target file name for the WAV file. + * @param withChime If true, adds a chime sound at the beginning and end of the audio. * @param callback A BiConsumer that accepts a Boolean indicating success or failure and a String message. */ - fun WavWriter(sources: List, target: String, callback: BiConsumer) { + fun WavWriter(sources: List, target: String, withChime: Boolean, callback: BiConsumer) { if (sources.isEmpty()) { callback.accept(false, " Invalid sources") return @@ -150,10 +161,10 @@ class AudioPlayer (var samplingrate: Int) { return } - val job = CoroutineScope(Dispatchers.Default) - job.launch(CoroutineName("WavWriter $target")) { + + val job = CoroutineScope(Dispatchers.IO).launch(CoroutineName("WavWriter $target")) { bass.BASS_SetDevice(0) // Set to No Sound device for writing - val streamhandle = bass.BASS_StreamCreate(samplingrate, 1, BASS_STREAM_DECODE, STREAMPROC_PUSH, null) + val streamhandle = bass.BASS_StreamCreate(samplingrate, 1, BASS_STREAM_DECODE, Pointer(-1), null) if (streamhandle==0){ callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}") return@launch @@ -164,46 +175,82 @@ class AudioPlayer (var samplingrate: Int) { callback.accept(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}") return@launch } - val playresult = bass.BASS_ChannelPlay(streamhandle,false) - if (!playresult) { - bassenc.BASS_Encode_Stop(encodehandle) - bass.BASS_StreamFree(streamhandle) - callback.accept(false, "BASS_ChannelPlay failed: ${bass.BASS_ErrorGetCode()}") - return@launch + + fun pushData(data: ByteArray): Boolean { + val mem = Memory(data.size.toLong()) + mem.write(0, data, 0, data.size) + val pushresult = bass.BASS_StreamPutData(streamhandle, mem, data.size) + if (pushresult==-1){ + val errcode = bass.BASS_ErrorGetCode() + println("BASS_StreamPutData failed: $errcode") + } + return pushresult != -1 } + + var allsuccess = true + if (withChime){ + val chup = commonAudio["chimeup"] + if (chup!=null && chup.isValid()){ + if (pushData(chup.bytes)){ + println("Chime up pushed") + } else { + allsuccess = false + println("Chime up failed") + } + } else println("Chime Up not valid") + } sources.forEach { source -> if (source.isValid()) { // write the bytes to the stream - val mem = Memory(source.bytes.size.toLong()) - mem.write(0, source.bytes, 0, source.bytes.size) - val pushresult = bass.BASS_StreamPutData(streamhandle, mem, source.bytes.size) - if (pushresult == -1) { - Logger.error { "Failed to write data from ${source.fileName} to stream: ${bass.BASS_ErrorGetCode()}" } + if (pushData(source.bytes)){ + println("Source ${source.fileName} pushed") + } else { allsuccess = false + println("Source ${source.fileName} push failed") } + } else { + allsuccess = false + println("Source ${source.fileName} is not valid") } + + } + if (withChime){ + val chdn = commonAudio["chimedown"] + if (chdn!=null && chdn.isValid()){ + if (pushData(chdn.bytes)){ + println("Chime down pushed") + } else { + allsuccess = false + println("Chime down failed") + } + } else println("Chime Down not valid") } - // now we wait until the stream is finished - while(bassenc.BASS_Encode_IsActive(encodehandle) == BASS_ACTIVE_PLAYING) { - Thread.sleep(100) // Sleep for a short time to avoid busy waiting - } + val readsize: Long = 1024 * 1024 // read 1 MB at a time + var totalread: Long = 0 + do{ + val p = Memory(readsize) + val read = bass.BASS_ChannelGetData(streamhandle, p, 4096) + if (read > 0) { + totalread += read + } + } while (read > 0) + println("Finished reading stream data, total $totalread bytes read") + // close the encoding handle bassenc.BASS_Encode_Stop(encodehandle) bass.BASS_ChannelFree(streamhandle) if (allsuccess){ + callback.accept(true, "WAV file written successfully: $target") } else { callback.accept(false, "Failed to write some data to WAV file: $target") } } - - - } diff --git a/src/audio/Bass.java b/src/audio/Bass.java index d025b1e..da058b8 100644 --- a/src/audio/Bass.java +++ b/src/audio/Bass.java @@ -716,6 +716,7 @@ public interface Bass extends Library { boolean BASS_SampleStop(int handle); int BASS_StreamCreate(int freq, int chans, int flags, STREAMPROC proc, Pointer user); + int BASS_StreamCreate(int freq, int chans, int flags, Pointer proc, Pointer user); // for STREAMPROC_DUMMY int BASS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags); int BASS_StreamCreateFile(Pointer file, long offset, long length, int flags); int BASS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); @@ -785,8 +786,7 @@ public interface Bass extends Library { boolean BASS_FXGetParameters(int handle, Object params); boolean BASS_FXSetPriority(int handle, int priority); boolean BASS_FXReset(int handle); - // gak bisa - int BASS_StreamCreate(int freq, int chans, int flags, int proc, Pointer user); + } diff --git a/src/barix/BarixConnection.kt b/src/barix/BarixConnection.kt index b70ada0..4e8e92b 100644 --- a/src/barix/BarixConnection.kt +++ b/src/barix/BarixConnection.kt @@ -93,6 +93,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin fun SendData(data: ByteArray, cbOK: Consumer, cbFail: Consumer) { if (data.isNotEmpty()) { CoroutineScope(Dispatchers.IO).launch { + val bb = ByteBuffer.wrap(data) while(bb.hasRemaining()){ try { diff --git a/src/codes/Somecodes.kt b/src/codes/Somecodes.kt index 27e3e05..09fc846 100644 --- a/src/codes/Somecodes.kt +++ b/src/codes/Somecodes.kt @@ -2,11 +2,13 @@ package codes import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import content.Language import content.ScheduleDay import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.tinylog.Logger import oshi.SystemInfo import oshi.hardware.CentralProcessor import oshi.hardware.GlobalMemory @@ -22,8 +24,18 @@ import kotlin.io.path.name class Somecodes { companion object { val current_directory : String = System.getProperty("user.dir") + val Soundbank_directory : Path = Path.of(current_directory,"Soundbank") + val Soundbank_Languages_directory : List = listOf( + Soundbank_directory.resolve(Language.INDONESIA.name), + Soundbank_directory.resolve(Language.LOCAL.name), + Soundbank_directory.resolve(Language.ENGLISH.name), + Soundbank_directory.resolve(Language.CHINESE.name), + Soundbank_directory.resolve(Language.JAPANESE.name), + Soundbank_directory.resolve(Language.ARABIC.name) + ) val SoundbankResult_directory : Path = Path.of(current_directory,"SoundbankResult") val PagingResult_directory : Path = Path.of(current_directory,"PagingResult") + val si = SystemInfo() val processor: CentralProcessor = si.hardware.processor val memory : GlobalMemory = si.hardware.memory @@ -42,6 +54,39 @@ class Somecodes { // regex for getting ann_id from Message, which is the number inside [] private val ann_id_regex = Regex("\\[(\\d+)]") + fun ExtractFilesFromClassPath(resourcePath: String, outputDir: Path) { + try { + val resource = Somecodes::class.java.getResource(resourcePath) + if (resource != null) { + val uri = resource.toURI() + val path = if (uri.scheme == "jar") { + val fileSystem = java.nio.file.FileSystems.newFileSystem(uri, emptyMap()) + fileSystem.getPath(resourcePath) + } else { + Path.of(uri) + } + + Files.walk(path).use { stream -> + stream.forEach { sourcePath -> + if (Files.isRegularFile(sourcePath)) { + val fn = sourcePath.fileName + val targetPath = outputDir.resolve(fn) + if (Files.isRegularFile(targetPath) && Files.size(targetPath) > 0) { + Logger.info { "File $targetPath already exists, skipping extraction." } + return@forEach + } + Files.copy(sourcePath, targetPath) + Logger.info { "Extracted ${sourcePath.name} to $targetPath" } + return@forEach + } + } + } + } else Logger.error { "Resource $resource not found" } + } catch (e: Exception) { + Logger.error { "Exception while extracting $resourcePath, Message = ${e.message}"} + } + } + /** * Check if a string is a valid number. */