如何使音频电平指示器易于访问?

How to make an audio level indicator accessible?

我正在尝试为 WebRTC 视频聊天构建一个易于访问的音频指示器。它应该基本上显示你说话的声音,当你说话的时候。这是隔离的代码(对于Codesandbox,您需要安装styled-components)。

import React, { useEffect, useRef, useState } from "react";
import styled, { css } from "styled-components";
import "./styles.css";

function useInterval(callback, delay) {
  const savedCallback = useRef();


  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);


  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

const VolumeMeterWrapper = styled.div`
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  height: 32px;
  width: 32px;
`;

const VolumeBar = styled.div`
  background: #25a968;
  border-radius: 1px;
  height: 4px;
  margin: 0 2px;
  width: 8px;
  transition: all 0.2s linear;
  transform: ${({ level }) => css`scaleY(${level + 1})`};
`;

function VolumeMeter({ level }) {
  return (
    <VolumeMeterWrapper>
      <VolumeBar level={level > 3 ? ((level - 2) * 5) / 4 : 0} />
      <VolumeBar level={level} />
      <VolumeBar level={level > 3 ? ((level - 2) * 5) / 4 : 0} />
    </VolumeMeterWrapper>
  );
}

export default function App() {
  const [level, setLevel] = useState(0);
  const [count, setCount] = useState(0);

  useInterval(() => {
    setCount((c) => c + 1);

    if (count % 10 > 4) {
      setLevel((l) => l - 1);
    } else {
      setLevel((l) => l + 1);
    }
  }, 200);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      Level: {level}
      <VolumeMeter level={level} />
    </div>
  );
}

如您所见,在此示例中没有真正的音频输入。 “音量”模拟了这个人说话时的音量。

您如何使此类内容易于访问?您甚至需要这样做吗(因为检查各种提供商的 UI 没有显示任何特殊标签或 aria-labels)?

前言

这很有趣!

首先向大家道歉,这个例子有点乱,因为我试图在我的过程中留下一些部分来给你选择,如果有任何不明白的地方,请告诉我!

它应该很容易整理并变成可重用的组件。

回答

公告的实际部分很简单,您只需要在上面添加一个 paragraph on the page with aria-live并更新其中的文字即可。

<p class="visually-hidden" aria-live="assertive">
   //update the text in here and it will be announced. 
</p>

更难的是做一个好看的画面reader界面和播音员体验

我是如何处理公告的

我最后做的是获取一段时间内的平均音量和峰值音量,并根据这两个参数发布消息。

如果音量超过 95(假设音量达到 100 的任意数字)或始终高于 80,那么我宣布麦克风声音太大。

如果音量平均低于 40,那么我宣布麦克风太安静。

如果平均音量低于 10,那么我认为麦克风不工作或者他们没有说话。

否则我宣布音量没问题

这些数字需要进行一些调整,因为我显然模拟了波动的音量级别,而现实世界的数字可能有所不同。

我做的另一件事是确保 aria-live 区域每 2 秒更新一次。但是,我的屏幕上的播音员速度很慢 reader,因此您可能需要大约 1200 毫秒。

这是为了避免播音员队列泛滥。

或者,您可以将其设置为更大的值(比如 10 秒),因为这对于在整个通话过程中进行持续监控非常有用。如果您决定这样做,请将播音员设置为 aria-live="polite",这样它就不会打断其他屏幕 reader 通知。

我想到的进一步改进

我没有实现这个,但是如果你想使用它,我可以想到两件事来使它更准确(并且不那么烦人,目前如果在整个通话中都无法使用)作为整个通话过程中的持续监控工具。

首先,我会丢弃任何小于 10 的音量值,只取大于 10 的音量的平均值。

当用户主动说话时,这将更能说明实际音量水平。

其次,如果其他人正在讲话,我会放弃所有音量级别的通知。您会希望用户此时保持安静,所以没有必要告诉他们他们的麦克风很安静!另外,您真的不希望在其他人讲话时收到公告。

我试过的另一种方法

