ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

同样是 440 Hz,为什么听起来不一样?我用华为云码道做了一个声音实验室

同样是 440 Hz,为什么听起来不一样?我用华为云码道做了一个声音实验室 同样是 440 Hz为什么听起来不一样我用华为云码道做了一个声音实验室华为云码道 CodeArts 体验入口https://developer.huaweicloud.com/codeartsco.html?sourcedmzntgwatomgit1sourceaddmzntgwatomgithdAtomGit 项目仓库https://atomgit.com/qq_40202349/sound-lab把频率固定在 440 Hz先播放正弦波再切到方波。数字没变声音却有了明显区别。频谱里基频还在原来的位置右边多出了几座峰。这次我用华为云码道做了一个“声音实验室”把这几种变化放到同一个网页里比较。固定频率换波形或者保持波形不变调音量听到的变化都能在时域波形和频谱中对照。下面是修复后的运行效果图1440 Hz 方波的时域形状以及基频之外的谐波。项目用了 Vue 3、TypeScript、Vite以及浏览器自带的 Web Audio API 和 Canvas。声音在浏览器里合成不需要上传音频也不需要申请麦克风权限。给码道的需求三个声音实验先在 AtomGit 网页端通过码道创建项目仓库再转到本地码道开发。图2向码道提交建仓需求。给码道的开发需求分成三个实验改频率听音高变化看同一段时间里波形变密还是变疏。改音量看波形振幅变化纵轴固定不能自动把每条波形拉到一样高。固定频率切换正弦波、三角波和方波比较音色与谐波。频率范围设为 110–880 Hz。页面有播放、停止和重置按钮第一次发声必须由用户点击。三个实验入口负责加载参数和操作指引让使用者知道这一次应该改哪个值、观察哪张图。需求里同时约定了图形和播放行为波形和频谱必须来自实际音频数据不能用随机动画或预画图形冒充。时域图使用固定时间范围与幅度刻度让频率和音量的变化能直观看出来。停止状态要明确修改参数和切换实验不应意外开始播放。启停、频率和增益变化做平滑处理减少突变造成的爆音。界面则按小型实验仪器来做深色背景、清晰网格、青绿色波形和少量暖色数值控制区与两张图放在一起。图形旁边保留必要的单位和说明读者调整参数时能就近找到变化。交付要求也一并写进需求创建可运行的项目文件完成测试和构建再启动本地预览。首版交付时声音合成、两张图、三个实验入口和运行说明都已经有了。初版 75 项测试通过换个顺序却出了问题码道的首版报告列出了 7 个测试文件、75 项通过以及生产构建成功。报告还把自动化测试、浏览器画面和实际听感分开记录明确写了听感仍需人工确认。图3初版 75 项测试通过报告单独注明听感需要人工确认。我在本机按三个实验的指引试听频率升高时音调变高音量调低时声音变轻切换波形也确实能听出区别。随后借助 Codex 做独立复核重新运行现有测试仍是 75 项通过类型检查与构建也通过。浏览器检查除了操作控件还读取了真实音频节点核对界面数值和实际参数是否相同。按照页面提示先播放、再调参数三个实验都能工作。把顺序换成“刷新页面先选择比较音高再第一次播放”界面显示 220 Hz读取实际振荡器参数却还是 440 Hz。继续沿着这个顺序检查先设置 660 Hz、10% 音量和方波再播放实际仍是默认的 440 Hz、60% 音量和正弦波。甚至先把音量调到 0首次播放也仍然产生了非零音频信号。再做一个对照先播放等音频引擎建立以后再设置同样的 660 Hz、10% 音量和方波三个参数就都能正确生效。相同设置在播放后能用排查范围就缩到了首次初始化。初版在点击播放时才创建引擎却始终传入DEFAULT_PARAMS。播放前调参数时引擎还不存在负责同步的watch没有对象可更新等引擎建立起来此前的参数变化也不会自动重放一遍。频率数字框也有问题。全选原来的数值逐字输入660第一个6被立即收窄为下限110后续输入又继续触发边界处理最后数字框停在了8800滑杆实际是880。这个问题用一次性填入660的脚本不容易发现按键一位一位输入才看得出来。图4同样输入 660初版会改写输入修复后在失焦时正确提交。桌面图表还有一处叠字频谱末尾的5000和“频率 / Hz”画在同一条基线上时域图也有类似问题。这些操作步骤和截图一起交给码道作为下一轮修改的依据。让码道沿着复现步骤修复这轮只改参数同步、数字输入和桌面坐标文字。反馈里写明了怎么复现以及希望改成什么样刷新后先选“比较音高”首次播放应采用界面上的 220 Hz先把音量设为 0首次播放也要保持静音。数字框允许暂时清空输入中的6、66先保留按 Enter 或失焦后再校验滑杆继续实时生效。为桌面图表的单位留出独立位置避免与末尾刻度叠在一起。同时要求补对应的回归验证尤其是首次播放要核对真实音频参数。只截一张显示 220 Hz 的页面无法证明实际振荡器也使用了 220 Hz。useAudioLab.ts用params保存界面当前选择用engine保存音频引擎。修复需要接上两段逻辑首次创建时读取当前参数创建之后继续监听变化。下面摘出初始化和频率监听省略音量、波形的同类监听及错误处理functionensureEngine():AudioEngine{if(!engine.value){engine.valuenewAudioEngine({...params},fftSize.value)}returnengine.value}watch(()params.frequency,(value)engine.value?.setFrequency(value),)watch负责后续同步频率一变就把新值传给已经存在的引擎。?.让引擎尚未创建时跳过调用所以播放前调参数不会报错也不会触发声音这时变化只保存在params里。首次创建引擎时{ ...params }复制当前的整组参数替换原先传入的DEFAULT_PARAMS。假如用户先选了 220 Hz这份快照就是 220 Hz如果又把音量调成 0引擎拿到的音量也会是 0。ensureEngine()本身只创建 JavaScript 对象真正建立或恢复AudioContext仍由播放事件中的start()完成。修改参数不会提前初始化音频。ControlPanel.vue则把“正在编辑的文本”和“已经提交的频率”分开保存。下面节选草稿更新与提交逻辑省略事件绑定、焦点处理和外部参数同步constfrequencyDraftref(String(props.frequency))leteditingFrequencyfalsefunctiononFrequencyDraftInput(event:Event):void{editingFrequencytruefrequencyDraft.value(event.targetasHTMLInputElement).value}functioncommitFrequency():void{constrawfrequencyDraft.value.trim()constparsedNumber(raw)constnextraw!Number.isFinite(parsed)?clampFrequency(parsed):props.frequency frequencyDraft.valueString(next)if(next!props.frequency)emit(update:frequency,next)}输入框的:value绑定frequencyDraft每次input只改草稿。于是输入660时6、66可以暂时留在框里滑杆和实际声音仍保持上一次提交的频率。按 Enter 或失焦后再调用commitFrequency()通过update:frequency把结果传回上层。提交时要先判断文本是否为空Number()会得到0如果直接交给范围校验清空输入框就会被误当成要设置最低频率。当前逻辑让空值回退到原频率合法数字再由clampFrequency收窄到 110–880 Hz。滑杆每次给出的值已经在范围内仍然直接更新无需等待提交。最后处理图表叠字。以频谱为例刻度文字放在y height 12横轴单位放在y height 26两者错开 14 个像素底部预留 34 个像素容纳这两行。纵轴名称挪到绘图区左上角避开外侧的顶部数字。码道的修复记录如下图5码道对话中的修复记录局部包含改动和对应验证。码道补了 7 项回归测试3 项检查首次播放前的参数4 项检查数字框逐字输入、清空、越界和回车提交。随后独立复核重新走原来的操作路径结果如下复核操作修复后的实际结果先选“比较音高”再首次播放界面与振荡器均为 220 Hz先设 660 Hz、10% 音量、方波再首次播放振荡器为 660 Hz、方波音量增益为 0.05先把音量设为 0再首次播放音量增益、采样峰值和 RMS 均为 0数字框逐字输入660依次保留6、66、660失焦后滑杆同步到 660查看桌面图表单位与末尾刻度不再重叠其中 10% 对应增益 0.05是因为界面音量还要乘以项目设置的最大增益 0.5。RMS 是均方根值用来描述这段采样的有效幅度音量为 0 时它和采样峰值都应为 0。这些浏览器检查读取了真实音频节点运行时关闭了设备出声验证的是信号数据听感依据是前面的本机试听。独立重跑测试得到 7 个文件、82 项通过类型检查与生产构建通过。应用代码和修复由本地码道完成需求与试听由我负责独立复核和素材整理借助 Codex。修复后用三个实验看声音接下来三段 GIF 都录自修复后的本地页面。GIF 没有声音可以先看图形变化再在自己的电脑上听。先比较音高。选正弦波把频率从 220 Hz 调到 880 Hz再调回来。时域图的时间窗口不变频率升高后里面容纳的周期更多曲线就变密了频谱的主峰也向右移动。图6220–880 Hz 的频率变化音量与波形类型保持不变。留意横轴它没有跟着频率自动缩放本次环境采样率为 48000 Hz一帧读取 2048 个采样显示的时间窗口约为 42.7 ms。220 Hz 升到 440 Hz周期减半同一段时间里就会出现约两倍的波形周期。再比较音量。频率固定为 440 Hz只把音量从 10% 调到 80%。波形的周期基本不变上下振幅明显变大。图7音量从 10% 调到 80%波形纵轴始终使用相同刻度。这里不能让纵轴自动缩放。如果每一帧都把当前最高点放大到顶格10% 和 80% 的波形会看起来差不多高音量调整带来的幅度差异就被抹掉了。在当前增益设置下正弦波的 10% 音量对应约 0.05 的峰值幅度80% 对应约 0.4。保持频率和波形不变就能把变化归到输出幅度上。不过波形幅度增大几倍并不等于人耳感觉也响了同样的倍数。最后比较音色。频率保持 440 Hz、音量保持 60%依次切换正弦波、三角波和方波。正弦波主要集中在基频附近换成三角波、方波以后基频之外出现了更明显的奇次谐波方波的高次谐波尤其容易看到。图8频率和音量设置相同切换波形观察谐波分布。440 Hz 表示基频声音里还可能有其他频率成分。它们的强弱不同听起来就会有不同的音色。另外同一个音量设置下不同波形的主观响度也可能不同这次只固定了滑杆数值并没有做严格的等响度比较。观察时可以先找 440 Hz 附近的主峰再看 1320 Hz、2200 Hz 附近也就是基频的 3 倍和 5 倍位置。三角波与方波都有奇次谐波但这些峰的强弱不同。这样再回头看时域图尖角、平顶和光滑曲线就能与频谱里的差别联系起来。声音、波形和频谱怎样联动拖动滑杆时控件先更新useAudioLab.ts中的参数监听把变化交给AudioEngine.ts音频节点产生新信号。两张 Canvas 读取分析器的数据再换算成坐标。连接音频节点音频节点在AudioEngine.ts的buildGraph()中创建、设置并连接。以下是代码节选ctx是已经创建或恢复的音频上下文省略重复创建保护、分析器显示配置和节点引用保存constoscillatorctx.createOscillator()constvolumeGainctx.createGain()constmasterGainctx.createGain()constanalyserctx.createAnalyser()analyser.fftSizethis.fftSizeValue oscillator.typethis.params.waveform oscillator.frequency.valuethis.params.frequency volumeGain.gain.valuethis.params.volume*MASTER_MAX_GAINmasterGain.gain.value0oscillator.connect(volumeGain)volumeGain.connect(masterGain)masterGain.connect(analyser)analyser.connect(ctx.destination)oscillator.start()oscillator负责连续生成波形type决定正弦波、三角波还是方波frequency决定每秒重复多少次。它后面的volumeGain把采样乘以音量系数例如 10% 音量对应0.1 × 0.5 0.05。信号继续经过masterGain再交给分析器和音频输出。分析器接在两个增益节点后面才能读到音量调整和停止后的信号。如果接在音量节点之前听到的声音变小了图形却仍可能读到未经衰减的信号。上面已经启动振荡器但masterGain初始为 0因此这时输出仍然静音。播放流程再用linearRampToValueAtTime(1, now 0.02)让总增益在 20 ms 内升到 1停止时平滑降回 0用户选择的音量保存在另一个增益节点中。播放后的频率、音量调整使用setTargetAtTime以 0.012 秒的时间常数逐渐靠近目标。这个参数控制变化快慢并不表示 12 ms 后精确到达终值。切换波形时还会短暂降低总增益再恢复以减轻突变。停止时保留节点方便重播组件销毁时再断开连接并关闭音频上下文。时域图把采样值换成坐标WaveformCanvas.vue用一个长度为fftSize的Float32Array接收采样每次绘制都复用这个缓冲区。下面把读取和绘制代码放在一起省略网格、配色与空节点检查x、y、width、height是绘图区的位置和尺寸分析器与缓冲区已经就绪constAMPLITUDE_RANGE1constcenterYyheight/2consthalfHeightheight/2analyser.getFloatTimeDomainData(buffer)constnbuffer.length ctx.beginPath()for(leti0;in;i1){letvaluebuffer[i]if(!Number.isFinite(value))value0if(valueAMPLITUDE_RANGE)valueAMPLITUDE_RANGEelseif(value-AMPLITUDE_RANGE)value-AMPLITUDE_RANGEconstpxx(i/(n-1))*widthconstpycenterY-(value/AMPLITUDE_RANGE)*halfHeightif(i0)ctx.moveTo(px,py)elsectx.lineTo(px,py)}ctx.stroke()横坐标按采样顺序排列第一个点在左边最后一个点在右边。相邻采样的时间间隔由实际采样率决定横轴标签也按fftSize / sampleRate换算成毫秒所以改频率时不必改变绘图区宽度。纵坐标则以中线代表 0正值向上、负值向下。Canvas 的纵坐标向下增大因此公式里用减号。假设绘图区高 200 像素半高就是 100采样幅度为 0.05 时偏离中线约 5 像素为 0.4 时偏离约 40 像素正好对应前面音量实验中的差别。公式的分母固定为AMPLITUDE_RANGE 1不会随当前一帧的最大值变化小幅度信号也就不会被放大到顶格。边界处理只把异常或超出刻度的值收回可绘范围曲线仍来自实际采样。频谱把频点换成 Hz 和像素频谱使用同一个分析器的getFloatFrequencyData取得每个频点的电平。在当前fftSize 2048下频域缓冲区有 1024 个值数组下标需要按下标 × 采样率 / FFT 长度换算成 Hz不能直接当作频率。SpectrumCanvas.vue先读取频谱再生成绘图点。下面节选这两部分省略缓冲区准备、网格和描边rate是实际采样率maxHz取 5000 Hz 与采样率一半中的较小值几个换算函数位于core/analysis.tsanalyser.getFloatFrequencyData(buffer)constdbToY(db:number):numberydbToRatio(db,MIN_DB,MAX_DB)*heightconstnbuffer.lengthconstpoints:Array{px:number;py:number}[]for(leti0;in;i1){constfreqbinToFrequency(i,rate,props.fftSize||n*2)if(freqmaxHz)breakconstvaluebuffer[i]constdbNumber.isFinite(value)?value:MIN_DBpoints.push({px:xfrequencyToRatio(freq,0,maxHz)*width,py:dbToY(db),})}横坐标先由binToFrequency算出这个点代表多少 Hz再由frequencyToRatio把它放到 0–5000 Hz 的显示范围中。例如 440 Hz 约在横轴宽度的 8.8% 处1320 Hz 约在 26.4% 处。遍历到显示范围之外就结束。纵轴刻度下限MIN_DB为 -100上限MAX_DB为 -10。dbToRatio用(MAX_DB - db) / (MAX_DB - MIN_DB)求出从顶部向下的比例再限制到 0–1信号越强数值越接近上限点就越靠上。静音时可能读到-Infinity这里把非有限值放到下限避免无效坐标进入 Canvas。频谱峰值还受 FFT 分辨率限制。本次采样率为 48000 Hz相邻频点间隔为48000 / 2048 23.4375 Hz。初版检查中振荡器设为 440 Hz最高频谱采样点却在 445.3125 Hz正好是下标为 19 的频点。这是分析分辨率造成的差异。判断首次播放是否用了错误参数时要看实际振荡器频率不能要求频谱峰值数字始终等于输入值。图里的橙色虚线另有来源harmonicFrequencies按当前基频的整数倍生成参考位置例如 440、880、1320 Hz再用同样的横坐标换算画虚线。它们帮助对照谐波位置蓝色曲线才来自分析数据正弦波图里出现这些虚线并不表示每个位置都有同样强的成分。两张图各自通过useRafLoop在requestAnimationFrame回调里读取数据、重画。Web Audio 持续生成声音画图循环读取当时的分析结果组件卸载时取消动画回调并释放资源。在自己的电脑上试一遍本文对应本地验证通过的修复版。完整代码同步到文首 AtomGit 仓库后可以用下面的命令启动本次验证环境是 Node.js v22.21.0gitclone https://atomgit.com/qq_40202349/sound-lab.gitcdsound-labnpmcinpmrun dev打开终端给出的本地地址。仓库根目录就是应用目录不需要再进入app。这里提供的是源码与本地运行方式仓库页面本身不是在线演示站点。第一次试可以按下面这个顺序把三个实验和这次修复一起走一遍刷新页面先选“比较音高”把音量调低再点击播放。此时使用的是 220 Hz把它改到 440 Hz听音高变化看波形变密。保持 440 Hz先播放正弦波再切换三角波和方波。观察主峰右侧增加了哪些峰听同一个基频下的区别。切回正弦波把音量从低到高缓慢调整。波形应主要改变高度周期基本不变最后调到 0声音消失时域曲线回到零附近。点停止选中频率数字框里的全部数字逐字输入660按 Enter。数字框和滑杆都应显示 660页面保持停止直到再次点击播放。需要检查或构建时运行npmtestnpmrun typechecknpmrun build这个版本面向桌面浏览器使用单一振荡器频谱只展示 0–5000 Hz。它适合对比声音的基本变化不是环境噪声测量工具页面的 dBFS 表示数字信号电平不能据此判断耳机或外放的实际声压。代码采用 MIT 协议。这次用码道的体会声音实验室的应用代码和后续修复都是由码道完成的。首版里已经有 Vue 工程、Web Audio 音频引擎、两张 Canvas 图和三个实验入口控件也接上了声音与图形更新。这替我省去了搭建工程、编写音频与绘图模块、再把它们连起来的工作。拿到首版后就能直接调参数、听声音检查实验有没有达到预期。码道还把实验要求落实成了具体的技术处理。比如比较音量时要能看出波形高低变化需求里因此要求固定纵轴。码道在绘图代码中把幅度范围固定为 ±1又把分析器接在增益节点后读取经过音量调整的真实采样声音和波形高度才能一起变化。启停时的平滑过渡也由它写进了音频引擎。需求描述的是希望听到、看到的现象支撑这些现象的节点连接、数据读取和坐标计算则由码道完成。首版的问题通过独立复核发现后复现步骤和原因定位一起交给码道。它继续在原工程中修改首次播放改为读取当前参数数字输入拆成草稿和提交两个阶段图表单位另起一行并补上相应的回归测试。这一轮码道承担了按反馈修改代码、补写测试的工作。修复报告还列出了改动文件和验证结果拿到交付后可以沿着原来的操作再次检查确认反馈是否落实。对这次项目而言码道承担了工程实现和反馈后的修补人工精力可以更多放在实验设计与验收上。首版漏掉的播放顺序和数字输入问题说明上手检查仍然必要。能把明确的实验需求做成可运行的页面又能在同一个工程里接着改是这次实际用下来最有帮助的地方。
返回列表