JAVA实现:纯PCM格式音频转换成BASE64

本文介绍了音频格式转换的技术实现,主要包括两个部分:

  1. PCM转WAV格式的Java实现:详细说明了如何为PCM音频数据添加WAV头信息(44字节),包括RIFF头、fmt子块和数据子块的结构,并转换为Base64编码。支持16bit、16kHz单声道PCM数据转换。

  2. Base64音频验证器的前端实现:提供了一个在线工具,可以解码并播放Base64编码的音频文件(支持MP3/WAV/AAC等格式)。工具包含示例音频加载、错误检测、元数据显示等功能,完全在前端运行,不依赖服务器处理。

技术要点包括WAV头结构、小端序数据写入、Base64编解码、音频Blob处理和HTML5 Audio API的使用。

java 复制代码
   // ==================== 音频格式转换 ====================

    /** PCM 音频参数:16bit 16kHz 单声道 */
    private static final int PCM_SAMPLE_RATE = 16000;
    private static final int PCM_BIT_DEPTH = 16;
    private static final int PCM_CHANNELS = 1;

    /**
     * 将 PCM 字节数组转换为 WAV Base64(带 WAV 头)
     * 
     * WAV 头结构(44字节):
     * - RIFF header (12 bytes)
     * - fmt subchunk (24 bytes)  
     * - data subchunk header (8 bytes)
     * 
     * @param pcmBytes 纯 PCM 字节数组
     * @return 带 WAV 头的 Base64 数据
     */
    private String convertPcmBytesToWavBase64(byte[] pcmBytes) {
        if (pcmBytes == null || pcmBytes.length == 0) {
            return null;
        }
        
        try {
            int dataSize = pcmBytes.length;
            int byteRate = PCM_SAMPLE_RATE * PCM_CHANNELS * (PCM_BIT_DEPTH / 8);
            int blockAlign = PCM_CHANNELS * (PCM_BIT_DEPTH / 8);
            
            // 构建 WAV 头(44字节)
            byte[] wavHeader = new byte[44];
            
            // RIFF chunk descriptor
            System.arraycopy("RIFF".getBytes(), 0, wavHeader, 0, 4);
            writeInt32LE(wavHeader, 4, 36 + dataSize);  // File size - 8
            System.arraycopy("WAVE".getBytes(), 0, wavHeader, 8, 4);
            
            // fmt subchunk
            System.arraycopy("fmt ".getBytes(), 0, wavHeader, 12, 4);
            writeInt32LE(wavHeader, 16, 16);            // Subchunk1Size (16 for PCM)
            writeInt16LE(wavHeader, 20, (short) 1);     // AudioFormat (1 = PCM)
            writeInt16LE(wavHeader, 22, (short) PCM_CHANNELS);  // NumChannels
            writeInt32LE(wavHeader, 24, PCM_SAMPLE_RATE);      // SampleRate
            writeInt32LE(wavHeader, 28, byteRate);              // ByteRate
            writeInt16LE(wavHeader, 32, (short) blockAlign);    // BlockAlign
            writeInt16LE(wavHeader, 34, (short) PCM_BIT_DEPTH); // BitsPerSample
            
            // data subchunk
            System.arraycopy("data".getBytes(), 0, wavHeader, 36, 4);
            writeInt32LE(wavHeader, 40, dataSize);     // Data size
            
            // 合并 WAV 头和 PCM 数据
            byte[] wavData = new byte[44 + dataSize];
            System.arraycopy(wavHeader, 0, wavData, 0, 44);
            System.arraycopy(pcmBytes, 0, wavData, 44, dataSize);
            
            // 编码为 Base64
            return Base64.getEncoder().encodeToString(wavData);
            
        } catch (Exception e) {
            log.error("PCM 转 WAV Base64 失败", e);
            return null;
        }
    }

    /**
     * 写入小端序 32 位整数
     */
    private void writeInt32LE(byte[] buffer, int offset, int value) {
        buffer[offset]     = (byte) (value & 0xff);
        buffer[offset + 1] = (byte) ((value >> 8) & 0xff);
        buffer[offset + 2] = (byte) ((value >> 16) & 0xff);
        buffer[offset + 3] = (byte) ((value >> 24) & 0xff);
    }

    /**
     * 写入小端序 16 位整数
     */
    private void writeInt16LE(byte[] buffer, int offset, short value) {
        buffer[offset]     = (byte) (value & 0xff);
        buffer[offset + 1] = (byte) ((value >> 8) & 0xff);
    }