我确实尝试过每 500 毫秒将音量宣布为一个整数值,但由于这是一个快照,我觉得它对发生的事情不是很准确。这就是我选择平均音量的原因。

然后我意识到您可以获得 50 的平均值,但峰值为 100(削波),因此也将峰值音量添加到支票中。

其他

1。重点宣布

我做到了,当你关注 EQ 图形级别的东西(不知道怎么称呼它!呵呵)时它会在一开始有焦点时宣布。

我意识到这是一个更好的切换按钮,这样用户就可以随意打开和关闭它。

最好有一个开关,因为它允许您在收听通知的同时 fiddle 调整音量,它还允许其他人根据自己的喜好打开和关闭它

2。通过平均和使用短语宣布

取平均值然后宣布一个词组而不是一个数字的另一个好处是如果指示器保持亮着则用户体验。

它只在 aria-live 区域发生变化时才发出通知,因此只要音量保持在可接受的水平,它就不会发出任何通知。显然,如果他们停止说话,它会宣布“麦克风太安静”,但它只会再次宣布一次。

3。更喜欢减少运动

患有前庭障碍(对运动障碍敏感)的用户可能会被酒吧分散注意力。

因此我为这些用户完全隐藏了 EQ。

现在因为我默认关闭了 EQ,所以没有必要这样做。但是,如果您决定默认打开 EQ,则可以使用 prefers-reduced-motion 媒体查询来设置更慢的动画速度等。这就是我将其保留的原因,作为如何使用它的示例(当我让 EQ 工作在焦点而不是切换时,这是一个悬垂,所以不再需要了)。

我认为最容易实现的方法是默认关闭 EQ。这也有助于有注意力障碍的人,他们可能很容易被栏分散注意力,并使页面对患有癫痫/癫痫症的人安全,因为栏可能在一秒钟内“闪烁”超过 3 次。

4。更改按钮文本。

我利用了我们visually-hiddenclass开EQ的时候

我更改了 EQ 中的文本,然后在视觉上将其隐藏,这样屏幕 reader 用户在关注 EQ 时仍然可以获得有用的按钮文本,但其他人只能看到 EQ 条。

例子

忽略JS的第一部分,这只是我制作假EQ的粗制滥造的方法。

我添加了一个非常大的注释块来标记您需要的实际部分的开始。它从 JS 的第 65 行开始。

还有一些部分供您参考,以供您参考其他做事方式(在CSS中宣布焦点和更喜欢减少运动)所以有点乱。

有问题尽管问。

//just making the random bars work, ignore this except for globalVars.currentVolume which is defined in the next section as that is what we use to announce the volume.
function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

function removeClass(selector, className){
    var el = document.querySelectorAll(selector);
    for (var i = 0; i < el.length; i++) {
        el[i].classList.remove(className);
    }
}

function addClass(selector, className){
    var el = document.querySelectorAll(selector);
    for (var i = 0; i < el.length; i++) {
        el[i].classList.add(className);
    }
}

var generateRandomVolume = function(){
    var stepHeight = 0.8;
    
    globalVars.currentVolume = randomInt(0,100);
    setBars(globalVars.currentVolume * stepHeight);
    
    setTimeout(generateRandomVolume, randomInt(102,250));
    
    return;
}

function setBars(volume){
    
    var bar1 = document.querySelector('.bar1');
    var bar2 = document.querySelector('.bar2');
    var bar3 = document.querySelector('.bar3');
    var smallHeightProportion = 0.75;
    var smallHeight = volume * smallHeightProportion;
    
    bar1.style.height = smallHeight + "px";
    bar2.style.height = volume + "px";
    bar3.style.height = smallHeight + "px";
    //console.log(globalVars.currentVolume);
    
    if(globalVars.currentVolume < 80){
       addClass('.bar', 'green');
       removeClass('.bar', 'orange');
       removeClass('.bar', 'red');
    }else if(globalVars.currentVolume >= 90){
       addClass('.bar', 'red');
       removeClass('.bar', 'orange');
       removeClass('.bar', 'green');
    }else{
       addClass('.bar', 'orange');
       removeClass('.bar', 'red');
       removeClass('.bar', 'green');
    }
}

