java音频实时采集

技术

后端 Spring Boot + WebSocket + 阿里云 NLS 实时音频采集、NLS 语音转文字、超时控制、数据推送
前端 HTML5 + Web Audio API + Canvas 麦克风权限获取、实时波形可视化、识别结果显示
前后端通信 WebSocket 实时传输「波形数据」和「识别结果」
音频格式 PCM 16K 16 位单声道 兼容阿里云 NLS 实时识别要求

 

 

 

整体流程设计

  1. 初始化阶段:前端请求麦克风权限 → 建立 WebSocket 连接 → 后端初始化音频采集、NLS 客户端、30s 超时定时器。
  2. 实时交互阶段:
    • 前端:通过 Web Audio API 分析麦克风流,生成波形数据;接收后端推送的「识别结果」并显示。
    • 后端:采集麦克风音频 → 计算波形振幅数据(推给前端)→ 分块发送音频给 NLS → 接收 NLS 实时识别结果(推给前端)。
  3. 停止阶段:30s 超时或手动停止 → 后端关闭音频采集 / NLS 连接 → 前端停止波形绘制并提示。

后端实现(java+springboot)

maven地址

https://mvnrepository.com/artifact/clojure-interop/javax.sound/1.0.2

核心依赖

<!-- Spring WebSocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 阿里云NLS -->
<dependency>
    <groupId>com.aliyun.nls</groupId>
    <artifactId>nls-sdk-java</artifactId>
    <version>2.2.18</version>
</dependency>
<!-- 音频采集 -->
<dependency>
    <groupId>javax.sound</groupId>
    <artifactId>jsound</artifactId>
    <version>1.0.2</version>
</dependency>

WebSocket 配置(处理实时通信)

package com.whale.cloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置:启用WebSocket端点
 */
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

实时语音处理 WebSocket 服务(核心)

package com.whale.cloud.service;

import com.alibaba.nls.client.NlsClient;
import com.alibaba.nls.client.protocol.InputFormatEnum;
import com.alibaba.nls.client.protocol.SampleRateEnum;
import com.alibaba.nls.client.protocol.asr.SpeechTranscriber;
import com.alibaba.nls.client.protocol.asr.SpeechTranscriberListener;
import com.alibaba.nls.client.protocol.asr.SpeechTranscriberResponse;
import com.whale.cloud.config.NLSConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sound.sampled.*;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 实时语音WebSocket服务:处理波形推送、实时转文字、超时控制
 */
@Slf4j
@Component
@ServerEndpoint("/ws/realtime-speech") // WebSocket端点地址
public class RealtimeSpeechWebSocket {
    // 音频配置(必须与NLS一致:PCM 16K 16位单声道)
    private static final AudioFormat AUDIO_FORMAT = new AudioFormat(
            16000F,    // 采样率:16K
            16,        // 位深度:16位
            1,         // 声道:单声道
            true,      //  signed
            false      //  little-endian
    );
    private static final int BUFFER_SIZE = 3200; // 音频块大小(与NLS匹配)
    private static final int TIMEOUT_SECONDS = 30; // 超时时间:30s

    // 注入NLS配置(单例NlsClient)
    @Autowired
    private NLSConfig nlsConfig;
    @Autowired
    private NlsClient nlsClient;

    // 本地资源
    private Session webSocketSession; // WebSocket会话
    private TargetDataLine micLine;   // 麦克风音频采集线
    private SpeechTranscriber transcriber; // NLS实时识别实例
    private ScheduledExecutorService timeoutExecutor; // 超时定时器
    private boolean isRunning = false; // 运行状态标记


    /**
     * 1. 客户端连接WebSocket时触发
     */
    @OnOpen
    public void onOpen(Session session) {
        this.webSocketSession = session;
        this.isRunning = true;
        log.info("WebSocket连接建立,会话ID:{}", session.getId());

        try {
            // 初始化麦克风采集
            initMicCapture();
            // 初始化NLS实时识别
            initNlsTranscriber();
            // 启动30s超时定时器
            startTimeoutTimer();
            // 启动音频采集+波形计算+NLS推送
            startAudioProcess();

        } catch (Exception e) {
            log.error("初始化实时语音服务失败", e);
            sendMessage("error: " + "初始化失败,请重试");
            closeAllResources();
        }
    }


