From 901d553da91773027d6578b2b6fcfec01dfd52d5 Mon Sep 17 00:00:00 2001 From: rdkartono Date: Mon, 28 Jul 2025 13:55:58 +0700 Subject: [PATCH] commit 28/07/2025 --- src/audio/AudioPlayer.kt | 1 - src/audio/UDPReceiverToFile.kt | 153 +++++++++++++++++++++++++++++++++ src/codes/QuadConsumer.kt | 14 +++ src/codes/TriConsumer.kt | 13 +++ 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/audio/UDPReceiverToFile.kt create mode 100644 src/codes/QuadConsumer.kt create mode 100644 src/codes/TriConsumer.kt diff --git a/src/audio/AudioPlayer.kt b/src/audio/AudioPlayer.kt index 3c41432..42cfc95 100644 --- a/src/audio/AudioPlayer.kt +++ b/src/audio/AudioPlayer.kt @@ -191,7 +191,6 @@ class AudioPlayer (var samplingrate: Int) { thread.isDaemon = true thread.start() - } diff --git a/src/audio/UDPReceiverToFile.kt b/src/audio/UDPReceiverToFile.kt new file mode 100644 index 0000000..4360e27 --- /dev/null +++ b/src/audio/UDPReceiverToFile.kt @@ -0,0 +1,153 @@ +package audio + +import audio.Bass.BASS_STREAMPROC_END +import audio.BassEnc.BASS_ENCODE_PCM +import org.tinylog.Logger +import java.io.PipedInputStream +import java.io.PipedOutputStream +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetSocketAddress +import java.util.function.BiConsumer +import java.util.function.Consumer + +@Suppress("unused") +/** + * UDPReceiverToFile is a class that listens for UDP packets on a specified address and port + * and writes the received data to a specified file. + * It is designed to run in a separate thread and can be stopped when no longer needed. + * @param listeningAddress The address to listen for incoming UDP packets. + * @param listeningPort The port to listen for incoming UDP packets. + * @param samplingrate The sampling rate for the audio data, default is 44,100 Hz. + * @param channel The number of audio channels, default is 1 (mono). + * @param outputFilePath The path to the file where the received data will be written. + * @param senderIP The IP address of the sender from which to accept packets. + */ +class UDPReceiverToFile(listeningAddress: String, listeningPort: Int, val samplingrate: Int=44100, val channel: Int=1, val outputFilePath: String, val senderIP: String) { + + private var socket: DatagramSocket? = null + private val bass : Bass = Bass.Instance + private val bassenc : BassEnc = BassEnc.Instance + private var isReceiving: Boolean = false + private val pipeIn= PipedInputStream(16*1024) // 16K + var isReady: Boolean = false; private set + var bytesReceived: Long = 0; private set + var bytesWritten: Long = 0; private set + + + init { + try{ + val socketaddress = InetSocketAddress(listeningAddress, listeningPort) + socket = DatagramSocket(socketaddress) + isReady = true + } catch (e : Exception) { + Logger.error {"Failed to create UDP socket: ${e.message}" } + } + } + + private val streamProc = Bass.STREAMPROC { handle, buffer, length, user -> + try{ + val dd = ByteArray(length) + val bytesread = pipeIn.read(dd) + + if (bytesread>0){ + buffer?.write(0, dd, 0, bytesread) // Write the data to the buffer + bytesWritten += bytesread + // Return the number of bytes read + bytesread + } else { + // if bytesread is 0, it means the pipe is empty, return BASS_STREAMPROC_END + BASS_STREAMPROC_END + } + } catch (e : Exception){ + // If an error occurs, log it and return BASS_STREAMPROC_END + Logger.error { "STREAMPROC exception on UDPReceiverToFile $senderIP $outputFilePath" } + BASS_STREAMPROC_END + } + } + + /** + * Starts receiving data from the UDP socket and writing it to the specified file. + * This method runs in a separate thread. + * @param callback A BiConsumer that accepts a Boolean indicating success or failure and a String message. + * @param udpIsReceiving A Consumer that accepts a Boolean indicating whether UDP is currently receiving + */ + fun startReceiving(callback : BiConsumer, udpIsReceiving: Consumer) { + var isReceiving = false + if (isReady){ + + val thread1 = Thread{ + Logger.info { "UDPReceiverToFile started, listening on ${socket?.localSocketAddress} , saving to $outputFilePath" } + + PipedOutputStream(pipeIn).use { pipeOut -> + while (isReceiving) { + try{ + val xx = DatagramPacket(ByteArray(1500),1500) + socket?.receive(xx) + if (xx.address.hostAddress!= senderIP) continue + if (xx.length < 1) continue + pipeOut.write(xx.data, 0, xx.length) + bytesReceived += xx.length + if (!isReceiving){ + isReceiving = true + udpIsReceiving.accept(true) + } + } catch (e : Exception){ + Logger.error { "Error receiving UDP packet: ${e.message}" } + continue + } + } + } + Logger.info { "UDPReceiverToFile ended" } + } + thread1.name = "UDPReceiverToFile Thread1 $senderIP $outputFilePath" + thread1.isDaemon = true + thread1.start() + + val thread2 = Thread{ + bass.BASS_SetDevice(0) // Set to No Sound device, we are not playing audio + val streamhandle = bass.BASS_StreamCreate(samplingrate, channel, 0, streamProc, null) + if (streamhandle!=0){ + bass.BASS_ChannelPlay(streamhandle,false) + val encodehandle = bassenc.BASS_Encode_Start(streamhandle, outputFilePath, BASS_ENCODE_PCM, null, null) + if (encodehandle!=0){ + Logger.info { "UDPReceiverToFile started writing to $outputFilePath" } + callback.accept(true, "UDPReceiverToFile started successfully, writing to $outputFilePath") + while (isReceiving) { + try { + Thread.sleep(1000) // Sleep to avoid busy waiting + } catch (e: InterruptedException) { + Logger.error { "UDPReceiverToFile thread interrupted: ${e.message}" } + break + } + } + bassenc.BASS_Encode_Stop(encodehandle) + bass.BASS_StreamFree(streamhandle) + Logger.info { "UDPReceiverToFile stopped writing to $outputFilePath" } + callback.accept(false, "UDPReceiverToFile stopped successfully, written bytes: $bytesWritten") + } else { + callback.accept(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}") + return@Thread + } + + } else { + callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}") + return@Thread + } + } + thread2.name = "UDPReceiverToFile Thread2 $senderIP $outputFilePath" + thread2.isDaemon = true + thread2.start() + + + + } else callback.accept(false, "UDPReceiverToFile is not ready. Check if the socket was created successfully.") + } + + /** + * Stop UDPReceiverToFile from receiving data. + */ + fun stopReceiving() { + isReceiving = false + } +} \ No newline at end of file diff --git a/src/codes/QuadConsumer.kt b/src/codes/QuadConsumer.kt new file mode 100644 index 0000000..c9d31c4 --- /dev/null +++ b/src/codes/QuadConsumer.kt @@ -0,0 +1,14 @@ +package codes + +@Suppress("unused") +interface QuadConsumer { + /** + * Performs this operation on the given arguments. + * + * @param a the first input argument + * @param b the second input argument + * @param c the third input argument + * @param d the fourth input argument + */ + fun accept(a: A, b: B, c: C, d: D) +} \ No newline at end of file diff --git a/src/codes/TriConsumer.kt b/src/codes/TriConsumer.kt new file mode 100644 index 0000000..092080a --- /dev/null +++ b/src/codes/TriConsumer.kt @@ -0,0 +1,13 @@ +package codes + +@Suppress("unused") +interface TriConsumer { + /** + * Performs this operation on the given arguments. + * + * @param a the first input argument + * @param b the second input argument + * @param c the third input argument + */ + fun accept(a: A, b: B, c: C) +} \ No newline at end of file