如何使音频电平指示器可访问? 我想到的进一步改进我尝试过的另一种方式 1重点宣布 2通过平均值并使用短语来宣布 3喜欢减少运动 4更改按钮文本

问题描述

我正在尝试为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.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>
  );
}

A picture of the audio indicator

在此示例中,您没有看到真实的音频输入。 “级别”伪造了该人会说话的声音。

您将如何使此类访问变得容易?您甚至需要这样做吗(因为检查各种提供程序的UI并没有显示任何特殊标签或aria标签)?

解决方法

前言

这很有趣!

首先道歉,该示例有点混乱,因为我尝试在过程的几个部分中为您提供选择,如果没有任何意义,请告诉我!

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

答案

宣布的实际部分很简单,您只需要在页面上放置visually hiddenaria-live段落,并更新其中的文本即可。

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

更难的是要获得良好的屏幕阅读器界面和播音员体验。

我如何处理公告

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

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

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

如果平均音量低于10,则我认为麦克风无法正常工作或他们没有说话。

否则,我宣布音量正常。

由于我显然模拟了波动的音量水平,因此实际数字可能会有所不同,因此需要对这些数字进行一些调整。

我要做的另一件事是确保aria-live区域仅每2秒更新一次。但是,我的屏幕阅读器播音员的速度很慢,因此您大概可以摆脱1200毫秒的干扰。

这是为了避免广播员队列过多。

或者,您可以将其设置为更大的值(例如10秒),因为它对于整个通话期间的持续监视很有用。如果您决定这样做,则将播音员设置为aria-live="polite",以免打扰其他屏幕阅读器播报。

我想到的进一步改进

我没有实现此功能,但是有两件事可以使它变得更准确(如果在整个通话过程中都无法使用,则可以减少烦人),如果您希望将其用作整个通话过程中提供持续监控工具。

首先,我将丢弃小于10的任何值作为音量,而仅取大于10的音量平均值。

这将更能指示用户积极讲话时的实际音量水平。

第二,如果有人在讲话,我将放弃所有音量级别的公告。您可能希望用户此时保持安静,所以不要告诉他们他们的麦克风很安静!另外,您真的不希望在其他人聊天时发出通知。

我尝试过的另一种方式

我确实尝试过每500毫秒将音量宣布为一个整数值,但是由于这是快照,所以我感觉发生的情况并不十分准确。这就是为什么我要平均音量。

然后我意识到您可以获得平均值50,但在100(剪切)时达到峰值,因此也为支票增加了峰值量。

其他事情

1。重点宣布

我这样做是为了让您在关注EQ图级别的事物时(不知道该怎么称呼!呵呵),它最初是在聚焦时宣布的。

我意识到作为切换按钮更好,以便用户可以随意打开和关闭它。

最好具有一个切换开关,因为它可以让您在收听公告时调小音量,也可以让其他所有人根据自己的喜好打开和关闭

2。通过平均值并使用短语来宣布

取平均值然后宣布一个短语而不是一个数字的另一个好处是,如果指示器保持打开状态,则用户体验会很好。

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

3。喜欢减少运动

患有前庭疾病(对运动障碍的敏感性)的使用者可能会被铁棒分散注意力。

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

现在,因为默认情况下我已关闭EQ,所以没有必要。但是,如果您决定默认情况下启用EQ,则可以使用prefers-reduced-motion媒体查询来设置慢得多的动画速度等。这就是为什么我将其保留下来,以作为如何使用它的示例(这是我使EQ专注于工作而不是进行拨动时的一个突出问题,因此不再需要。)

我认为实现此目标的最容易方法是默认情况下关闭EQ。这还可以帮助注意力不集中的人轻松地被铁杆分散注意力,并让癫痫/癫痫症患者的页面安全,因为铁杆可以在一秒钟内“闪” 3次以上。 >

4。更改按钮文本。

打开EQ时,我利用了visually-hidden类。

我更改了EQ中的文本,然后在视觉上将其隐藏,以便屏幕阅读器用户在聚焦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','green');
    }else{
       addClass('.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); /*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>

最终思想

虽然上述方法可行,但问题是“从UX角度来看,情商图是否相关/良好的体验?”。

您必须进行用户测试才能找出答案。

最好使用类似于Zoom的方法(稍作调整并可以访问界面)。

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

这显然只是一个想法,您可能有一个很好的用EQ图做事的用例,就像我说的那样!