commit 07/11/2025

This commit is contained in:
2025-11-07 13:54:20 +07:00
parent 6479fe6aee
commit 6b06a67283
22 changed files with 167 additions and 75 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/win32-x86/bassmix.dll Normal file

Binary file not shown.

118
src/audio/BassMix.java Normal file
View File

@@ -0,0 +1,118 @@
package audio;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
@SuppressWarnings("unused")
public interface BassMix extends Library {
BassMix Instance = (BassMix) Native.load("bassmix", BassMix.class);
// Envelope node
class BASS_MIXER_NODE {
public BASS_MIXER_NODE() {}
public BASS_MIXER_NODE(long _pos, float _value) { pos=_pos; value=_value; }
public long pos;
public float value;
}
// Additional BASS_SetConfig options
int BASS_CONFIG_MIXER_BUFFER = 0x10601;
int BASS_CONFIG_MIXER_POSEX = 0x10602;
int BASS_CONFIG_SPLIT_BUFFER = 0x10610;
// BASS_Mixer_StreamCreate flags
int BASS_MIXER_RESUME = 0x1000; // resume stalled immediately upon new/unpaused source
int BASS_MIXER_POSEX = 0x2000; // enable BASS_Mixer_ChannelGetPositionEx support
int BASS_MIXER_NOSPEAKER = 0x4000; // ignore speaker arrangement
int BASS_MIXER_QUEUE = 0x8000; // queue sources
int BASS_MIXER_END = 0x10000; // end the stream when there are no sources
int BASS_MIXER_NONSTOP = 0x20000; // don't stall when there are no sources
// BASS_Mixer_StreamAddChannel/Ex flags
int BASS_MIXER_CHAN_ABSOLUTE = 0x1000; // start is an absolute position
int BASS_MIXER_CHAN_BUFFER = 0x2000; // buffer data for BASS_Mixer_ChannelGetData/Level
int BASS_MIXER_CHAN_LIMIT = 0x4000; // limit mixer processing to the amount available from this source
int BASS_MIXER_CHAN_MATRIX = 0x10000; // matrix mixing
int BASS_MIXER_CHAN_PAUSE = 0x20000; // don't process the source
int BASS_MIXER_CHAN_DOWNMIX = 0x400000; // downmix to stereo/mono
int BASS_MIXER_CHAN_NORAMPIN = 0x800000; // don't ramp-in the start
int BASS_MIXER_BUFFER = BASS_MIXER_CHAN_BUFFER;
int BASS_MIXER_LIMIT = BASS_MIXER_CHAN_LIMIT;
int BASS_MIXER_MATRIX = BASS_MIXER_CHAN_MATRIX;
int BASS_MIXER_PAUSE = BASS_MIXER_CHAN_PAUSE;
int BASS_MIXER_DOWNMIX = BASS_MIXER_CHAN_DOWNMIX;
int BASS_MIXER_NORAMPIN = BASS_MIXER_CHAN_NORAMPIN;
// Mixer attributes
int BASS_ATTRIB_MIXER_LATENCY = 0x15000;
int BASS_ATTRIB_MIXER_THREADS = 0x15001;
int BASS_ATTRIB_MIXER_VOL = 0x15002;
// Additional BASS_Mixer_ChannelIsActive return values
int BASS_ACTIVE_WAITING = 5;
int BASS_ACTIVE_QUEUED = 6;
// BASS_Split_StreamCreate flags
int BASS_SPLIT_SLAVE = 0x1000; // only read buffered data
int BASS_SPLIT_POS = 0x2000;
// Splitter attributes
int BASS_ATTRIB_SPLIT_ASYNCBUFFER = 0x15010;
int BASS_ATTRIB_SPLIT_ASYNCPERIOD = 0x15011;
// Envelope types
int BASS_MIXER_ENV_FREQ = 1;
int BASS_MIXER_ENV_VOL = 2;
int BASS_MIXER_ENV_PAN = 3;
int BASS_MIXER_ENV_LOOP = 0x10000; // flag: loop
int BASS_MIXER_ENV_REMOVE = 0x20000; // flag: remove at end
// Additional sync types
int BASS_SYNC_MIXER_ENVELOPE = 0x10200;
int BASS_SYNC_MIXER_ENVELOPE_NODE = 0x10201;
int BASS_SYNC_MIXER_QUEUE = 0x10202;
// Additional BASS_Mixer_ChannelSetPosition flag
int BASS_POS_MIXER_RESET = 0x10000; // flag: clear mixer's playback buffer
// Additional BASS_Mixer_ChannelGetPosition mode
int BASS_POS_MIXER_DELAY = 5;
// BASS_CHANNELINFO types
int BASS_CTYPE_STREAM_MIXER = 0x10800;
int BASS_CTYPE_STREAM_SPLIT = 0x10801;
int BASS_Mixer_GetVersion();
int BASS_Mixer_StreamCreate(int freq, int chans, int flags);
boolean BASS_Mixer_StreamAddChannel(int handle, int channel, int flags);
boolean BASS_Mixer_StreamAddChannelEx(int handle, int channel, int flags, long start, long length);
int BASS_Mixer_StreamGetChannels(int handle, int[] channels, int count);
int BASS_Mixer_ChannelGetMixer(int handle);
int BASS_Mixer_ChannelIsActive(int handle);
int BASS_Mixer_ChannelFlags(int handle, int flags, int mask);
boolean BASS_Mixer_ChannelRemove(int handle);
boolean BASS_Mixer_ChannelSetPosition(int handle, long pos, int mode);
long BASS_Mixer_ChannelGetPosition(int handle, int mode);
long BASS_Mixer_ChannelGetPositionEx(int channel, int mode, int delay);
int BASS_Mixer_ChannelGetLevel(int handle);
boolean BASS_Mixer_ChannelGetLevelEx(int handle, float[] levels, float length, int flags);
int BASS_Mixer_ChannelGetData(int handle, Pointer buffer, int length);
int BASS_Mixer_ChannelSetSync(int handle, int type, long param, Bass.SYNCPROC proc, Object user);
boolean BASS_Mixer_ChannelRemoveSync(int channel, int sync);
boolean BASS_Mixer_ChannelSetMatrix(int handle, float[][] matrix);
boolean BASS_Mixer_ChannelSetMatrixEx(int handle, float[][] matrix, float time);
boolean BASS_Mixer_ChannelGetMatrix(int handle, float[][] matrix);
boolean BASS_Mixer_ChannelSetEnvelope(int handle, int type, BASS_MIXER_NODE[] nodes, int count);
boolean BASS_Mixer_ChannelSetEnvelopePos(int handle, int type, long pos);
long BASS_Mixer_ChannelGetEnvelopePos(int handle, int type, Bass.FloatValue value);
int BASS_Split_StreamCreate(int channel, int flags, int[] chanmap);
int BASS_Split_StreamGetSource(int handle);
int BASS_Split_StreamGetSplits(int handle, int[] splits, int count);
boolean BASS_Split_StreamReset(int handle);
boolean BASS_Split_StreamResetEx(int handle, int offset);
int BASS_Split_StreamGetAvailable(int handle);
}