    /**
     * 2. 初始化麦克风采集
     */
    private void initMicCapture() throws LineUnavailableException {
        DataLine.Info micInfo = new DataLine.Info(TargetDataLine.class, AUDIO_FORMAT);
        // 检查麦克风权限
        if (!AudioSystem.isLineSupported(micInfo)) {
            throw new LineUnavailableException("麦克风不支持当前音频格式(需PCM 16K 16位单声道)");
        }
        // 打开麦克风采集线
        micLine = (TargetDataLine) AudioSystem.getLine(micInfo);
        micLine.open(AUDIO_FORMAT);
        micLine.start();
        log.info("麦克风采集初始化完成");
    }


    /**
     * 3. 初始化NLS实时识别(支持中英文混合)
     */
    private void initNlsTranscriber() {
        // 构建NLS监听器(实时接收识别结果)
        SpeechTranscriberListener listener = new SpeechTranscriberListener() {
            // 中英文混合识别结果(句子结束时推送)
            @Override
            public void onSentenceEnd(SpeechTranscriberResponse response) {
                if ("20000000".equals(response.getStatus())) {
                    String result = response.getTransSentenceText(); // 中英文混合结果
                    sendMessage("result: " + result); // 推给前端显示
                    log.info("实时识别结果:{}", result);
                }
            }

            // 识别失败
            @Override
            public void onFail(SpeechTranscriberResponse response) {
                String errorMsg = "识别失败:" + response.getStatusText();
                log.error(errorMsg);
                sendMessage("error: " + errorMsg);
                closeAllResources();
            }

            // 其他回调(简化,保留必要逻辑)
            @Override public void onTranscriberStart(SpeechTranscriberResponse response) {}
            @Override public void onSentenceBegin(SpeechTranscriberResponse response) {}
            @Override public void onTranscriptionComplete(SpeechTranscriberResponse response) {}
            @Override public void onTranscriptionResultChange(SpeechTranscriberResponse response) {}
        };

        // 创建NLS识别实例
        transcriber = new SpeechTranscriber(nlsClient, listener);
        transcriber.setAppKey(nlsConfig.getAppKey());
        transcriber.setFormat(InputFormatEnum.PCM); // 音频格式PCM
        transcriber.setSampleRate(SampleRateEnum.SAMPLE_RATE_16K); // 16K采样率
        transcriber.setEnablePunctuation(true); // 保留标点(中英文标点)
        transcriber.setEnableITN(false); // 关闭数字转换(避免"123"转"one hundred twenty-three")
        transcriber.setEnableIntermediateResult(false); // 关闭中间结果,只推最终句子

        try {
            transcriber.start(); // 启动NLS识别
            log.info("NLS实时识别初始化完成(支持中英文混合)");
        } catch (IOException e) {
            throw new RuntimeException("NLS启动失败", e);
        }
    }


    /**
     * 4. 启动30s超时定时器
     */
    private void startTimeoutTimer() {
        timeoutExecutor = new ScheduledThreadPoolExecutor(1);
        timeoutExecutor.schedule(() -> {
            if (isRunning) {
                sendMessage("timeout: " + "30秒超时,已自动停止");
                log.info("实时语音超时(30s),自动停止");
                closeAllResources();
            }
        }, TIMEOUT_SECONDS, TimeUnit.SECONDS);
    }


    /**
     * 5. 核心:音频采集+波形计算+NLS推送
     */
    private void startAudioProcess() {
        new Thread(() -> {
            byte[] audioBuffer = new byte[BUFFER_SIZE];
            while (isRunning && micLine != null && micLine.isOpen()) {
                try {
                    // 步骤1:采集音频块(从麦克风)
                    int readLen = micLine.read(audioBuffer, 0, BUFFER_SIZE);
                    if (readLen <= 0) break;

                    // 步骤2:计算波形数据(振幅归一化,用于前端可视化)
                    int[] waveData = calculateWaveData(audioBuffer, readLen);
                    sendMessage("wave: " + arrayToString(waveData)); // 推波形给前端

                    // 步骤3:将音频块推给NLS(实时识别)
                    if (transcriber != null && transcriber.isStarted()) {
                        transcriber.send(audioBuffer, readLen);
                    }

                } catch (Exception e) {
                    log.error("音频处理异常", e);
                    break;
                }
            }
        }, "Audio-Process-Thread").start();
    }


    /**
     * 计算波形数据:将PCM音频块转为振幅数组(归一化到0-100,方便前端绘制)
     */
    private int[] calculateWaveData(byte[] audioBuffer, int readLen) {
        int[] waveData = new int[readLen / 2]; // 16位PCM:2字节=1个样本
        for (int i = 0; i < readLen; i += 2) {
            // 16位PCM数据:拼接2字节为有符号整数(振幅)
            short sample = (short) ((audioBuffer[i] & 0xFF) | (audioBuffer[i + 1] << 8));
            // 振幅取绝对值,归一化到0-100(前端绘制范围)
            int amplitude = Math.min(Math.abs(sample) / 327, 100); // 32768(16位最大值)/100≈327
            waveData[i / 2] = amplitude;
        }
        return waveData;
    }