window.addEventListener('load', function() {
    setTimeout(generateRandomVolume, 250);
});

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////// ACTUAL ANSWER ///////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////




//actual code you need, sorry it is a bit messy but you should be able to clean it up (and make it "Reactified")
// global variables, obviously only the currentVolume needs to be global so it can be used by both the bars and the announcer
var globalVars = {};
globalVars.currentVolume = 0;
globalVars.volumes = [];
globalVars.averageVolume = 0;
globalVars.announcementsOn = false;
//globalVars.indicatorFocused = false;


var audioIndicator = document.getElementById('audio-indicator');
var liveRegion = document.querySelector('.audio-indicator-announce');
var buttonText = document.querySelector('.button-text');
var announceTimeout;


//adjust the speech interval, 2 seconds felt right for me but I have a slow announcer speed, I would imagine real screen reader users could handle about 1200ms update times easily.
var config = {};
config.speakInterval = 2000;


//push volume level every 100ms for use in getting averages
window.addEventListener('load', function() {
    setInterval(sampleVolume, 100);
});

var sampleVolume = function(){
    globalVars.volumes.push(globalVars.currentVolume);
}

audioIndicator.addEventListener('click', function(e) {
    toggleActive();
});

audioIndicator.addEventListener('keyup',function(e){
    if (e.keyCode === 13) {
        toggleActive();
    }  
});

function toggleActive(){
    if(!audioIndicator.classList.contains('on')) {
      audioIndicator.classList.add('on');
       announceTimeout = setTimeout(announceVolumeInfo, config.speakInterval);
       liveRegion.innerHTML = "announcing your microphone volume is now on";
       
       buttonText.classList.add('visually-hidden');
       buttonText.innerHTML = 'Mic<span class="visually-hidden">rophone</span> Level indicator on (click to turn off)';
       
       console.log("SPEAK:","announcing your microphone volume is on");
    }else{
      audioIndicator.classList.remove('on');
      clearTimeout(announceTimeout);
      liveRegion.innerHTML = "announcing your microphone volume is now off";
      
      buttonText.classList.remove('visually-hidden');
       buttonText.innerHTML = 'Mic<span class="visually-hidden">rophone</span> Level indicator off (click to turn on)';
      
      console.log("SPEAK:","announcing your microphone volume is off");
    }
}




//switch on the announcer - deprecated idea, instead used toggle switch
//audioIndicator.addEventListener('focus', (e) => {
//    setTimeout(announceVolumeInfo, config.speakInterval);
//});

//we take the average over the speakInterval. We also take the peak so we can see if the users microphone is clipping.
function getVolumeInfo(){
    var samples = globalVars.volumes.length;
    var totalVol = 0;
    var avgVol, peakVol = 0;
    var sample;
    for(var x = 0; x < samples; x++){
        sample = globalVars.volumes[x]
        totalVol += sample;
        if(sample > peakVol){
          peakVol = sample;
        }
        
    }
     globalVars.volumes = [];
    
    
    var volumes = {};
    volumes.average = totalVol / samples;
    volumes.peak = peakVol;
    return volumes;
}

var announceVolumeInfo = function(){
    
    var volumeInfo = getVolumeInfo();
    updateLiveRegion (volumeInfo.average, volumeInfo.peak);
    
    //part of deprecated idea of announcing only on focus
    //if(document.activeElement == document.getElementById('audio-indicator')){
    //    setTimeout(announceVolumeInfo, config.speakInterval);
    //}
    
    if(audioIndicator.classList.contains('on')) {
      announceTimeout = setTimeout(announceVolumeInfo, config.speakInterval);
    }
}