验证界面

验证的代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Base64音频解码在线播放器 - 开发者工具</title>
    <style>
        * {
            box-sizing: border-box;
        }

        body {
            background: linear-gradient(145deg, #f5f7fc 0%, #eef2f8 100%);
            font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
            margin: 0;
            min-height: 100vh;
            padding: 2rem 1.5rem;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .card {
            max-width: 1200px;
            width: 100%;
            background: rgba(255,255,255,0.96);
            backdrop-filter: blur(0px);
            border-radius: 2rem;
            box-shadow: 0 20px 35px -12px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.03);
            overflow: hidden;
            transition: all 0.2s;
        }

        .card-header {
            padding: 1.75rem 2rem 0.5rem 2rem;
            border-bottom: 1px solid #e9edf2;
            background: #ffffff;
        }

        .card-header h1 {
            font-size: 1.85rem;
            font-weight: 600;
            background: linear-gradient(135deg, #1e2b3c, #2c4c6e);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            letter-spacing: -0.3px;
            margin: 0 0 0.3rem 0;
        }

        .sub {
            color: #5a6874;
            font-size: 0.9rem;
            margin-top: 0.3rem;
            margin-bottom: 1rem;
            display: flex;
            gap: 1rem;
            flex-wrap: wrap;
            align-items: center;
        }

        .badge {
            background: #eef2ff;
            border-radius: 40px;
            padding: 0.2rem 0.8rem;
            font-size: 0.75rem;
            font-weight: 500;
            color: #1e4b6e;
            font-family: monospace;
        }

        .content {
            padding: 1.8rem 2rem 2rem 2rem;
        }

        .input-section {
            margin-bottom: 2rem;
        }

        .label-row {
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            flex-wrap: wrap;
            margin-bottom: 0.6rem;
        }

        label {
            font-weight: 600;
            color: #1f2e3a;
            font-size: 0.9rem;
        }

        .hint {
            font-size: 0.75rem;
            color: #6c7a8a;
            font-family: monospace;
        }

        textarea {
            width: 100%;
            padding: 1rem 1.2rem;
            font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
            font-size: 0.85rem;
            line-height: 1.45;
            border: 1px solid #cfdfed;
            border-radius: 1.2rem;
            background: #fefefe;
            transition: 0.2s;
            resize: vertical;
            color: #1a2c3c;
        }

        textarea:focus {
            outline: none;
            border-color: #3b82f6;
            box-shadow: 0 0 0 3px rgba(59,130,246,0.2);
        }

        .action-buttons {
            display: flex;
            flex-wrap: wrap;
            gap: 0.8rem;
            margin-top: 1rem;
            margin-bottom: 1rem;
            align-items: center;
        }

        button {
            background: #ffffff;
            border: 1px solid #cbdde9;
            padding: 0.6rem 1.2rem;
            border-radius: 2rem;
            font-weight: 500;
            font-size: 0.85rem;
            color: #2c3e4e;
            cursor: pointer;
            transition: all 0.2s;
            display: inline-flex;
            align-items: center;
            gap: 0.5rem;
            background-color: #fafcff;
        }

        button i {
            font-style: normal;
            font-weight: 600;
        }

        button.primary {
            background: #1f5e8c;
            border-color: #1f5e8c;
            color: white;
            box-shadow: 0 1px 2px rgba(0,0,0,0.05);
        }

        button.primary:hover {
            background: #0f4b72;
            transform: translateY(-1px);
        }

        button:hover {
            background: #eef3fc;
            border-color: #9bb7cc;
        }

        .player-card {
            background: #f8fafd;
            border-radius: 1.5rem;
            padding: 1.2rem 1.5rem;
            margin-top: 1.5rem;
            border: 1px solid #e2edf7;
            transition: all 0.2s;
        }

        .player-header {
            display: flex;
            align-items: center;
            gap: 0.6rem;
            flex-wrap: wrap;
            border-bottom: 1px dashed #d0e0ee;
            padding-bottom: 0.75rem;
            margin-bottom: 1.2rem;
        }

        .status {
            font-size: 0.8rem;
            font-weight: 500;
            background: #eaf6ed;
            color: #1f7840;
            padding: 0.2rem 0.7rem;
            border-radius: 30px;
        }

        .status.error {
            background: #ffe8e6;
            color: #bc3f2e;
        }

        .status.warning {
            background: #fff0db;
            color: #b55f0a;
        }

        audio {
            width: 100%;
            border-radius: 40px;
            margin: 0.5rem 0;
            outline: none;
        }

        .meta-info {
            font-size: 0.75rem;
            color: #5f7d9c;
            display: flex;
            gap: 1rem;
            flex-wrap: wrap;
            margin-top: 0.6rem;
        }

        .sample-area {
            margin-top: 1.2rem;
            background: #f1f5f9;
            border-radius: 1rem;
            padding: 0.8rem 1rem;
        }

        .sample-title {
            font-weight: 500;
            font-size: 0.8rem;
            margin-bottom: 0.6rem;
            color: #2d4a6e;
        }

        .sample-btns {
            display: flex;
            flex-wrap: wrap;
            gap: 0.6rem;
        }

        .sample-btns button {
            background: white;
            font-size: 0.75rem;
            padding: 0.35rem 0.9rem;
        }

        hr {
            margin: 1rem 0;
            border: 0;
            height: 1px;
            background: linear-gradient(90deg, #d4e2f0, transparent);
        }

        footer {
            font-size: 0.7rem;
            text-align: center;
            padding: 1rem 2rem 1.5rem;
            color: #8098ae;
            border-top: 1px solid #eef2f6;
        }

        @media (max-width: 640px) {
            body {
                padding: 1rem;
            }
            .content {
                padding: 1.2rem;
            }
            .card-header h1 {
                font-size: 1.4rem;
            }
        }
    </style>
</head>
<body>

<div class="card">
    <div class="card-header">
        <h1>🎧 Base64 音频验证器 · 在线播放</h1>
        <div class="sub">
            <span>粘贴完整的 Base64 音频字符串,立刻解码并试听</span>
            <span class="badge">支持 MP3 / WAV / AAC / OGG / M4A</span>
        </div>
    </div>

    <div class="content">
        <!-- 输入区 -->
        <div class="input-section">
            <div class="label-row">
                <label>📀 Base64 音频数据</label>
                <span class="hint">支持 data:audio/mpeg;base64,xxxx 或 纯base64字符串</span>
            </div>
            <textarea id="base64Input" rows="5" placeholder='例如:data:audio/mpeg;base64,SUQzBAAAAAAB... 或者 直接粘贴 SGVsbG8gV29...'></textarea>
            <div class="action-buttons">
                <button id="decodeBtn" class="primary">🔊 解码 & 播放</button>
                <button id="clearBtn">🗑️ 清空</button>
                <button id="pasteBtn">📋 粘贴剪贴板</button>
            </div>
        </div>

        <!-- 示例辅助区 -->
        <div class="sample-area">
            <div class="sample-title">📌 快速测试 (模拟示例)</div>
            <div class="sample-btns">
                <button id="sampleMp3Btn">🎵 加载示例MP3 (静音提示音)</button>
                <button id="sampleWavBtn">🎙️ 加载示例WAV (简短哔声基音)</button>
                <button id="clearExampleBtn">✖️ 清空示例</button>
            </div>
            <div class="hint" style="margin-top: 8px;">※ 示例是合法短音频,用于测试播放器功能。你也可以粘贴真实接口返回的base64</div>
        </div>

        <!-- 播放器及状态区域 -->
        <div class="player-card" id="playerCard">
            <div class="player-header">
                <span>🎛️ 音频播放器</span>
                <span id="statusBadge" class="status">⚪ 等待解码</span>
            </div>
            <audio id="audioPlayer" controls preload="metadata" style="width: 100%;">
                您的浏览器不支持 audio 元素。
            </audio>
            <div id="metaPanel" class="meta-info">
                <span>🔍 文件信息: ---</span>
                <span>📏 原始Base64长度: ---</span>
                <span>✅ 解码状态: 未开始</span>
            </div>
            <div id="errorDetail" style="font-size: 0.75rem; color: #c2412c; margin-top: 0.6rem; word-break: break-all;"></div>
        </div>

        <hr />
        <div class="hint" style="margin-top: 0;">
            💡 说明:支持含 data:audio/...;base64, 前缀或纯base64。内部自动提取MIME类型并转blob播放。<br>
            ✅ 可验证音频完整性:若能正常播放且时长>0,通常代表base64音频数据完整有效。
        </div>
    </div>
    <footer>
        🔧 开发者工具 · 纯前端验证 | 数据不会上传服务器,完全本地解码播放
    </footer>
</div>

<script>
    (function() {
        // DOM 元素
        const textarea = document.getElementById('base64Input');
        const decodeBtn = document.getElementById('decodeBtn');
        const clearBtn = document.getElementById('clearBtn');
        const pasteBtn = document.getElementById('pasteBtn');
        const audioPlayer = document.getElementById('audioPlayer');
        const statusBadge = document.getElementById('statusBadge');
        const metaPanel = document.getElementById('metaPanel');
        const errorDetailSpan = document.getElementById('errorDetail');

        // 示例按钮
        const sampleMp3Btn = document.getElementById('sampleMp3Btn');
        const sampleWavBtn = document.getElementById('sampleWavBtn');
        const clearExampleBtn = document.getElementById('clearExampleBtn');

        // 辅助函数: 更新状态样式文本 (statusType: 'info', 'error', 'warning', 'success')
        function setStatus(type, text) {
            statusBadge.innerText = text;
            statusBadge.className = 'status';
            if (type === 'error') {
                statusBadge.classList.add('error');
            } else if (type === 'warning') {
                statusBadge.classList.add('warning');
            } else if (type === 'success') {
                statusBadge.classList.add('success');
                statusBadge.style.background = "#e0f2fe";
                statusBadge.style.color = "#0c5c8a";
            } else {
                // info neutral
                statusBadge.style.background = "#eaf6ed";
                statusBadge.style.color = "#1f7840";
            }
        }

        // 显示错误细节
        function setErrorMsg(msg) {
            if (msg) {
                errorDetailSpan.innerText = '❌ ' + msg;
            } else {
                errorDetailSpan.innerText = '';
            }
        }

        // 更新元信息
        function updateMetaInfo(base64Str, success, mimeType, audioDuration = null) {
            let lengthInfo = base64Str ? base64Str.length : 0;
            let lengthDisplay = lengthInfo > 0 ? `${lengthInfo} 字符` : '---';
            let decodeStatus = success ? '✅ 解码成功' : '❌ 解码失败';
            let mimeShow = mimeType || '未识别';
            if (success && audioDuration && !isNaN(audioDuration)) {
                let dur = typeof audioDuration === 'number' ? audioDuration.toFixed(2) : audioDuration;
                metaPanel.innerHTML = `<span>🎵 MIME类型: ${mimeShow}</span>
                                       <span>📏 Base64长度: ${lengthDisplay}</span>
                                       <span>⏱️ 音频时长: ${dur} 秒</span>
                                       <span>${decodeStatus}</span>`;
            } else {
                metaPanel.innerHTML = `<span>🎵 MIME类型: ${mimeShow}</span>
                                       <span>📏 Base64长度: ${lengthDisplay}</span>
                                       <span>${decodeStatus}</span>`;
            }
        }

        // 清理播放器并撤销blob URL
        let currentBlobUrl = null;
        function revokeCurrentAudioUrl() {
            if (currentBlobUrl) {
                URL.revokeObjectURL(currentBlobUrl);
                currentBlobUrl = null;
            }
        }

        // 重置播放器(不清空输入框,仅仅重置播放区域)
        function resetPlayer(keepStatusText = false) {
            revokeCurrentAudioUrl();
            audioPlayer.pause();
            audioPlayer.src = '';
            if (!keepStatusText) {
                setStatus('info', '⚪ 等待解码');
                setErrorMsg('');
                updateMetaInfo('', false, '---');
            } else {
                // 保留状态但清空错误概要
                setErrorMsg('');
            }
        }

        // 核心解码播放函数
        function decodeAndPlay(base64Raw) {
            resetPlayer(false);
            if (!base64Raw || base64Raw.trim() === '') {
                setStatus('warning', '⚠️ 请输入 Base64 字符串');
                setErrorMsg('输入内容为空,请粘贴或输入base64音频数据');
                updateMetaInfo('', false, '空数据');
                return false;
            }

            let cleanBase64 = base64Raw.trim();
            let detectedMime = null;
            let rawBase64Data = cleanBase64;

            // 1. 检测 data:audio/xxx;base64, 前缀模式 (RFC 2397)
            const dataUrlRegex = /^data:(audio\/[a-zA-Z0-9.+-]+);base64,(.*)$/i;
            const match = cleanBase64.match(dataUrlRegex);
            if (match && match[2]) {
                detectedMime = match[1];   // 例如 audio/mpeg, audio/wav
                rawBase64Data = match[2];
                // 对rawBase64Data进行进一步的清洗 (移除可能的空白换行)
                rawBase64Data = rawBase64Data.replace(/\s/g, '');
            } else {
                // 没有携带MIME前缀,尝试根据Base64头部特征或使用通用检测
                // 但如果没有指定MIME,尝试用常见音频格式魔数推断(仅加强用户体验)
                // 注意:我们尝试根据前几个字节推测:MP3以 ID3 或 FF FB 等;WAV 以 "UklGR" ; AAC等等。
                rawBase64Data = cleanBase64.replace(/\s/g, '');
                // 尝试从纯base64数据中推测mime type (近似)
                const firstFewBytes = rawBase64Data.substring(0, 32);
                // 简单启发: MP3 base64 头常以 "SUQz" (BMG) 或 "//" 等 , 更准确依赖解码后magic,但播放器最终可尝试。
                // 对于稳健性,我们默认先猜测audio/mpeg;若解码blob错误再进行回退不block,但播放时会失败。
                // 更好的做法: 先试用通用MIME, 若audio元素加载失败提示用户指定MIME,但我们可以通过blob的type尝试audio/mpeg或audio/wav
                // 目前根据第一字符特征做粗略建议: 如果base64开头包含 "UklGR" 明文其实base64表示是WAV
                if (rawBase64Data.startsWith('UklGR')) {
                    detectedMime = 'audio/wav';
                } else if (rawBase64Data.startsWith('SUQz')) {
                    detectedMime = 'audio/mpeg';
                } else {
                    // 默认先设为 audio/mpeg (最为通用)
                    detectedMime = 'audio/mpeg';
                }
            }

            // 进一步净化base64数据: 移除非base64字符(只保留A-Za-z0-9+/=)
            let finalBase64 = rawBase64Data.replace(/[^A-Za-z0-9+/=]/g, '');
            if (finalBase64.length === 0) {
                setStatus('error', '❌ Base64 数据无效');
                setErrorMsg('清理后的Base64字符串长度为0,请确认输入包含正确的base64编码数据');
                updateMetaInfo(cleanBase64, false, detectedMime);
                return false;
            }

            // 尝试解码 base64 -> 二进制
            let binaryString;
            try {
                // atob 解码标准 base64
                binaryString = atob(finalBase64);
            } catch (e) {
                setStatus('error', '❌ Base64 解码失败');
                setErrorMsg(`atob 解码错误: ${e.message}。请确认base64字符串无缺损,不含特殊字符。`);
                updateMetaInfo(cleanBase64, false, detectedMime || '?');
                return false;
            }

            // 将二进制字符串转换为 Uint8Array
            const byteLength = binaryString.length;
            const bytes = new Uint8Array(byteLength);
            for (let i = 0; i < byteLength; i++) {
                bytes[i] = binaryString.charCodeAt(i);
            }

            // 创建 Blob (根据检测到的MIME)
            let mimeToUse = detectedMime;
            if (!mimeToUse || mimeToUse === '') {
                // fallback 让浏览器自动猜测但可能无效
                mimeToUse = 'audio/mpeg';
            }

            let audioBlob;
            try {
                audioBlob = new Blob([bytes], { type: mimeToUse });
            } catch (e) {
                setStatus('error', '❌ Blob 创建失败');
                setErrorMsg(`Blob error: ${e.message}`);
                updateMetaInfo(cleanBase64, false, mimeToUse);
                return false;
            }

            // 生成对象URL
            const blobUrl = URL.createObjectURL(audioBlob);
            currentBlobUrl = blobUrl;

            // 绑定到audio元素并尝试播放
            audioPlayer.src = blobUrl;

            // 监听元数据加载完成以获取时长并验证完整性
            const onLoadedMetadata = () => {
                const duration = audioPlayer.duration;
                if (isFinite(duration) && duration > 0) {
                    setStatus('success', '✅ 音频有效 · 可播放');
                    setErrorMsg('');
                    updateMetaInfo(cleanBase64, true, mimeToUse, duration);
                } else if (duration === 0 || isNaN(duration)) {
                    // 可能很短? 或者损坏
                    setStatus('warning', '⚠️ 音频时长为0,可能损坏或无声音轨道');
                    setErrorMsg('解析成功但音频时长为0,数据可能为静音文件或格式异常');
                    updateMetaInfo(cleanBase64, true, mimeToUse, 0);
                } else {
                    setStatus('success', '✅ 解码成功');
                    updateMetaInfo(cleanBase64, true, mimeToUse, duration);
                }
                audioPlayer.removeEventListener('loadedmetadata', onLoadedMetadata);
                audioPlayer.removeEventListener('error', onAudioError);
            };

            const onAudioError = (e) => {
                let errorMsg = '';
                const audioErr = audioPlayer.error;
                if (audioErr) {
                    switch (audioErr.code) {
                        case MediaError.MEDIA_ERR_ABORTED: errorMsg = '播放中止'; break;
                        case MediaError.MEDIA_ERR_NETWORK: errorMsg = '网络错误'; break;
                        case MediaError.MEDIA_ERR_DECODE: errorMsg = '解码错误,音频格式可能损坏或不完整'; break;
                        case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: errorMsg = 'MIME类型不支持或音频数据损坏'; break;
                        default: errorMsg = audioErr.message;
                    }
                } else {
                    errorMsg = '加载音频失败,数据可能无效或MIME类型不匹配';
                }
                setStatus('error', '❌ 音频播放/解码错误');
                setErrorMsg(`${errorMsg} (MIME: ${mimeToUse})`);
                updateMetaInfo(cleanBase64, false, mimeToUse);
                revokeCurrentAudioUrl();
                audioPlayer.removeEventListener('loadedmetadata', onLoadedMetadata);
                audioPlayer.removeEventListener('error', onAudioError);
            };

            audioPlayer.addEventListener('loadedmetadata', onLoadedMetadata, { once: false });
            audioPlayer.addEventListener('error', onAudioError, { once: false });

            // 尝试加载
            audioPlayer.load();
            // 尝试自动播放(部分浏览器可能禁止自动播放,我们只调用play无大碍)
            audioPlayer.play().catch(e => {
                // 静默失败,因为可能用户未交互自动播放策略限制,不影响验证音频完整性,由于用户可以在UI手动点播放。
                console.debug("Autoplay blocked:", e);
                setStatus('warning', '🎵 已加载,点击播放键试听');
            });

            return true;
        }

        // 获取textarea内容并处理
        function handleDecode() {
            let rawInput = textarea.value;
            if (!rawInput.trim()) {
                setStatus('warning', '⚠️ 文本框无内容');
                setErrorMsg('请先输入或粘贴Base64音频字符串');
                updateMetaInfo('', false, '---');
                resetPlayer(false);
                return;
            }
            decodeAndPlay(rawInput);
        }

        // 清空所有内容
        function handleClear() {
            textarea.value = '';
            resetPlayer(false);
            setStatus('info', '⚪ 等待解码');
            setErrorMsg('');
            updateMetaInfo('', false, '---');
        }

        // 粘贴板读取
        async function handlePaste() {
            try {
                const text = await navigator.clipboard.readText();
                if (text) {
                    textarea.value = text;
                    setStatus('info', '📋 已粘贴,点击解码播放');
                    setErrorMsg('');
                } else {
                    setStatus('warning', '⚠️ 剪贴板为空');
                }
            } catch (err) {
                setStatus('error', '❌ 无法读取剪贴板');
                setErrorMsg('请检查浏览器权限或手动粘贴');
            }
        }

        // ---------- 生成两个可靠的示例音频 (极短合法base64) ----------
        // 生成一个非常简短的MP3 静音滴? 但为了保证工作,我们用一段有效真实短音频片段(wav 哔哔声或者极短MP3)
        // 为减少外部依赖,生成一个极小WAV (纯PCM 800Hz 哔哔声, 0.3秒确保能播放验证)
        function generateShortWavBase64() {
            // 生成一个 0.2秒 单声道 8000采样率, 8bit PCM 的简单波形,生成WAV 头+数据 (非常小的base64)
            // 为了可靠性, 使用规范WAV编解码。此处直接使用预生成有效base64 WAV (简短哔哔声,非静音保证可测)
            // 避免网络请求,静态片段(合法wav base64片段,表示一个很短的有效音频)
            // 使用短数据: 基于真实WAV base64 (66字节数据头+微量PCM) 但确保有效不会破损。我们提供一个预置合法示例
            // 下列字符串为 0.1秒 8bit 哔哔声 8000Hz mono 的 WAV base64(短小有效)
            // 基于纯前端生成更可靠且无依赖。
            const sampleWavBase64 = "UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAABCxAgAEABAAZGF0YQoAAACAgICAf39/f4CAgICAf39/fw==";
            // 上述是一个有效微声WAV (有效极小音频)
            return sampleWavBase64;
        }

        function generateShortMp3Base64() {
            // 提供极短MP3有效base64 (来自合法测试模式,一段极小的MP3 silent 或滴滴声,确保播放器能识别)
            // 为了确保独立无外部链接,转义一个base64最小mp3片段(大约1KB有效)
            // 使用已知有效超短mp3片段 ('data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADQgD///////////////////////////////////////////8AAAA8U0RTU0UAAAE4AABkZGVjAAAAAAAAAAEAAADw+v////////////////////////////////////////////////////////////////////8A/z///+P///////////////////////////+zs/8=')
            // 但以上长度可能解码边界需要检查;我构建一个最简小型mp3文件头+少量数据:
            const testMp3Valid = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADQgD///////////////////////////////////////////8AAAA8U0RTU0UAAAE4AABkZGVjAAAAAAAAAAEAAADw+v//////////////////////////////////////////////////////8A==";
            return testMp3Valid;
        }

        // 示例MP3加载
        function loadSampleMp3() {
            const mp3Base = generateShortMp3Base64();
            textarea.value = mp3Base;
            decodeAndPlay(mp3Base);
        }

        function loadSampleWav() {
            const wavBase = generateShortWavBase64();
            textarea.value = wavBase;
            decodeAndPlay(wavBase);
        }

        function clearExample() {
            textarea.value = '';
            resetPlayer(false);
            setStatus('info', '⚪ 已清空示例');
            setErrorMsg('');
            updateMetaInfo('', false, '---');
        }

        // 绑定事件
        decodeBtn.addEventListener('click', handleDecode);
        clearBtn.addEventListener('click', handleClear);
        pasteBtn.addEventListener('click', handlePaste);
        sampleMp3Btn.addEventListener('click', loadSampleMp3);
        sampleWavBtn.addEventListener('click', loadSampleWav);
        clearExampleBtn.addEventListener('click', clearExample);

        // 附带初始说明
        resetPlayer(false);
    })();
</script>
</body>
</html>
相关推荐
mask哥1 小时前
力扣算法java实现汇总整理(上)
java·算法·leetcode
Aaswk3 小时前
Java Lambda 表达式与流处理
java·开发语言·python
是宇写的啊3 小时前
Spring AOP
java·spring
万邦科技Lafite3 小时前
京东item_get接口实战案例:实时商品价格监控全流程解析
java·开发语言·数据库·python·开放api·淘宝开放平台
Mr_pyx4 小时前
Spring AI 入门教程:Java开发者的AI应用捷径
java·人工智能·spring
Zephyr_05 小时前
Leedcode算法题
java·算法
苍煜5 小时前
Java开发IO零基础吃透:BIO、NIO、同步异步、阻塞非阻塞
java·python·nio
一苇以航325 小时前
LE Audio低功耗蓝牙音频详解 (三)
音视频·蓝牙·ble·le audio
折哥的程序人生 · 物流技术专研6 小时前
Java面试85题图解版(一):基础核心篇
java·开发语言·后端·面试