    /**
     * WebSocket发送消息给前端
     */
    private void sendMessage(String message) {
        if (webSocketSession != null && webSocketSession.isOpen()) {
            try {
                webSocketSession.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("WebSocket消息发送失败", e);
            }
        }
    }


    /**
     * 客户端断开连接时触发
     */
    @OnClose
    public void onClose(SessionCloseReason reason) {
        log.info("WebSocket连接关闭,原因:{}", reason.getReasonPhrase());
        closeAllResources();
    }


    /**
     * 关闭所有资源(麦克风、NLS、定时器、WebSocket)
     */
    private void closeAllResources() {
        this.isRunning = false;

        // 1. 关闭麦克风
        if (micLine != null && micLine.isOpen()) {
            micLine.stop();
            micLine.close();
        }

        // 2. 关闭NLS识别
        if (transcriber != null) {
            transcriber.stop();
            transcriber.close();
        }

        // 3. 关闭超时定时器
        if (timeoutExecutor != null) {
            timeoutExecutor.shutdownNow();
        }

        // 4. 关闭WebSocket
        if (webSocketSession != null && webSocketSession.isOpen()) {
            try {
                webSocketSession.close();
            } catch (IOException e) {
                log.error("WebSocket关闭异常", e);
            }
        }

        log.info("所有实时语音资源已关闭");
    }


    /**
     * 工具方法:数组转字符串(前端解析波形数据用)
     */
    private String arrayToString(int[] array) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < array.length; i++) {
            sb.append(array[i]);
            if (i < array.length - 1) sb.append(",");
        }
        return sb.toString();
    }
}

前端实现(HTML + JS + Canvas)

