MIC CHECK · LOCAL ONLY

听见了吗?

录一小段声音,再亲耳听回来。
一次完成授权、采集与回放检查。

RECORDER

麦克风自检

正在检查环境
实时电平等待声音 — dBFS

dBFS 是数字相对电平;没有设备校准,不能当作环境声压 dB。

00:00.0 最长 60 秒

正在确认 Toy SDK 与浏览器录音能力。

SIGNAL PATH

三步都亮,录音就通了

  1. 01 Toy SDK等待能力检测 等待
  2. 02 浏览器录音等待格式检测 等待
  3. 03 麦克风信号点击开始后确认 等待

CORE SOURCE

麦克风录音的核心链路

不再读取隐藏页面或外部仓库。下面直接内嵌真正决定录音能否工作的代码,打开页面就能看到。

  1. 01用户点击保留授权手势
  2. 02Toy SDK取得媒体流
  3. 03Web Audio读取 PCM
  4. 04dBFS计算实时电平
  5. 05MediaRecorder收集音频片段
  6. 06Blob URL页面内回放
01

向 Toy 申请麦克风

必须在按钮点击的同步调用链里立即申请。SDK 返回 WebRTC 音轨后,用静音 audio 节点启动 Chromium 解码;它不会把麦克风声音外放。

button.addEventListener("click", startRecording);

async function startRecording() {
  // 保留用户点击产生的 user activation。
  const streamPromise = window.toy.requestMicrophone();
  const stream = await streamPromise;

  // Toy SDK 返回 WebRTC 远端音轨;静音播放只负责启动解码。
  const sink = document.createElement("audio");
  sink.autoplay = true;
  sink.muted = true;
  sink.playsInline = true;
  sink.srcObject = stream;
  document.body.appendChild(sink);
  await sink.play();

  return { stream, sink };
}
02

把 PCM 换算成实时 dBFS

AnalyserNode 读取时域采样,先算 RMS,再用 20 × log₁₀(RMS) 得到数字满刻度相对电平。它不是需要设备校准的环境声压 dB。

const context = new AudioContext();
await context.resume();

const source = context.createMediaStreamSource(stream);
const analyser = context.createAnalyser();
analyser.fftSize = 512;
source.connect(analyser);

const samples = new Float32Array(analyser.fftSize);

function readDbfs() {
  analyser.getFloatTimeDomainData(samples);
  let sumSquares = 0;
  for (const sample of samples) sumSquares += sample * sample;

  const rms = Math.sqrt(sumSquares / samples.length);
  return Math.max(-60, 20 * Math.log10(Math.max(rms, 0.001)));
}
03

录成 Blob,并在页面播放

MediaRecorder 每 250 ms 产出一段数据;停止后合成 Blob,再生成当前页面可用的临时 URL。播放按钮直接调用 audio.play()。

const chunks = [];
const destination = context.createMediaStreamDestination();
source.connect(destination);

const recordingStream = destination.stream;
const recorder = new MediaRecorder(recordingStream);

recorder.ondataavailable = (event) => {
  if (event.data.size > 0) chunks.push(event.data);
};

recorder.onstop = () => {
  const blob = new Blob(chunks, { type: recorder.mimeType });
  const recordingUrl = URL.createObjectURL(blob);
  audioPlayer.src = recordingUrl;
  playbackButton.onclick = () => audioPlayer.play();
};

recorder.start(250);
04

停止设备并清掉临时录音

录音完成就释放麦克风;重新录制或离开页面时撤销 Blob URL。设备资源和页面内存必须分别清理。

async function releaseMedia(stream, context, sink) {
  sink.pause();
  sink.srcObject = null;
  sink.remove();
  await context.close();

  try {
    await window.toy.stopMedia(stream);
  } catch {
    stream.getTracks().forEach((track) => track.stop());
  }
}

function clearRecording(recordingUrl) {
  audioPlayer.pause();
  audioPlayer.removeAttribute("src");
  URL.revokeObjectURL(recordingUrl);
}