diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..043447c --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,56 @@ +name: Build and Push Docker Image + +on: + push: + branches: [ main ] + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: gmij/audio3a + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./samples/Audio3A.WebApi/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/docs/AUDIO_RECORDING.md b/docs/AUDIO_RECORDING.md new file mode 100644 index 0000000..c224bea --- /dev/null +++ b/docs/AUDIO_RECORDING.md @@ -0,0 +1,296 @@ +# 音频录制和下载功能说明 + +## 概述 + +在通话界面新增了音频录制和下载功能,用户可以录制整个通话过程,并分别下载原声和经过 3A 处理后的音频。 + +## 功能特性 + +### 录制功能 +- ✅ 实时录制通话音频 +- ✅ 同时录制原声和处理后音频 +- ✅ 可随时开始/停止录制 +- ✅ 录制状态可视化(脉冲动画) + +### 下载功能 +- ✅ 下载原声音频(麦克风直接输入) +- ✅ 下载净化后音频(经过 3A 处理) +- ✅ 自动生成带时间戳的文件名 +- ✅ WebM 格式(Opus 编解码器) + +## 使用指南 + +### 1. 进入通话 + +首先创建或加入一个房间,进入通话界面。 + +### 2. 开始录制 + +点击控制栏中的**录制按钮**(摄像机图标): +- 按钮会变为红色 +- 显示脉冲动画表示正在录制 +- 此时会同时录制两路音频流 + +### 3. 停止录制 + +再次点击录制按钮停止录制: +- 按钮恢复正常颜色 +- 动画停止 +- 录制的音频已保存在浏览器内存中 + +### 4. 下载音频 + +点击**下载按钮**(向下箭头图标),从下拉菜单选择: + +**选项 1:下载原声音频** +- 文件名:`input-audio-YYYYMMDD-HHmmss.webm` +- 内容:麦克风直接采集的原始音频 +- 包含:环境噪声、回声、音量波动等 + +**选项 2:下载净化后音频** +- 文件名:`processed-audio-YYYYMMDD-HHmmss.webm` +- 内容:经过 3A 处理的音频 +- 效果:回声消除、噪声抑制、自动增益控制 + +## 界面说明 + +### 控制按钮布局 + +通话界面底部控制栏(从左到右): + +1. **静音按钮** 🎤 + - 功能:切换麦克风静音 + - 状态:静音时显示红色 + +2. **录制按钮** 📹 + - 功能:开始/停止录制 + - 状态:录制中显示红色脉冲动画 + +3. **挂断按钮** 📞 + - 功能:结束通话 + - 样式:红色大按钮 + +4. **下载按钮** ⬇️ + - 功能:下载录制的音频 + - 状态:未录制时禁用(灰色) + +5. **设置按钮** ⚙️ + - 功能:预留(暂未实现) + - 状态:禁用 + +### 视觉反馈 + +**录制中**: +- 录制按钮背景变红 +- 脉冲动画(淡入淡出效果) +- 清晰提示正在录制状态 + +**录制完成**: +- 下载按钮从禁用变为可用 +- 可以重复录制(会覆盖之前的录音) + +## 技术实现 + +### 架构 + +``` +麦克风输入 + ↓ +Web Audio API + ├─→ 原始音频流 → MediaRecorder → 原声录音 + └─→ 3A 处理 → MediaStreamDestination → MediaRecorder → 净化后录音 +``` + +### 使用的 API + +1. **MediaRecorder API** + - 用途:录制音频流 + - 编码:audio/webm (Opus codec) + - 参数:每 100ms 收集一次数据 + +2. **Web Audio API** + - 用途:创建音频处理图 + - ScriptProcessor:处理音频数据 + - MediaStreamDestination:导出处理后的音频流 + +3. **JavaScript Interop** + - Blazor 与 JavaScript 通信 + - 控制录制状态 + - 触发下载操作 + +### 文件格式 + +- **格式**:WebM +- **编解码器**:Opus +- **采样率**:取决于浏览器和麦克风设置 +- **声道**:单声道(Mono) +- **比特率**:自动调整 + +## 浏览器兼容性 + +### 支持的浏览器 + +| 浏览器 | 版本要求 | MediaRecorder | Web Audio API | +|--------|---------|---------------|---------------| +| Chrome | 49+ | ✅ | ✅ | +| Edge | 79+ | ✅ | ✅ | +| Firefox | 25+ | ✅ | ✅ | +| Safari | 14.1+ | ✅ | ✅ | +| Opera | 36+ | ✅ | ✅ | + +### 已知限制 + +1. **iOS Safari** + - 需要用户手势才能开始录制 + - WebM 支持可能受限(Safari 14.1+) + +2. **移动浏览器** + - 可能需要额外的权限请求 + - 某些设备可能不支持 Opus 编码 + +3. **隐私模式** + - 某些浏览器在隐私模式下可能限制音频录制 + +## 使用场景 + +### 1. 会议记录 +- 录制完整会议内容 +- 对比 3A 处理效果 +- 事后回顾讨论内容 + +### 2. 音频质量测试 +- 测试 3A 算法效果 +- 对比原声和处理后的差异 +- 优化音频处理参数 + +### 3. 培训和演示 +- 录制演示内容 +- 提供清晰的音频材料 +- 展示 3A 处理能力 + +### 4. 故障诊断 +- 记录音频问题 +- 分析噪声来源 +- 测试不同环境下的表现 + +## 性能考虑 + +### 内存使用 + +- 录制时音频数据存储在浏览器内存 +- 长时间录制会占用较多内存 +- 建议录制时长不超过 30 分钟 + +### 文件大小 + +估算公式(Opus @ 48kHz): +- 比特率:约 24-32 kbps(单声道) +- 1 分钟:约 180-240 KB +- 10 分钟:约 1.8-2.4 MB +- 30 分钟:约 5.4-7.2 MB + +### 优化建议 + +1. **定期下载** + - 长时间通话时分段录制 + - 及时下载释放内存 + +2. **监控内存** + - 注意浏览器内存警告 + - 必要时停止录制 + +3. **网络影响** + - 录制在本地进行,不占用网络 + - 下载也是本地操作 + +## 故障排查 + +### 无法开始录制 + +**症状**:点击录制按钮无反应 + +**解决方法**: +1. 检查麦克风权限是否已授予 +2. 确认已成功进入通话 +3. 查看浏览器控制台错误信息 +4. 尝试刷新页面重新进入 + +### 下载按钮禁用 + +**症状**:无法点击下载按钮 + +**原因**:未进行过录制 + +**解决方法**: +1. 先点击录制按钮开始录制 +2. 等待一段时间 +3. 停止录制 +4. 下载按钮会变为可用 + +### 下载的文件无法播放 + +**症状**:下载的 WebM 文件播放失败 + +**解决方法**: +1. 使用支持 WebM 的播放器(VLC、Chrome) +2. 转换为 MP3 格式(使用 FFmpeg): + ```bash + ffmpeg -i input-audio.webm -codec:a libmp3lame output.mp3 + ``` +3. 检查文件大小(如果为 0 说明录制失败) + +### 音频质量问题 + +**症状**:录制的音频质量差 + +**解决方法**: +1. 检查麦克风质量 +2. 确保良好的网络环境 +3. 启用 3A 处理功能 +4. 调整麦克风音量和位置 + +## 隐私和安全 + +### 数据存储 + +- ✅ 所有录制在浏览器本地完成 +- ✅ 音频数据不会上传到服务器 +- ✅ 下载后立即从内存清除 +- ✅ 关闭页面会丢失未下载的录音 + +### 权限要求 + +- 麦克风访问权限 +- 文件下载权限(某些浏览器) + +### 最佳实践 + +1. **告知参与者** + - 在开始录制前通知其他参与者 + - 遵守隐私法规(如 GDPR) + +2. **安全存储** + - 下载后的文件妥善保管 + - 考虑加密敏感内容 + +3. **及时删除** + - 不需要的录音及时删除 + - 避免长期保存敏感对话 + +## 未来改进 + +计划中的功能: + +- [ ] 支持更多音频格式(MP3、AAC) +- [ ] 云端存储集成 +- [ ] 自动分段录制 +- [ ] 录音质量设置 +- [ ] 音频编辑功能 +- [ ] 转录服务集成 + +## 参考资料 + +- [MediaRecorder API](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) +- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) +- [WebM 格式规范](https://www.webmproject.org/) +- [Opus 编解码器](https://opus-codec.org/) diff --git a/docs/DOCKER.md b/docs/DOCKER.md new file mode 100644 index 0000000..cec77c6 --- /dev/null +++ b/docs/DOCKER.md @@ -0,0 +1,227 @@ +# Docker 部署指南 + +本文档介绍如何使用 Docker 部署 Audio3A WebAPI 服务。 + +## 快速开始 + +### 使用预构建镜像 + +从 Docker Hub 拉取并运行: + +```bash +docker pull gmij/audio3a:latest +docker run -d -p 8080:80 --name audio3a-api gmij/audio3a:latest +``` + +访问 `http://localhost:8080/swagger` 查看 API 文档。 + +### 本地构建 + +从源码构建镜像: + +```bash +# 在项目根目录执行 +docker build -t audio3a-api -f samples/Audio3A.WebApi/Dockerfile . + +# 运行容器 +docker run -d -p 8080:80 --name audio3a-api audio3a-api +``` + +## 环境变量配置 + +可以通过环境变量配置服务: + +```bash +docker run -d -p 8080:80 \ + -e ASPNETCORE_ENVIRONMENT=Production \ + -e ASPNETCORE_URLS=http://+:80 \ + --name audio3a-api \ + gmij/audio3a:latest +``` + +### 常用环境变量 + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `ASPNETCORE_ENVIRONMENT` | Production | 运行环境(Development/Production) | +| `ASPNETCORE_URLS` | http://+:80 | 监听地址和端口 | + +## Docker Compose + +使用 Docker Compose 部署完整服务栈: + +```yaml +version: '3.8' + +services: + audio3a-api: + image: gmij/audio3a:latest + ports: + - "8080:80" + environment: + - ASPNETCORE_ENVIRONMENT=Production + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/swagger"] + interval: 30s + timeout: 10s + retries: 3 +``` + +保存为 `docker-compose.yml` 后运行: + +```bash +docker-compose up -d +``` + +## 持久化数据 + +如果需要持久化数据,可以挂载卷: + +```bash +docker run -d -p 8080:80 \ + -v audio3a-data:/app/data \ + --name audio3a-api \ + gmij/audio3a:latest +``` + +## 健康检查 + +检查容器状态: + +```bash +# 查看容器日志 +docker logs audio3a-api + +# 检查容器状态 +docker ps | grep audio3a-api + +# 测试 API 可用性 +curl http://localhost:8080/api/rooms/stats +``` + +## 生产环境部署 + +### 使用 HTTPS + +推荐使用反向代理(如 Nginx 或 Traefik)提供 HTTPS: + +```nginx +server { + listen 443 ssl http2; + server_name api.audio3a.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### 资源限制 + +限制容器资源使用: + +```bash +docker run -d \ + -p 8080:80 \ + --memory="512m" \ + --cpus="1.0" \ + --name audio3a-api \ + gmij/audio3a:latest +``` + +### 自动重启 + +设置容器自动重启策略: + +```bash +docker run -d \ + -p 8080:80 \ + --restart=unless-stopped \ + --name audio3a-api \ + gmij/audio3a:latest +``` + +## 故障排查 + +### 容器无法启动 + +```bash +# 查看详细日志 +docker logs --tail 100 audio3a-api + +# 进入容器检查 +docker exec -it audio3a-api /bin/bash +``` + +### 端口冲突 + +如果 8080 端口被占用,更改映射端口: + +```bash +docker run -d -p 9090:80 --name audio3a-api gmij/audio3a:latest +``` + +### 性能问题 + +监控容器资源使用: + +```bash +docker stats audio3a-api +``` + +## 镜像标签说明 + +| 标签 | 说明 | +|------|------| +| `latest` | main 分支最新版本 | +| `v1.0.0` | 特定版本号 | +| `main-abc1234` | 分支名+提交 SHA | + +## 清理 + +停止并删除容器: + +```bash +docker stop audio3a-api +docker rm audio3a-api +``` + +删除镜像: + +```bash +docker rmi gmij/audio3a:latest +``` + +清理未使用的镜像和卷: + +```bash +docker system prune -a +``` + +## CI/CD 集成 + +GitHub Actions 会自动构建和推送镜像到 Docker Hub。 + +**触发条件**: +- 推送到 main 分支 +- 创建版本标签(如 `v1.0.0`) +- 手动触发 workflow + +**所需配置**: +在 GitHub Repository Settings → Secrets 中添加: +- `DOCKERHUB_USERNAME` - Docker Hub 用户名 +- `DOCKERHUB_TOKEN` - Docker Hub 访问令牌 + +## 参考资料 + +- [Docker 官方文档](https://docs.docker.com/) +- [ASP.NET Core Docker 部署](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/) +- [Docker Compose 文档](https://docs.docker.com/compose/) diff --git a/docs/NEW_FEATURES.md b/docs/NEW_FEATURES.md new file mode 100644 index 0000000..dad8a10 --- /dev/null +++ b/docs/NEW_FEATURES.md @@ -0,0 +1,74 @@ +# 新增功能说明 + +## 概述 + +本次更新实现了两个主要功能: + +1. **服务端房间管理集成** - 将 Web 前端从浏览器内存模拟切换到真实的 WebAPI 后端 +2. **实时波形可视化** - 在通话界面显示输入音频和 3A 处理后的波形图 + +## 1. 服务端房间管理 + +### 功能描述 + +之前的 Web 应用使用 `MockApiService` 在浏览器内存中模拟房间管理功能,现在已经升级支持连接真实的 WebAPI 后端服务。 + +### 配置方式 + +#### appsettings.json + +```json +{ + "ApiBaseUrl": "https://localhost:7063", + "UseMockApi": false +} +``` + +**配置项说明**: +- `ApiBaseUrl`: WebAPI 后端地址 +- `UseMockApi`: + - `false` - 使用真实 API(连接服务端,默认) + - `true` - 使用 Mock API(浏览器内存,用于 GitHub Pages) + +## 2. 实时波形可视化 + +### 功能描述 + +在语音通话界面显示两个实时波形图: +- **输入音频波形**(蓝色)- 显示从麦克风采集的原始音频 +- **3A 处理后波形**(绿色)- 显示经过回声消除、增益控制、噪声抑制后的音频 + +### 使用效果 + +1. **启动通话**:点击"开始通话"按钮 +2. **授权麦克风**:浏览器会请求麦克风权限 +3. **查看波形**: + - 上方显示输入音频的实时波形(蓝色) + - 下方显示 3A 处理后的波形(绿色) +4. **静音功能**:点击静音按钮时,波形停止更新 + +## 部署说明 + +### 开发环境(本地测试) + +1. 修改 `samples/Audio3A.Web/wwwroot/appsettings.json`: + ```json + { + "ApiBaseUrl": "https://localhost:7063", + "UseMockApi": false + } + ``` + +2. 启动服务: + ```bash + # 终端 1 + cd samples/Audio3A.WebApi + dotnet run + + # 终端 2 + cd samples/Audio3A.Web + dotnet run + ``` + +3. 访问 `https://localhost:5001` + diff --git a/samples/Audio3A.Web/Components/WaveformVisualizer.razor b/samples/Audio3A.Web/Components/WaveformVisualizer.razor new file mode 100644 index 0000000..5ed04b8 --- /dev/null +++ b/samples/Audio3A.Web/Components/WaveformVisualizer.razor @@ -0,0 +1,121 @@ +@using Microsoft.JSInterop + +
+ + @if (!string.IsNullOrEmpty(Label)) + { +
@Label
+ } +
+ + + +@code { + [Parameter] + public int Width { get; set; } = 800; + + [Parameter] + public int Height { get; set; } = 120; + + [Parameter] + public string? Label { get; set; } + + [Parameter] + public string Color { get; set; } = "#52c41a"; + + [Parameter] + public string BackgroundColor { get; set; } = "rgba(0, 0, 0, 0.5)"; + + [Inject] + private IJSRuntime JSRuntime { get; set; } = null!; + + private ElementReference _canvasRef; + private IJSObjectReference? _module; + private DotNetObjectReference? _objRef; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + _objRef = DotNetObjectReference.Create(this); + _module = await JSRuntime.InvokeAsync("import", "./js/waveform.js"); + await _module.InvokeVoidAsync("initWaveform", _canvasRef, Width, Height, Color, BackgroundColor); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to initialize waveform: {ex.Message}"); + } + } + } + + /// + /// 更新波形数据 + /// + public async Task UpdateWaveformAsync(float[] data) + { + if (_module != null) + { + try + { + await _module.InvokeVoidAsync("updateWaveform", _canvasRef, data); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to update waveform: {ex.Message}"); + } + } + } + + /// + /// 更新波形数据(byte array) + /// + public async Task UpdateWaveformAsync(byte[] data) + { + if (_module != null) + { + try + { + await _module.InvokeVoidAsync("updateWaveformFromBytes", _canvasRef, data); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to update waveform from bytes: {ex.Message}"); + } + } + } + + public async ValueTask DisposeAsync() + { + if (_module != null) + { + await _module.DisposeAsync(); + } + _objRef?.Dispose(); + } +} diff --git a/samples/Audio3A.Web/Pages/Call.razor b/samples/Audio3A.Web/Pages/Call.razor index 60283c5..ba3e8e5 100644 --- a/samples/Audio3A.Web/Pages/Call.razor +++ b/samples/Audio3A.Web/Pages/Call.razor @@ -1,5 +1,6 @@ @page "/call/{RoomId}/{ParticipantId}" -@inject MockApiService MockApi +@using Audio3A.Web.Components +@inject IApiService ApiService @inject AudioCallService AudioService @inject NavigationManager Navigation @inject IMessageService Message @@ -25,6 +26,26 @@ + +
+
+ +
+
+ +
+
+
@@ -61,6 +82,14 @@ + + + + + + + + 下载原声音频 + + + + 下载净化后音频 + + + + + + + +