//we announce using this function, if you just want to read the current volume this can be as simple as "liveRegion.innerHTML = globalVars.currentVolume"
var updateLiveRegion = function(avgVolume, peak){
    var speak = "Your microphone is ";
    
    //doing it this way has a few advantages detailed in the post.
    if(peak > 95 || avgVolume > 80){
        speak = speak + "too loud";
    }else if(avgVolume < 40){
        speak = speak + "too quiet";
    }else if(avgVolume < 10){
        speak = speak + "not working or you are not talking";
    }else{
        speak = speak + "at a good volume";
    }
    
    console.log("SPEAK:", speak);
    console.log("average volume:", avgVolume, "peak volume:", peak);
    
    liveRegion.innerHTML = speak;
    
    //optionally you could just read out the current volume level and that would do away with the need for tracking averages etc..
    //liveRegion.innerHTML = "Your microphone volums level is " + globalVars.currentVolume;
    
}
#audio-indicator{
    width: 100px;
    height: 100px;
    position: relative;
    background-color: #333;
}
/** make sure we have a focus indicator, if you decide to make the item interactive with the mouse then also add a different hover state. **/
#audio-indicator:focus{
   outline: 2px solid #666;
   outline-offset: 3px;
}
#audio-indicator:hover{
   background-color: #666;
   cursor: pointer;
}


/*************we simply hide the indicator if the user has indicated that they do not want to see animations**********/
@media (prefers-reduced-motion) {
    #audio-indicator{
       display: none;
    }
}

/***********my visually hidden class for hiding content visually but still making it screen reader accessible, preferable to sr-only etc as futureproofed and better compatibility********/
.visually-hidden { 
    border: 0;
    padding: 0;
    margin: 0;
    position: absolute !important;
    height: 1px; 
    width: 1px;
    overflow: hidden;
    clip: rect(1px 1px 1px 1px); /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
    clip: rect(1px, 1px, 1px, 1px); /*maybe deprecated but we need to support legacy browsers */
    clip-path: inset(50%); /*modern browsers, clip-path works inwards from each corner*/
    white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
}


#audio-indicator .bar{
   display: none;
}
#audio-indicator.on .bar{
   display: block;
}

.bar, .button-text{
    position: absolute;
    top: 50%;
    left: 50%;
    min-height: 2px;
    width: 25%;
    transition: all 0.1s linear;
}
.button-text{
   width: 90px;
   transform: translate(-50%,-50%);
   text-align: center;
   color: #fff;
}
.bar1{
    transform: translate(-175%,-50%);
}
.bar2{
    transform: translate(-50%,-50%);
}
.bar3{
    transform: translate(75%,-50%);
}
.green{
    background-color: green;
}
.orange{
   background-color: orange;
}
.red{
   background-color: red;
}
<a href="#">dummy link for focus</a>
<div class="audio-indicator-announce visually-hidden" aria-live="assertive">
    
    </div>
<div id="audio-indicator" tabindex="0">
    
    <div class="button-text">Mic<span class="visually-hidden">rophone</span> Level indicator off (click to turn on)</div>
    <div class="bar bar1"></div>
    <div class="bar bar2"></div>
    <div class="bar bar3"></div>
</div>

<a href="#">dummy link for focus</a>

最后的想法

虽然上面的方法可行,但问题是“从用户体验的角度来看,EQ 图是否相关/良好的体验?”。

您必须通过用户测试才能找到答案。

使用类似于缩放的方法可能更可取(稍作调整并提供可访问的界面)。

  1. 在通话之前(或通话期间)允许用户先收听预先录制的声音。这允许他们设置自己的音量级别。
  2. 然后允许用户发言,然后播放他们的音频,以便他们可以检查他们的麦克风电平/他们的麦克风是否工作。
  3. 允许用户打开“自动调整我的麦克风音量”,并使用与我在通话时使用的平均音量类似的方法在整个通话过程中自动调整音量。
  4. 您还可以有一个屏幕 reader 选项,允许系统在自动调整音量时发出通知。
  5. 您还可以在麦克风图标/按钮上有一个视觉指示器,如果您愿意,如果音量自动调整,它会显示向上或向下箭头。

这显然只是一个想法,您可能有一个很好的 EQ 图形做事方式的用例,正如我所说,这只是一个想法!