SOUND / 155

Low double tone

低い二音のサイン
三角波の二つの音を下げ、ほかの通知と違う音色にします。

再生ボタンを押すと音が鳴ります。音量を調整してお試しください。

ラボで詳しく試す ↗

ひとつ試すなら

どこが変わるかを観察して、似た表現と見比べてみよう。

こんなときに

入力内容の確認など、操作の見直しを促す場面。

仕組みと使い方

エラーの理由と直し方は、見える場所にも示します。

エラー低い注意
実装コードを見る
<!doctype html><html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sound specimen</title><style>body{font:16px system-ui;padding:32px;max-width:540px;margin:auto;background:#fafaf7;color:#222}button{font:inherit;padding:10px 20px;background:#222;color:#fff;border:0;border-radius:999px;margin-right:8px}label{display:block;margin-top:24px}input{accent-color:#222}p{font-size:14px}</style><button id="play">聴いてみる</button><button id="stop">停止</button><label>音量 <input id="volume" type="range" min="0" max="0.5" step="0.01" value="0.2"></label><p id="status" aria-live="polite">再生ボタンを押すと音が出ます。</p><script>const createSoundEngine=function createSoundEngine(){
 let context=null,active=null,serial=0,pendingOwner=null;
 const clamp=v=>Math.min(.5,Math.max(0,Number(v)||0));
 function stop(owner){if(pendingOwner===owner){serial++;pendingOwner=null}if(active?.owner===owner)active.stop()}
 function volume(owner,value){if(active?.owner===owner)active.master.gain.setTargetAtTime(clamp(value),context.currentTime,.015)}
 async function play(preset,level=.2,onEnd=()=>{},owner={}){
  const token=++serial;pendingOwner=owner;if(active)active.stop();
  const AC=globalThis.AudioContext||globalThis.webkitAudioContext;
  if(!AC)throw new Error('このブラウザではWeb Audioを利用できません。');
  context ||= new AC();if(context.state==='suspended')await context.resume();if(token!==serial)return null;pendingOwner=null;
  const master=context.createGain(),analyser=context.createAnalyser();master.gain.value=clamp(level);analyser.fftSize=256;master.connect(analyser);analyser.connect(context.destination);
  const start=context.currentTime+.012,nodes=[],sources=[];let end=.1,finished=false,timer;
  const keep=n=>(nodes.push(n),n);
  function tone(frequency,delay,duration,type='sine',amplitude=.35,endFrequency=null,attack=.006){
   const osc=keep(context.createOscillator()),gain=keep(context.createGain());sources.push(osc);const t=start+delay,a=Math.min(attack,duration*.4);
   osc.type=type;osc.frequency.setValueAtTime(frequency,t);if(endFrequency)osc.frequency.exponentialRampToValueAtTime(endFrequency,t+duration);
   gain.gain.setValueAtTime(.0001,t);gain.gain.exponentialRampToValueAtTime(Math.max(.0002,amplitude),t+a);gain.gain.exponentialRampToValueAtTime(.0001,t+duration);
   osc.connect(gain);gain.connect(master);osc.start(t);osc.stop(t+duration+.015);end=Math.max(end,delay+duration+.03);
  }
  function noise(delay,duration,amplitude,filterType,frequency,endFrequency=null,attack=.004){
   const buffer=context.createBuffer(1,Math.ceil(context.sampleRate*duration),context.sampleRate),data=buffer.getChannelData(0);let seed=9631;
   for(let i=0;i<data.length;i++){seed=(Math.imul(seed,1664525)+1013904223)|0;data[i]=((seed>>>0)/4294967296)*2-1}
   const source=keep(context.createBufferSource()),filter=keep(context.createBiquadFilter()),gain=keep(context.createGain());sources.push(source);const t=start+delay;
   source.buffer=buffer;filter.type=filterType;filter.Q.value=.7;filter.frequency.setValueAtTime(frequency,t);if(endFrequency)filter.frequency.exponentialRampToValueAtTime(endFrequency,t+duration);
   gain.gain.setValueAtTime(.0001,t);gain.gain.exponentialRampToValueAtTime(amplitude,t+Math.min(attack,duration*.4));gain.gain.exponentialRampToValueAtTime(.0001,t+duration);
   source.connect(filter);filter.connect(gain);gain.connect(master);source.start(t);source.stop(t+duration+.015);end=Math.max(end,delay+duration+.03);
  }
  switch(preset){
   case 'click':noise(0,.035,.5,'highpass',1800);tone(1600,0,.025,'sine',.16);break;
   case 'pop':tone(650,0,.18,'sine',.65,170,.004);break;
   case 'tap':tone(180,0,.12,'sine',.48,115,.004);noise(0,.055,.12,'lowpass',900);break;
   case 'notify-up':tone(440,0,.25,'sine',.42);tone(660,.14,.36,'sine',.42);break;
   case 'notify-down':tone(660,0,.25,'sine',.42);tone(440,.16,.38,'sine',.42);break;
   case 'success':tone(523.25,0,.22,'sine',.32);tone(659.25,.11,.23,'sine',.32);tone(783.99,.22,.24,'sine',.32);tone(1046.5,.34,.5,'sine',.36);break;
   case 'error':tone(196,0,.23,'triangle',.38);tone(146.83,.2,.37,'triangle',.38);break;
   case 'warning':tone(740,0,.15,'triangle',.28);tone(740,.24,.15,'triangle',.28);break;
   case 'switch':noise(0,.022,.42,'highpass',2200);noise(.065,.038,.36,'bandpass',1700);break;
   case 'bell':tone(660,0,1.45,'sine',.4);tone(1326,0,.85,'sine',.13);tone(1848,0,.52,'sine',.07);break;
   case 'chime':tone(783.99,0,.65,'sine',.29);tone(1046.5,.2,.8,'sine',.29);tone(1318.51,.4,1,'sine',.25);break;
   case 'shimmer':[988,1480,1175,1760,1319,1976,1568,2093].forEach((f,i)=>tone(f,i*.07,.46,'sine',.11,null,.018));break;
   case 'sweep':tone(160,0,.72,'triangle',.27,1300,.1);break;
   case 'whoosh':noise(0,.85,.7,'bandpass',400,4200,.23);break;
   case 'pulse':[0,.24,.48].forEach(t=>tone(440,t,.1,'sine',.4,null,.01));break;
   case 'heartbeat':[0,.19,.75,.94].forEach((t,i)=>tone(i%2?67:83,t,.17,'sine',i%2?.42:.6,45,.01));break;
   default:master.disconnect();analyser.disconnect();throw new Error('音の種類が見つかりません。');
  }
  const handle={owner,master,analyser,duration:end,stop(){if(finished)return;finished=true;clearTimeout(timer);sources.forEach(s=>{try{s.stop()}catch{}});nodes.forEach(n=>{try{n.disconnect()}catch{}});master.disconnect();analyser.disconnect();if(active===handle)active=null;onEnd()}};
  active=handle;timer=setTimeout(()=>handle.stop(),(end+.08)*1000);return handle;
 }
 return{play,stop,volume};
};const engine=createSoundEngine(),owner={},status=document.querySelector('#status'),volume=document.querySelector('#volume');document.querySelector('#play').addEventListener('click',async()=>{try{status.textContent='再生中';await engine.play("error",Number(volume.value),()=>status.textContent='再生終了',owner)}catch(error){status.textContent=error.message}});document.querySelector('#stop').addEventListener('click',()=>engine.stop(owner));volume.addEventListener('input',()=>engine.volume(owner,Number(volume.value)));addEventListener('pagehide',()=>engine.stop(owner),{once:true});</script></html>