diff --git a/libs/linux-aarch64/libbass.so b/libs/linux-aarch64/libbass.so index ab2a517..a6eadb7 100644 Binary files a/libs/linux-aarch64/libbass.so and b/libs/linux-aarch64/libbass.so differ diff --git a/libs/linux-aarch64/libbassenc.so b/libs/linux-aarch64/libbassenc.so index 82fb7be..048433a 100644 Binary files a/libs/linux-aarch64/libbassenc.so and b/libs/linux-aarch64/libbassenc.so differ diff --git a/libs/linux-aarch64/libbassmix.so b/libs/linux-aarch64/libbassmix.so new file mode 100644 index 0000000..af5a600 Binary files /dev/null and b/libs/linux-aarch64/libbassmix.so differ diff --git a/libs/linux-armhf/libbass.so b/libs/linux-armhf/libbass.so index ec90dc0..57f055f 100644 Binary files a/libs/linux-armhf/libbass.so and b/libs/linux-armhf/libbass.so differ diff --git a/libs/linux-armhf/libbassenc.so b/libs/linux-armhf/libbassenc.so index 20016ee..f721770 100644 Binary files a/libs/linux-armhf/libbassenc.so and b/libs/linux-armhf/libbassenc.so differ diff --git a/libs/linux-armhf/libbassmix.so b/libs/linux-armhf/libbassmix.so new file mode 100644 index 0000000..23d9c67 Binary files /dev/null and b/libs/linux-armhf/libbassmix.so differ diff --git a/libs/linux-x86-64/libbass.so b/libs/linux-x86-64/libbass.so index 3317ed4..90d283a 100644 Binary files a/libs/linux-x86-64/libbass.so and b/libs/linux-x86-64/libbass.so differ diff --git a/libs/linux-x86-64/libbassenc.so b/libs/linux-x86-64/libbassenc.so index bb7240b..bffe081 100644 Binary files a/libs/linux-x86-64/libbassenc.so and b/libs/linux-x86-64/libbassenc.so differ diff --git a/libs/linux-x86-64/libbassmix.so b/libs/linux-x86-64/libbassmix.so new file mode 100644 index 0000000..b90214f Binary files /dev/null and b/libs/linux-x86-64/libbassmix.so differ diff --git a/libs/linux-x86/libbass.so b/libs/linux-x86/libbass.so index 74a204d..88254a9 100644 Binary files a/libs/linux-x86/libbass.so and b/libs/linux-x86/libbass.so differ diff --git a/libs/linux-x86/libbassenc.so b/libs/linux-x86/libbassenc.so index 874faee..b61bb1b 100644 Binary files a/libs/linux-x86/libbassenc.so and b/libs/linux-x86/libbassenc.so differ diff --git a/libs/linux-x86/libbassmix.so b/libs/linux-x86/libbassmix.so new file mode 100644 index 0000000..81c8ecb Binary files /dev/null and b/libs/linux-x86/libbassmix.so differ diff --git a/libs/win32-x86-64/bass.dll b/libs/win32-x86-64/bass.dll index e02c370..16c8e01 100644 Binary files a/libs/win32-x86-64/bass.dll and b/libs/win32-x86-64/bass.dll differ diff --git a/libs/win32-x86-64/bassenc.dll b/libs/win32-x86-64/bassenc.dll index b8f6960..6dd00d7 100644 Binary files a/libs/win32-x86-64/bassenc.dll and b/libs/win32-x86-64/bassenc.dll differ diff --git a/libs/win32-x86-64/bassmix.dll b/libs/win32-x86-64/bassmix.dll new file mode 100644 index 0000000..e19f75f Binary files /dev/null and b/libs/win32-x86-64/bassmix.dll differ diff --git a/libs/win32-x86/bass.dll b/libs/win32-x86/bass.dll index 10114b6..dc5d4d6 100644 Binary files a/libs/win32-x86/bass.dll and b/libs/win32-x86/bass.dll differ diff --git a/libs/win32-x86/bassenc.dll b/libs/win32-x86/bassenc.dll index d61ca83..d27a89a 100644 Binary files a/libs/win32-x86/bassenc.dll and b/libs/win32-x86/bassenc.dll differ diff --git a/libs/win32-x86/bassmix.dll b/libs/win32-x86/bassmix.dll new file mode 100644 index 0000000..e413823 Binary files /dev/null and b/libs/win32-x86/bassmix.dll differ diff --git a/src/audio/BassMix.java b/src/audio/BassMix.java new file mode 100644 index 0000000..8900339 --- /dev/null +++ b/src/audio/BassMix.java @@ -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); +} diff --git a/src/audio/Mp3Encoder.kt b/src/audio/Mp3Encoder.kt index 149f9fb..c105e14 100644 --- a/src/audio/Mp3Encoder.kt +++ b/src/audio/Mp3Encoder.kt @@ -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 diff --git a/src/barix/BarixConnection.kt b/src/barix/BarixConnection.kt index 3ddf2c4..131f7f5 100644 --- a/src/barix/BarixConnection.kt +++ b/src/barix/BarixConnection.kt @@ -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() private val mp3encoder = Mp3Encoder() + private val mp3Consumer = mutableMapOf>() - 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) { + 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()){ diff --git a/src/web/WebApp.kt b/src/web/WebApp.kt index 12fe609..e761e0b 100644 --- a/src/web/WebApp.kt +++ b/src/web/WebApp.kt @@ -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>, val lateinit var app: Javalin lateinit var semiauto: Javalin val objectmapper = jacksonObjectMapper() + val WsContextMap = mutableMapOf() private fun SendReply(context: WsMessageContext, command: String, value: String) { try { @@ -246,43 +245,47 @@ class WebApp(val listenPort: Int, val userlist: List>, 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>, 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")))