From 25805cd55625021fdda382854c47f35ec3cef3ba Mon Sep 17 00:00:00 2001 From: loliball <26589867+loliball@users.noreply.github.com> Date: Sat, 31 Aug 2024 23:04:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=E6=95=B4?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Client.kt | 139 ++++++++++++++++++++++------------------------- src/DataPack.kt | 10 ++++ src/Main.kt | 44 +++++++++++---- src/Program.java | 118 ---------------------------------------- src/Server.kt | 78 +++++++++++++------------- 5 files changed, 149 insertions(+), 240 deletions(-) create mode 100644 src/DataPack.kt delete mode 100644 src/Program.java diff --git a/src/Client.kt b/src/Client.kt index b6c0f84..20e06c2 100644 --- a/src/Client.kt +++ b/src/Client.kt @@ -1,7 +1,9 @@ -import io.github.jaredmdobson.concentus.* +import io.github.jaredmdobson.concentus.OpusApplication +import io.github.jaredmdobson.concentus.OpusDecoder +import io.github.jaredmdobson.concentus.OpusEncoder +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -11,85 +13,76 @@ import java.net.Socket import javax.sound.sampled.AudioSystem import kotlin.system.measureTimeMillis -val SERVER_PORT = 7860 -val ECHO_CANCELLATION = true -val PACKET_SAMPLES = 2880 -val SAMPLE_RATE = 48000 -val CHANNELS = 1 -val BUFFER_SIZE = PACKET_SAMPLES * CHANNELS -val AUDIO_FORMAT = AudioConvert.getAudioFormat(SAMPLE_RATE, CHANNELS) - -object Client { - @JvmStatic - fun main(args: Array) { - mainClient() - } -} - -data class DataPack( - val timestamp: Long, - val opus: ByteArray, -) : Serializable - -fun mainClient() = runBlocking { - val socket = Socket() - - val t = measureTimeMillis { - socket.connect(InetSocketAddress("zedo.top", SERVER_PORT)) -// socket.connect(InetSocketAddress("127.0.0.1", SERVER_PORT)) - } - println("connect time $t") - - val out = ObjectOutputStream(socket.getOutputStream()) - val inp = ObjectInputStream(socket.getInputStream()) - - val bufferOut = ByteArray(BUFFER_SIZE * 2) - val targetDataLine = AudioSystem.getTargetDataLine(AUDIO_FORMAT) - targetDataLine.open(AUDIO_FORMAT, bufferOut.size) - targetDataLine.start() - - val bufferIn = ByteArray(BUFFER_SIZE * 2) - val sourceDataLine = AudioSystem.getSourceDataLine(AUDIO_FORMAT) - sourceDataLine.open(AUDIO_FORMAT, bufferIn.size) - sourceDataLine.start() - - val decoder = OpusDecoder(SAMPLE_RATE, CHANNELS) - val encoder = OpusEncoder(SAMPLE_RATE, CHANNELS, OpusApplication.OPUS_APPLICATION_AUDIO).also { - it.bitrate = 96000 - it.forceMode = OpusMode.MODE_CELT_ONLY - it.signalType = OpusSignal.OPUS_SIGNAL_MUSIC - it.complexity = 0 - } - - launch(Dispatchers.IO) { - while (true) { - targetDataLine.read(bufferOut, 0, bufferOut.size) - try { - out.writeObject(encoder.encode(bufferOut)) - out.flush() - } catch (e: Exception) { - e.printStackTrace() - System.exit(0) - } +class Client { + companion object { + @JvmStatic + fun main(args: Array) { + Client().mainClient() } } - launch(Dispatchers.IO) { - while (true) { - try { + val bufferOut = ByteArray(BUFFER_SIZE) + val targetDataLine = AudioSystem.getTargetDataLine(AUDIO_FORMAT).also { + it.open(AUDIO_FORMAT, bufferOut.size) + it.start() + } + + val bufferIn = ByteArray(BUFFER_SIZE) + val sourceDataLine = AudioSystem.getSourceDataLine(AUDIO_FORMAT).also { + it.open(AUDIO_FORMAT, bufferIn.size) + it.start() + } + + val decoder = OpusDecoder(SAMPLE_RATE, CHANNELS) + val encoder = OpusEncoder(SAMPLE_RATE, CHANNELS, OpusApplication.OPUS_APPLICATION_AUDIO).also { + it.bitrate = OPUS_BITRATE + it.forceMode = OPUS_MODE + it.signalType = OPUS_SIGNAL_TYPE + it.complexity = OPUS_COMPLEXITY + } + + val scope = CoroutineScope(Dispatchers.IO) + + fun mainClient() { + val socket = Socket() + + val t = measureTimeMillis { + socket.connect(InetSocketAddress(SERVER_HOST, SERVER_PORT)) + } + "connect time $t".loge() + + val out = ObjectOutputStream(socket.getOutputStream()) + val inp = ObjectInputStream(socket.getInputStream()) + + scope.launch { + while (true) { + targetDataLine.read(bufferOut, 0, bufferOut.size) + try { + out.writeObject(encoder.encode(bufferOut)) + out.flush() + } catch (e: Exception) { + e.printStackTrace() + System.exit(0) + } + } + } + + scope.launch { + while (true) { + try { // if (`in`!!.available() > bufferIn.size * 2) { // `in`!!.skip(`in`!!.available().toLong() - bufferIn.size) // println("丢弃") // } // `in`!!.read(bufferIn, 0, bufferIn.size) - val dataPack = inp.readObject() as DataPack - decoder.decode(dataPack, bufferIn) - sourceDataLine.write(bufferIn, 0, bufferIn.size) - } catch (e: IOException) { - e.printStackTrace() - System.exit(0) + val dataPack = inp.readObject() as DataPack + decoder.decode(dataPack, bufferIn) + sourceDataLine.write(bufferIn, 0, bufferIn.size) + } catch (e: IOException) { + e.printStackTrace() + System.exit(0) + } } } } - -} \ No newline at end of file +} diff --git a/src/DataPack.kt b/src/DataPack.kt new file mode 100644 index 0000000..d6d82fc --- /dev/null +++ b/src/DataPack.kt @@ -0,0 +1,10 @@ +import java.io.Serializable + +/** + * Created by LoliBall on 2024/8/31 23:02. + * https://github.com/WhichWho + */ +data class DataPack( + val timestamp: Long, + val opus: ByteArray, +) : Serializable \ No newline at end of file diff --git a/src/Main.kt b/src/Main.kt index ff903ae..081616f 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,30 +1,50 @@ -import io.github.jaredmdobson.concentus.OpusDecoder -import io.github.jaredmdobson.concentus.OpusEncoder +import io.github.jaredmdobson.concentus.* import kotlinx.coroutines.* /** * Created by LoliBall on 2024/8/31 5:26. * https://github.com/WhichWho */ + +//val SERVER_HOST = "zedo.top" +val SERVER_HOST = "127.0.0.1" +val SERVER_PORT = 7860 +val ECHO_CANCELLATION = true // 回声消除 + +// 2.5, 5, 10, 20, 40, 60 ms +// 120, 240, 480, 960, 1920, 2880 +val PACKET_SAMPLES = 2880 // 48000Hz * 60ms = 2880 +val SAMPLE_RATE = 48000 // 8000、12000、16000、24000、48000 +val CHANNELS = 1 +val BUFFER_SIZE = PACKET_SAMPLES * CHANNELS * 2 // * 2是short | 48000Hz * 2(short) * 60ms = 5760B +val AUDIO_FORMAT = AudioConvert.getAudioFormat(SAMPLE_RATE, CHANNELS) + +val OPUS_BITRATE = OpusConstants.OPUS_AUTO // 500 - 300000 +val OPUS_COMPLEXITY = 10 // 0: 快,质量差 10: 慢,质量好 +val OPUS_SIGNAL_TYPE = OpusSignal.OPUS_SIGNAL_VOICE +val OPUS_MODE = OpusMode.MODE_HYBRID + fun main() = runBlocking { CoroutineScope(Dispatchers.IO).launch { Server.main(emptyArray()) } delay(1000) Client.main(emptyArray()) + delay(Long.MAX_VALUE) } -fun OpusEncoder.encode( - pcm: ByteArray, - opusBuffer: ByteArray = ByteArray(pcm.size) -): DataPack { +val encodeOpusBuffer = mutableMapOf() + +// 1275是opus单帧包最大值 +fun OpusEncoder.encode(pcm: ByteArray): DataPack { + val opusBuffer = encodeOpusBuffer[this] ?: ByteArray(1275).also { encodeOpusBuffer[this] = it } val bytesEncoded = encode(pcm, 0, PACKET_SAMPLES, opusBuffer, 0, opusBuffer.size) + "compress rate: %.2f%%".format(bytesEncoded / pcm.size.toFloat() * 100).loge() return DataPack(System.currentTimeMillis(), opusBuffer.sliceArray(0.. T.loge() = also { println(this) } \ No newline at end of file diff --git a/src/Program.java b/src/Program.java deleted file mode 100644 index fb283e2..0000000 --- a/src/Program.java +++ /dev/null @@ -1,118 +0,0 @@ -import io.github.jaredmdobson.concentus.*; - -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; - -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ - -/** - * @author lostromb - */ -public class Program { - - /** - * @param args the command line arguments - */ - public static void main(String[] args) { - test(); - } - - public static void test() { - try { - FileInputStream fileIn = new FileInputStream("a.pcm"); - OpusEncoder encoder = new OpusEncoder(48000, 1, OpusApplication.OPUS_APPLICATION_AUDIO); - encoder.setBitrate(96000); - encoder.setForceMode(OpusMode.MODE_CELT_ONLY); - encoder.setSignalType(OpusSignal.OPUS_SIGNAL_MUSIC); - encoder.setComplexity(0); - - OpusDecoder decoder = new OpusDecoder(48000, 1); - - FileOutputStream fileOut = new FileOutputStream("b.pcm"); - int packetSamples = 960; - byte[] inBuf = new byte[packetSamples * 2 * 2]; - byte[] data_packet = new byte[1275]; - long start = System.currentTimeMillis(); - long size = 0; - while (fileIn.available() >= inBuf.length) { - int bytesRead = fileIn.read(inBuf, 0, inBuf.length); - short[] pcm = BytesToShorts(inBuf, 0, inBuf.length); - int bytesEncoded = encoder.encode(pcm, 0, packetSamples, data_packet, 0, 1275); - //System.out.println(bytesEncoded + " bytes encoded"); - - size += bytesEncoded; - - int samplesDecoded = decoder.decode(data_packet, 0, bytesEncoded, pcm, 0, packetSamples, false); - //System.out.println(samplesDecoded + " samples decoded"); - byte[] bytesOut = ShortsToBytes(pcm); - fileOut.write(bytesOut, 0, bytesOut.length); - } - - long end = System.currentTimeMillis(); - System.out.println("Time was " + (end - start) + "ms"); - fileIn.close(); - fileOut.close(); - System.out.println("Done!"); - } catch (IOException e) { - System.out.println(e.getMessage()); - } catch (OpusException e) { - System.out.println(e.getMessage()); - } - } - - /// - /// Converts interleaved byte samples (such as what you get from a capture device) - /// into linear short samples (that are much easier to work with) - /// - /// - /// - public static short[] BytesToShorts(byte[] input) { - return BytesToShorts(input, 0, input.length); - } - - /// - /// Converts interleaved byte samples (such as what you get from a capture device) - /// into linear short samples (that are much easier to work with) - /// - /// - /// - public static short[] BytesToShorts(byte[] input, int offset, int length) { - short[] processedValues = new short[length / 2]; - for (int c = 0; c < processedValues.length; c++) { - short a = (short) (((int) input[(c * 2) + offset]) & 0xFF); - short b = (short) (((int) input[(c * 2) + 1 + offset]) << 8); - processedValues[c] = (short) (a | b); - } - - return processedValues; - } - - /// - /// Converts linear short samples into interleaved byte samples, for writing to a file, waveout device, etc. - /// - /// - /// - public static byte[] ShortsToBytes(short[] input) { - return ShortsToBytes(input, 0, input.length); - } - - /// - /// Converts linear short samples into interleaved byte samples, for writing to a file, waveout device, etc. - /// - /// - /// - public static byte[] ShortsToBytes(short[] input, int offset, int length) { - byte[] processedValues = new byte[length * 2]; - for (int c = 0; c < length; c++) { - processedValues[c * 2] = (byte) (input[c + offset] & 0xFF); - processedValues[c * 2 + 1] = (byte) ((input[c + offset] >> 8) & 0xFF); - } - - return processedValues; - } -} diff --git a/src/Server.kt b/src/Server.kt index e9b749d..abae919 100644 --- a/src/Server.kt +++ b/src/Server.kt @@ -1,7 +1,9 @@ -import io.github.jaredmdobson.concentus.* +import io.github.jaredmdobson.concentus.OpusApplication +import io.github.jaredmdobson.concentus.OpusDecoder +import io.github.jaredmdobson.concentus.OpusEncoder +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -13,60 +15,62 @@ import java.util.concurrent.CopyOnWriteArrayList val clientAudios = CopyOnWriteArrayList() object Server { + @JvmStatic fun main(args: Array) { mainServer() } -} -fun mainServer() = runBlocking { - //混音 - launch(Dispatchers.IO) { - while (true) { - for (client in clientAudios) { - if (!client.isConnected) { - clientAudios -= client - continue - } - client.read() - } + fun mainServer() { - for (client in clientAudios) { - launch(Dispatchers.IO) { - for (audio in clientAudios) { - if (audio != client || !ECHO_CANCELLATION) { - client.mix(audio.sample) - } + CoroutineScope(Dispatchers.IO).launch { + while (true) { + for (client in clientAudios) { + if (!client.isConnected) { + clientAudios -= client + continue + } + client.read() + } + + for (client in clientAudios) { + launch { + for (audio in clientAudios) { + if (audio != client || !ECHO_CANCELLATION) { + client.mix(audio.sample) + } + } + client.send() } - client.send() } } } + + val sever = ServerSocket(SERVER_PORT) + while (true) { + val socket = sever.accept() + println("Accepted connection from " + socket.remoteSocketAddress) + clientAudios += ClientAudio(socket) + } } - val sever = ServerSocket(SERVER_PORT) - while (true) { - val socket = sever.accept() - println("Accepted connection from " + socket.remoteSocketAddress) - clientAudios += ClientAudio(socket) - } } class ClientAudio(val socket: Socket) { val inp = ObjectInputStream(socket.getInputStream()) val out = ObjectOutputStream(socket.getOutputStream()) - val buffer = ByteArray(BUFFER_SIZE * 2) - val sample = FloatArray(BUFFER_SIZE) - val mixSample = FloatArray(BUFFER_SIZE) - val mixBuffer = ByteArray(BUFFER_SIZE * 2) + val buffer = ByteArray(BUFFER_SIZE) + val sample = FloatArray(PACKET_SAMPLES) + val mixSample = FloatArray(PACKET_SAMPLES) + val mixBuffer = ByteArray(BUFFER_SIZE) - val decoder = OpusDecoder(48000, 1) - val encoder = OpusEncoder(48000, 1, OpusApplication.OPUS_APPLICATION_AUDIO).also { - it.bitrate = 96000 - it.forceMode = OpusMode.MODE_CELT_ONLY - it.signalType = OpusSignal.OPUS_SIGNAL_MUSIC - it.complexity = 0 + val decoder = OpusDecoder(SAMPLE_RATE, CHANNELS) + val encoder = OpusEncoder(SAMPLE_RATE, CHANNELS, OpusApplication.OPUS_APPLICATION_AUDIO).also { + it.bitrate = OPUS_BITRATE + it.forceMode = OPUS_MODE + it.signalType = OPUS_SIGNAL_TYPE + it.complexity = OPUS_COMPLEXITY } var isConnected = true