commit 06/10/2025

This commit is contained in:
2025-10-06 13:50:00 +07:00
parent cfb38556b5
commit 611745439f
7 changed files with 190 additions and 42 deletions

5
.gitignore vendored
View File

@@ -30,3 +30,8 @@ bin/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
## Soundbank directories ##
/PagingResult/
/SoundBanks/
/SoundbankResult/

View File

@@ -1,7 +1,9 @@
import audio.AudioFileInfo
import audio.AudioPlayer import audio.AudioPlayer
import audio.UDPReceiver import audio.UDPReceiver
import barix.BarixConnection import barix.BarixConnection
import barix.TCP_Barix_Command_Server import barix.TCP_Barix_Command_Server
import codes.Somecodes
import com.sun.jna.Platform import com.sun.jna.Platform
import commandServer.TCP_Android_Command_Server import commandServer.TCP_Android_Command_Server
import content.Language import content.Language
@@ -16,7 +18,9 @@ import kotlinx.coroutines.launch
import org.tinylog.Logger import org.tinylog.Logger
import oshi.util.GlobalConfig import oshi.util.GlobalConfig
import web.WebApp import web.WebApp
import java.nio.file.Files
import kotlin.concurrent.fixedRateTimer import kotlin.concurrent.fixedRateTimer
import kotlin.io.path.absolutePathString
lateinit var db: MariaDB lateinit var db: MariaDB
lateinit var audioPlayer: AudioPlayer lateinit var audioPlayer: AudioPlayer
@@ -39,6 +43,49 @@ val urutan_bahasa = listOf(
Language.ARABIC.name Language.ARABIC.name
) )
/**
* Common audio files, seperti chimeup, chimedown, silence1s, silencehalf
*/
val commonAudio : MutableMap<String, AudioFileInfo> = 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 // Application start here
fun main() { fun main() {
if (Platform.isWindows()) { if (Platform.isWindows()) {
@@ -46,8 +93,14 @@ fun main() {
GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true) GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true)
} }
Logger.info { "Starting AAS New Generation version $version" } Logger.info { "Starting AAS New Generation version $version" }
folder_preparation()
audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate
audioPlayer.InitAudio(1) audioPlayer.InitAudio(1)
files_preparation()
db = MariaDB() db = MariaDB()
val subcode01 = MainExtension01() val subcode01 = MainExtension01()

View File

@@ -580,7 +580,7 @@ class MainExtension01 {
).toString() ).toString()
audioPlayer.WavWriter( audioPlayer.WavWriter(
listafi, listafi,
targetfile targetfile, true,
) { success, message -> ) { success, message ->
db.Add_Log("AAS", message) db.Add_Log("AAS", message)
if (success) { if (success) {
@@ -662,11 +662,8 @@ class MainExtension01 {
val zz = qa.BroadcastZones.split(";") val zz = qa.BroadcastZones.split(";")
if (AllBroadcastZonesValid(zz)) { if (AllBroadcastZonesValid(zz)) {
println("All broadcast zones valid")
val ips = BroadcastZones_to_SoundChannel_IP(zz) val ips = BroadcastZones_to_SoundChannel_IP(zz)
println("Broadcast zones $zz converted to SoundChannel IPs: $ips")
if (AllStreamerOutputIdle(ips)) { if (AllStreamerOutputIdle(ips)) {
println("All broadcast zones idle")
if (qa.Type == "SOUNDBANK") { if (qa.Type == "SOUNDBANK") {
val variables = Get_Soundbank_Data(qa.SB_TAGS) val variables = Get_Soundbank_Data(qa.SB_TAGS)
val languages = qa.Language.split(";") val languages = qa.Language.split(";")
@@ -708,17 +705,14 @@ class MainExtension01 {
val listafi = mutableListOf<AudioFileInfo>() val listafi = mutableListOf<AudioFileInfo>()
mblist.forEach { mb -> 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(), { Get_Soundbank_Files(mb, variables ?: emptyMap(), {
listfile -> listfile ->
println("Got soundbank files: $listfile")
listfile.forEach { filenya -> listfile.forEach { filenya ->
val afi = audioPlayer.LoadAudioFile(filenya) val afi = audioPlayer.LoadAudioFile(filenya)
if (afi.isValid()) { if (afi.isValid()) {
listafi.add(afi) listafi.add(afi)
} }
} }
println("Loaded AudioFileInfo list: $listafi")
}, },
{ {
err -> err ->
@@ -728,27 +722,30 @@ class MainExtension01 {
} }
if (listafi.isNotEmpty()){ if (listafi.isNotEmpty()){
db.queuetableDB.DeleteByIndex(qa.index.toInt())
val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Soundbank","")).toString() val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Soundbank","")).toString()
println("Writing to target WAV file: $targetfile") println("Writing to target WAV file: $targetfile")
audioPlayer.WavWriter(listafi, targetfile audioPlayer.WavWriter(listafi, targetfile, true,
) { success, message -> ) { success, message ->
if (success) { if (success) {
// file siap broadcast // file siap broadcast
println("Successfully wrote WAV file: $targetfile") println("Successfully wrote WAV file: $targetfile")
val targetafi = audioPlayer.LoadAudioFile(targetfile) val targetafi = audioPlayer.LoadAudioFile(targetfile)
if (targetafi.isValid()) { if (targetafi.isValid()) {
zz.forEach { z1 -> ips.forEach { ip ->
StreamerOutputs.values.find { it.channel == z1 } StreamerOutputs[ip].let{ sc ->
?.SendData( sc?.SendData(targetafi.bytes,
targetafi.bytes,
{ db.Add_Log("AAS", it) }, { db.Add_Log("AAS", it) },
{ db.Add_Log("AAS", it) } ) { db.Add_Log("AAS", it) } )
} }
}
val logmsg = val logmsg =
"Broadcast started SOUNDBANK message with generated file '$targetfile' to zones: ${qa.BroadcastZones}" "Broadcast started SOUNDBANK message with generated file '$targetfile' to zones: ${qa.BroadcastZones}"
Logger.info { logmsg } Logger.info { logmsg }
db.Add_Log("AAS", logmsg) db.Add_Log("AAS", logmsg)
db.queuetableDB.DeleteByIndex(qa.index.toInt())
} }
} else { } else {
@@ -783,7 +780,7 @@ class MainExtension01 {
} }
} }
val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Timer","")).toString() val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Timer","")).toString()
audioPlayer.WavWriter(listafi, targetfile audioPlayer.WavWriter(listafi, targetfile, true,
) { success, message -> ) { success, message ->
if (success) { if (success) {
// file siap broadcast // file siap broadcast

View File

@@ -11,9 +11,13 @@ import audio.BassEnc.BASS_ENCODE_PCM
import codes.Somecodes.Companion.ValidFile import codes.Somecodes.Companion.ValidFile
import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.ValidString
import com.sun.jna.Memory import com.sun.jna.Memory
import com.sun.jna.Pointer
import commonAudio
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tinylog.Logger import org.tinylog.Logger
import java.util.function.BiConsumer import java.util.function.BiConsumer
@@ -126,21 +130,28 @@ class AudioPlayer (var samplingrate: Int) {
return result 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<Boolean, String>) { fun WavWriter(data: ByteArray, target: String, callback: BiConsumer<Boolean, String>) {
val source = AudioFileInfo() val source = AudioFileInfo()
source.bytes = data source.bytes = data
source.fileName = "In-Memory Data" source.fileName = "In-Memory Data"
val sources = listOf(source) val sources = listOf(source)
WavWriter(sources, target, callback) WavWriter(sources, target, false, callback)
} }
/** /**
* Writes the audio data from the sources to a WAV file. * Writes the audio data from the sources to a WAV file.
* @param sources List of AudioFileInfo objects containing the audio data to write. * @param sources List of AudioFileInfo objects containing the audio data to write.
* @param target The target file name for the WAV file. * @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. * @param callback A BiConsumer that accepts a Boolean indicating success or failure and a String message.
*/ */
fun WavWriter(sources: List<AudioFileInfo>, target: String, callback: BiConsumer<Boolean, String>) { fun WavWriter(sources: List<AudioFileInfo>, target: String, withChime: Boolean, callback: BiConsumer<Boolean, String>) {
if (sources.isEmpty()) { if (sources.isEmpty()) {
callback.accept(false, " Invalid sources") callback.accept(false, " Invalid sources")
return return
@@ -150,10 +161,10 @@ class AudioPlayer (var samplingrate: Int) {
return 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 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){ if (streamhandle==0){
callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}") callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}")
return@launch return@launch
@@ -164,46 +175,82 @@ class AudioPlayer (var samplingrate: Int) {
callback.accept(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}") callback.accept(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}")
return@launch return@launch
} }
val playresult = bass.BASS_ChannelPlay(streamhandle,false)
if (!playresult) { fun pushData(data: ByteArray): Boolean {
bassenc.BASS_Encode_Stop(encodehandle) val mem = Memory(data.size.toLong())
bass.BASS_StreamFree(streamhandle) mem.write(0, data, 0, data.size)
callback.accept(false, "BASS_ChannelPlay failed: ${bass.BASS_ErrorGetCode()}") val pushresult = bass.BASS_StreamPutData(streamhandle, mem, data.size)
return@launch if (pushresult==-1){
val errcode = bass.BASS_ErrorGetCode()
println("BASS_StreamPutData failed: $errcode")
}
return pushresult != -1
} }
var allsuccess = true 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 -> sources.forEach { source ->
if (source.isValid()) { if (source.isValid()) {
// write the bytes to the stream // write the bytes to the stream
val mem = Memory(source.bytes.size.toLong()) if (pushData(source.bytes)){
mem.write(0, source.bytes, 0, source.bytes.size) println("Source ${source.fileName} pushed")
val pushresult = bass.BASS_StreamPutData(streamhandle, mem, source.bytes.size) } else {
if (pushresult == -1) {
Logger.error { "Failed to write data from ${source.fileName} to stream: ${bass.BASS_ErrorGetCode()}" }
allsuccess = false allsuccess = false
println("Source ${source.fileName} push failed")
} }
} } else {
allsuccess = false
println("Source ${source.fileName} is 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
} }
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")
}
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 // close the encoding handle
bassenc.BASS_Encode_Stop(encodehandle) bassenc.BASS_Encode_Stop(encodehandle)
bass.BASS_ChannelFree(streamhandle) bass.BASS_ChannelFree(streamhandle)
if (allsuccess){ if (allsuccess){
callback.accept(true, "WAV file written successfully: $target") callback.accept(true, "WAV file written successfully: $target")
} else { } else {
callback.accept(false, "Failed to write some data to WAV file: $target") callback.accept(false, "Failed to write some data to WAV file: $target")
} }
} }
} }

View File

@@ -716,6 +716,7 @@ public interface Bass extends Library {
boolean BASS_SampleStop(int handle); 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, 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(boolean mem, String file, long offset, long length, int flags);
int BASS_StreamCreateFile(Pointer 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); 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_FXGetParameters(int handle, Object params);
boolean BASS_FXSetPriority(int handle, int priority); boolean BASS_FXSetPriority(int handle, int priority);
boolean BASS_FXReset(int handle); boolean BASS_FXReset(int handle);
// gak bisa
int BASS_StreamCreate(int freq, int chans, int flags, int proc, Pointer user);
} }

View File

@@ -93,6 +93,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) { fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) {
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val bb = ByteBuffer.wrap(data) val bb = ByteBuffer.wrap(data)
while(bb.hasRemaining()){ while(bb.hasRemaining()){
try { try {

View File

@@ -2,11 +2,13 @@ package codes
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import content.Language
import content.ScheduleDay import content.ScheduleDay
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tinylog.Logger
import oshi.SystemInfo import oshi.SystemInfo
import oshi.hardware.CentralProcessor import oshi.hardware.CentralProcessor
import oshi.hardware.GlobalMemory import oshi.hardware.GlobalMemory
@@ -22,8 +24,18 @@ import kotlin.io.path.name
class Somecodes { class Somecodes {
companion object { companion object {
val current_directory : String = System.getProperty("user.dir") val current_directory : String = System.getProperty("user.dir")
val Soundbank_directory : Path = Path.of(current_directory,"Soundbank")
val Soundbank_Languages_directory : List<Path> = 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 SoundbankResult_directory : Path = Path.of(current_directory,"SoundbankResult")
val PagingResult_directory : Path = Path.of(current_directory,"PagingResult") val PagingResult_directory : Path = Path.of(current_directory,"PagingResult")
val si = SystemInfo() val si = SystemInfo()
val processor: CentralProcessor = si.hardware.processor val processor: CentralProcessor = si.hardware.processor
val memory : GlobalMemory = si.hardware.memory val memory : GlobalMemory = si.hardware.memory
@@ -42,6 +54,39 @@ class Somecodes {
// regex for getting ann_id from Message, which is the number inside [] // regex for getting ann_id from Message, which is the number inside []
private val ann_id_regex = Regex("\\[(\\d+)]") 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<String, Any>())
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. * Check if a string is a valid number.
*/ */