OBS配信用アバター（mic）

<iframe width="560" height="400" src="https://www.youtube.com/embed/_RT3tDLMhzo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

```html:Avatar.html
<!DOCTYPE html>
<html lang="ja"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>

$(e=>{
	var a = new Avatar(30).addSprite(
		new Avatar.Sprite('100.png',0,100,150,150),
		new Avatar.Sprite('200.png',0,0,400,400),
		new Avatar.Pachi('300.png',100,50,200,100),
		new Avatar.Paku('400.png',100,100,200,100),
		new Avatar.Sprite('500.png',200,50,200,200),
	)	
	$('body').append(a.view)
})

class Avatar{
	constructor(...a){
		this.view = $('<div class="Avatar">').append(
			this.$content = $('<div class="content">')
		).click(e=>{
			this.view.off("click")
			this.voice = new Avatar.Voice()
			setInterval(e=>{
				this.loop.forEach(v=>{v.loop(this)})
				this.frame++
			},Math.round(1000/a[0]))
		})
		this.frame = 0
		this.loop = []
	}
	addSprite(...a){
		this.sprite = a
		a.forEach(v=>{
			this.$content.append(v.view)
			if(v.loop) this.loop.push(v)
		})
		return this
	}
}

Avatar.Voice = class{
	constructor(...a){
		this.volume	= 0
		navigator.mediaDevices.getUserMedia({audio:true}).then(stream=>{
			var c = new AudioContext();
			var s = c.createMediaStreamSource(stream)
			var p = c.createScriptProcessor(a[0]||1024)
			p.onaudioprocess = e=>{
				var d = e.inputBuffer.getChannelData(0)
				this.volume = d.reduce((a,b)=>a+Math.abs(b),0)/p.bufferSize
			}
			p.connect(c.destination)
			s.connect(p)
		})
	}
	speaking(){
		return this.volume>0.01
	}
}

Avatar.Sprite = class{
	constructor(...a){
		var s = a[0].split('.').shift()
		this.view = $('<div>').addClass('Sprite s'+s).css({
			backgroundImage:`url(${a[0]})`,
			left:a[1],top:a[2],width:a[3],height:a[4]
		})
		this.X = a[1]
		this.y = a[2]
		this.delta = Math.floor(60*Math.random())
		this.init(...a)
	}
	init(...a){
		var img = new Image()
		img.src = a[0]
		this.frame = []
		img.onload = e=>{
			var j = Math.floor(img.height/a[4])
			for(var i=0;i<j;i++){
				this.frame.push(-i*a[4])
			}
		}
	}
	loop(e){
		this.top(e)
		if(this.frame.length==0) return
		if(e.frame%5>0) return
		if(e.voice.speaking()) this.random()
	}
	top(e){
		this.view.css({top:this.y+Math.sin((e.frame+this.delta)/30)*3})
	}
	random(){
		var i = Math.floor(Math.random()*this.frame.length)
		this.view.css({backgroundPositionY:this.frame[i]})
	}
}

Avatar.Pachi = class extends Avatar.Sprite{
	init(){
		this.blink = '000121000000122222100000000000000000000000000000000000000000000'
	}
	loop(e){
		this.top(e)
		var f = e.frame%this.blink.length
		this.view.css({backgroundPositionY:-this.blink[f]*100})
	}
}

Avatar.Paku = class extends Avatar.Sprite{
	loop(e){
		this.top(e)
		if(this.frame.length==0) return
		if(e.frame%3>0) return
		if(e.voice.speaking()) {
			this.random()
		}else{
			this.view.css({backgroundPositionY:0})
		}
	}
}

</script>
<style>

*{margin:0;padding:0;}
body{background:#0F0;}
.Sprite{position:absolute;}
.Avatar{
	position:fixed;right:0;bottom:0;width:400px;height:400px;
	animation:horizontal 1.6s ease-in-out infinite alternate;
}
.Avatar .content{
	animation:vertical 1.3s ease-in-out infinite alternate;
}

@keyframes horizontal {
    0% { transform:translateX( -3px); }
  100% { transform:translateX(  0px); }
}
@keyframes vertical {
    0% { transform:translateY(-6px); }
  100% { transform:translateY(  0px); }
}

</style>
</head>
</html>
```