实时语音波形 + 转文字页面(realtime-speech.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>实时语音波形+转文字</title>
    <style>
        .container { width: 800px; margin: 50px auto; }
        #waveCanvas { width: 100%; height: 150px; border: 1px solid #ccc; background: #f5f5f5; }
        #resultBox { margin-top: 20px; padding: 15px; border: 1px solid #eee; min-height: 50px; font-size: 18px; }
        .status { margin-top: 10px; color: #666; font-size: 14px; }
    </style>
</head>
<body>
    <div class="container">
        <h2>麦克风实时语音(中英文混合)</h2>
        <!-- 波形可视化Canvas -->
        <canvas id="waveCanvas"></canvas>
        <!-- 实时识别结果 -->
        <div id="resultBox">识别结果:</div>
        <!-- 状态提示 -->
        <div class="status">状态:等待连接...(30秒后自动停止)</div>
        <!-- 手动停止按钮 -->
        <button id="stopBtn" style="margin-top:10px; padding:8px 16px; background:#ff4444; color:white; border:none; cursor:pointer;">
            手动停止
        </button>
    </div>

    <script>
        // 1. 初始化DOM元素
        const waveCanvas = document.getElementById('waveCanvas');
        const ctx = waveCanvas.getContext('2d');
        const resultBox = document.getElementById('resultBox');
        const statusText = document.querySelector('.status');
        const stopBtn = document.getElementById('stopBtn');

        // 2. 调整Canvas实际分辨率(避免模糊)
        waveCanvas.width = waveCanvas.offsetWidth * 2;
        waveCanvas.height = waveCanvas.offsetHeight * 2;
        ctx.scale(2, 2); // 缩放绘图上下文

        // 3. WebSocket连接(替换为你的后端地址)
        const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const wsUrl = `${wsProtocol}//${window.location.host}/ws/realtime-speech`;
        let webSocket = new WebSocket(wsUrl);

        // 4. 波形绘制配置
        let waveData = []; // 存储实时波形数据
        const waveColor = '#2196F3'; // 波形颜色
        const waveWidth = 2; // 波形柱宽度
        const waveGap = 1; // 波形柱间距


        /**
         * 5. WebSocket消息处理
         */
        webSocket.onmessage = function(event) {
            const message = event.data;
            if (message.startsWith('wave:')) {
                // 处理波形数据
                const waveStr = message.split(':')[1];
                waveData = waveStr.split(',').map(Number); // 转为数字数组
                drawWaveform(); // 绘制波形

            } else if (message.startsWith('result:')) {
                // 处理识别结果(中英文混合)
                const result = message.split(':')[1];
                resultBox.innerHTML = `识别结果:${result}`;

            } else if (message.startsWith('timeout:')) {
                // 处理超时
                const timeoutMsg = message.split(':')[1];
                statusText.textContent = `状态:${timeoutMsg}`;
                stopBtn.disabled = true;

            } else if (message.startsWith('error:')) {
                // 处理错误
                const errorMsg = message.split(':')[1];
                statusText.textContent = `状态:错误 - ${errorMsg}`;
                resultBox.innerHTML = `识别结果:错误 - ${errorMsg}`;
                stopBtn.disabled = true;
            }
        };

        // WebSocket连接成功
        webSocket.onopen = function() {
            statusText.textContent = '状态:正在录音...(30秒后自动停止)';
        };

        // WebSocket连接关闭
        webSocket.onclose = function() {
            statusText.textContent = '状态:已停止';
            stopBtn.disabled = true;
        };

        // WebSocket错误
        webSocket.onerror = function(error) {
            statusText.textContent = `状态:连接错误 - ${error.message}`;
        };


        /**
         * 6. Canvas绘制实时波形
         */
        function drawWaveform() {
            // 清空Canvas
            ctx.clearRect(0, 0, waveCanvas.offsetWidth, waveCanvas.offsetHeight);
            const canvasHeight = waveCanvas.offsetHeight;
            const canvasWidth = waveCanvas.offsetWidth;

            // 计算波形柱总数(避免超出Canvas宽度)
            const maxWaveCount = Math.floor(canvasWidth / (waveWidth + waveGap));
            const drawData = waveData.slice(0, maxWaveCount); // 截取有效数据

            // 绘制每个波形柱(垂直柱形)
            drawData.forEach((amplitude, index) => {
                const x = index * (waveWidth + waveGap); // 柱形X坐标
                const柱形高度 = (amplitude / 100) * (canvasHeight / 2); // 振幅→高度(归一化)
                const y = (canvasHeight / 2) - (柱形高度 / 2); // 柱形Y坐标(居中)

                // 绘制柱形
                ctx.fillStyle = waveColor;
                ctx.fillRect(x, y, waveWidth, 柱形高度);
            });
        }


        /**
         * 7. 手动停止按钮点击事件
         */
        stopBtn.addEventListener('click', function() {
            if (webSocket.readyState === WebSocket.OPEN) {
                webSocket.close();
            }
            statusText.textContent = '状态:已手动停止';
            this.disabled = true;
        });
    </script>
</body>
</html>

关键功能说明

1. 中英文混合识别

  • 阿里云 NLS 默认支持中英文混合输入,无需额外配置;
  • 后端通过transcriber.setEnableITN(false)关闭 “数字转换”(避免中文 “一百” 转英文 “one hundred”);
  • setEnablePunctuation(true)保留中英文标点(如 “Hello,世界!”)。

2. 30 秒超时自动停止

  • 后端用ScheduledExecutorService定时 30 秒,超时后触发closeAllResources()
  • 超时前会向前端推送timeout: 30秒超时,已自动停止消息,前端更新 UI。

3. 实时波形可视化

  • 后端采集音频时,将 16 位 PCM 数据转为 “振幅数组”(归一化到 0-100),通过 WebSocket 推给前端;
  • 前端用 Web Audio API 辅助音频流处理,Canvas 绘制垂直柱形波形,振幅越大柱形越高。

部署与测试注意事项

  1. 麦克风权限:
    • 前端需在 HTTPS 环境下运行(localhost除外),否则浏览器会拒绝麦克风权限;
    • 测试时需点击浏览器的 “允许麦克风访问” 弹窗。
  2. 音频格式兼容性:
    • 确保后端音频采集格式为PCM 16K 16位单声道(NLS 强制要求),否则识别失败;
    • 部分浏览器 / 麦克风可能不支持该格式,需在前端添加格式检测。
  3. 性能优化:
    • 前端波形绘制频率控制在30fps以内(通过截取波形数据长度避免卡顿);
    • 后端音频采集线程单独运行,避免阻塞 WebSocket 主线程。
  4. 阿里云 NLS 配置:
    • 确保NLSConfig中的appKeyaccessKeyIdaccessKeySecret正确,且已开通 “实时语音识别” 服务;
    • 若需更高识别准确率,可在 NLS 控制台配置 “自定义词汇表”(针对行业术语)。

 

posted @ 2025-09-12 22:13  白玉神驹  阅读(44)  评论(0)    收藏  举报