View File

@@ -48,8 +48,9 @@ class Mp3Encoder(val samplingrate: Int=44100, val channels: Int=1) {
push_handle = bass.BASS_StreamCreate(samplingrate, channels, Bass.BASS_STREAM_DECODE, Pointer(-1), null)
if (push_handle!=0){
Logger.info{"MP3 Encoder initialized with sampling rate $samplingrate Hz and $channels channel(s)" }
mp3_handle = bassencmp3.BASS_Encode_MP3_Start(push_handle, null, BassEnc.BASS_ENCODE_AUTOFREE,proc, null)
val options = "lame -b 128 --cbr --write-xing 0 --nohist --noid3v2 - -"
val flag = BassEnc.BASS_ENCODE_AUTOFREE or BassEnc.BASS_ENCODE_NOHEAD
mp3_handle = bassencmp3.BASS_Encode_MP3_Start(push_handle, null, flag,proc, null)
if (mp3_handle!=0){
callback = cb

View File

@@ -1,7 +1,6 @@
package barix
import audio.Mp3Encoder
import audio.OpusEncoder
import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode
import kotlinx.coroutines.CoroutineScope
@@ -9,7 +8,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.io.PipedOutputStream
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
@@ -26,38 +24,21 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
private val inet = InetSocketAddress(ipaddress, port)
private val maxUDPsize = 1000
private var _tcp: Socket? = null
private val pipeOuts = mutableMapOf<String,PipedOutputStream>()
private val mp3encoder = Mp3Encoder()
private val mp3Consumer = mutableMapOf<String, Consumer<ByteArray>>()
fun AddPipeOut(key: String, pipeOut: PipedOutputStream) {
RemovePipeOut(key)
pipeOuts[key] = pipeOut
println("Added pipeOut $key to BarixConnection $channel ($ipaddress)")
fun Add_Mp3_Consumer(key: String, cb: Consumer<ByteArray>) {
mp3Consumer[key] = cb
}
fun RemovePipeOut(key: String) {
if (pipeOuts.contains(key)){
val pipe = pipeOuts[key]
try {
pipe?.close()
} catch (e: Exception) {
// ignore
}
println("Removed pipeOut $key from BarixConnection $channel ($ipaddress)")
pipeOuts.remove(key)
}
fun Remove_Mp3_Consumer(key: String){
mp3Consumer.remove(key)
}
fun ClearPipeOuts() {
pipeOuts.values.forEach { piped ->
try {
piped.close()
} catch (e: Exception) {
// ignore
}
}
pipeOuts.clear()
fun Exists_Mp3_Consumer(key: String) : Boolean{
return mp3Consumer.containsKey(key)
}
/**
* Buffer remain in bytes
*/
@@ -141,18 +122,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
val bb = ByteBuffer.wrap(data)
if (!mp3encoder.isStarted()) {
mp3encoder.Start { data ->
pipeOuts.keys.forEach { kk ->
val pp = pipeOuts[kk]
try {
pp?.write(data)
pp?.flush()
println("Written ${data.size} bytes to pipeOut $kk")
} catch (e: Exception) {
Logger.error { "Failed to write to pipeOut $kk, message: ${e.message}" }
pp?.close()
pipeOuts.remove(kk)
}
}
mp3Consumer.values.forEach { it.accept(data) }
}
}
while(bb.hasRemaining()){

View File

@@ -3,7 +3,6 @@ package web
import StreamerOutputs
import barix.BarixConnection
import codes.Somecodes
import codes.Somecodes.Companion.Generate_WAV_Header
import codes.Somecodes.Companion.GetSensorsInfo
import codes.Somecodes.Companion.GetUptime
import codes.Somecodes.Companion.ListAudioFiles
@@ -44,10 +43,9 @@ import java.nio.file.Files
import java.time.LocalDateTime
import codes.configKeys
import database.QueueTable
import io.javalin.websocket.WsContext
import org.tinylog.Logger
import java.io.File
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.file.Path
import java.util.UUID
@@ -58,6 +56,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
lateinit var app: Javalin
lateinit var semiauto: Javalin
val objectmapper = jacksonObjectMapper()
val WsContextMap = mutableMapOf<String, WsContext>()
private fun SendReply(context: WsMessageContext, command: String, value: String) {
try {
@@ -246,43 +245,47 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
before { CheckUsers(it) }
}
path("api") {
//TODO gak jalan
//TODO https://stackoverflow.com/questions/70002015/streaming-into-audio-element
path("LiveAudio") {
ws("/ws/{uuid}"){ws ->
ws.onConnect {
ctx ->
val uuid = ctx.pathParam("uuid")
val cookieresult = ctx.cookie("client-stream-id")
println("Ws connected with uuid=$uuid, cookie=$cookieresult")
WsContextMap[uuid] = ctx
}
ws.onClose {
ctx ->
val uuid = ctx.pathParam("uuid")
println("Ws close on uuid=$uuid")
WsContextMap.remove(uuid)
}
ws.onError {
ctx ->
val uuid = ctx.pathParam("uuid")
println("Ws error on uuid=$uuid")
WsContextMap.remove(uuid)
}
}
get("Open/{broadcastzone}") { ctx ->
val param = ctx.pathParam("broadcastzone")
println("LiveAudio Open for zone $param")
println("LiveAudio Open for zone $param from ${ctx.host()}" )
if (param.isNotEmpty()) {
val bc = Get_Barix_Connection_by_ZoneName(param)
if (bc != null) {
ctx.contentType("audio/mpeg")
ctx.header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.header("Pragma", "no-cache")
ctx.header("Expires", "0")
ctx.header("Transfer-Encoding", "chunked")
ctx.header("Connection", "keep-alive")
val key = ctx.cookie("client-id") ?: UUID.randomUUID().toString()
.also { ctx.cookie("client-id", it) }
val key = ctx.cookie("client-stream-id") ?: UUID.randomUUID().toString()
.also { ctx.cookie("client-stream-id", it) }
val responseStream = ctx.res().outputStream
ctx.async {
val pipeIN = PipedInputStream(8192)
val pipeOUT = PipedOutputStream(pipeIN)
bc.AddPipeOut(key, pipeOUT)
println("LiveAudio Open for zone $param SUCCESS")
try{
pipeIN.transferTo(responseStream)
responseStream.flush()
} catch(e: Exception){
} finally{
println("Cleaning up stream for $key")
bc.RemovePipeOut(key)
pipeIN.close()
}
bc.Add_Mp3_Consumer(key){ mp3data ->
WsContextMap[key]?.send(mp3data)
println("Send ${mp3data.size} to $key")
}
ResultMessageString(ctx, 200, key)
} else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Broadcastzone not found")))
} else ctx.status(400)
@@ -292,14 +295,14 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
get("Close/{broadcastzone}") { ctx ->
val param = ctx.pathParam("broadcastzone")
println("LiveAudio Close for zone $param")
val key = ctx.cookie("client-id")
val key = ctx.cookie("client-stream-id")
if (key != null && key.isNotEmpty()) {
if (param.isNotEmpty()) {
val bc = Get_Barix_Connection_by_ZoneName(param)
if (bc != null) {
bc.RemovePipeOut(key)
ctx.result(objectmapper.writeValueAsString(resultMessage("OK")))
bc.Remove_Mp3_Consumer(key)
WsContextMap.remove(key)
ResultMessageString(ctx, 200, "OK")
println("LiveAudio Close for zone $param SUCCESS")
} else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Broadcastzone not found")))