HTML画布&Javascript - 通过选择(从多个地方)触发音频

2022-01-17 00:00:00 canvas javascript html html5-canvas

我的 HTML 画布中有一个选择菜单,我想触发相应的音频文件.我尝试通过在 if (this.hovered) & 中声明图像来实现这一点selectionForMenu 原型中 makeSelection 函数的 (this.clicked) 部分,这样在每次新选择时都会重新定义选定的音频文件,但这会导致加载缓慢和重叠的音频.这也是有问题的,因为我试图让屏幕底部的扬声器按钮也播放与当前选择相对应的音频,所以如果它只在该函数中定义,则 makeButton 函数.

I have a selection menu in my HTML canvas that I would like to trigger corresponding audio files. I have tried implementing this by declaring the images inside the if (this.hovered) & (this.clicked) part of the makeSelection function within the selectionForMenu prototype, such that on each new selection the selected audio file is redefined, but this causes problems like slow loading and overlapping audio. It is also problematic as I am trying to get the speaker button at the bottom of the screen to play the audio corresponding to the current selection too, so if it is only defined within that function it is not accessible to the makeButton function.

您可以在下面的代码段中看到选择菜单和扬声器按钮.菜单中的每个新选择都应该播放一次与其对应的音频文件(我无法将其添加到此演示中).可以通过重新单击选择或单击扬声器按钮来重放它,但每次单击都应该只引发一次音频播放,当然重叠是不希望的.任何帮助将不胜感激.

You can see the selection menu and speaker button in the snippet below. Each new selection in the menu should play once an audio file that corresponds to it (which I have not been able to add to this demonstration). It can be replayed by re-clicking the selection or clicking the speaker button, but each click should only provoke one play of the audio and of course overlapping is undesired. Any help will be appreciated.

var c=document.getElementById('game'),
		canvasX=c.offsetLeft,
		canvasY=c.offsetTop,
		ctx=c.getContext('2d');

var button = function(id, x, strokeColor) {
	this.id = id;
	this.x = x;
	this.strokeColor = strokeColor;
	this.hovered = false;
	this.clicked = false;
}

button.prototype.makeInteractiveButton = function() {
	if (this.hovered) {
		if (this.clicked) {
			this.fillColor = '#DFBCDE';
		} else {
			this.fillColor = '#CA92C8'
		}
	} else {
		this.fillColor = '#BC77BA'
	}
	ctx.strokeStyle=this.strokeColor;
	ctx.fillStyle=this.fillColor;
	ctx.beginPath();
	ctx.lineWidth='5';
	ctx.arc(this.x, 475, 20, 0, 2*Math.PI);
	ctx.closePath();
	ctx.stroke();
	ctx.fill();
}

button.prototype.hitTest = function(x, y) {
	return (Math.pow(x-this.x, 2) + Math.pow(y-475, 2) < Math.pow(20, 2));
}

var selectionForMenu = function(id, text, y) {
	this.id = id;
	this.text = text;
	this.y = y;
	this.hovered = false;
	this.clicked = false;
	this.lastClicked = false;
}

selectionForMenu.prototype.makeSelection = function() {
	var fillColor='#A84FA5';
	if (this.hovered) {
		if (this.clicked) {
			if (this.lastClicked) {
				fillColor='#E4C7E2';
			} else {
				fillColor='#D5A9D3';
			}
		} else if (this.lastClicked) {
			fillColor='#D3A4D0';
		} else {
			fillColor='#BA74B7';
		}
	} else if (this.lastClicked) {
		fillColor='#C78DC5';
	} else {
		fillColor='#A84FA5';
	}
	ctx.beginPath();
	ctx.fillStyle=fillColor;
	ctx.fillRect(0, this.y, 350, 30)
	ctx.stroke();

	ctx.font='10px Noto Sans';
	ctx.fillStyle='white';
	ctx.textAlign='left';
	ctx.fillText(this.text, 10, this.y+19);
}

selectionForMenu.prototype.hitTest = function(x, y) {
	return (x >= 0) && (x <= (350)) && (y >= this.y) && (y <= (this.y+30)) && !((x >= 0) && (y > 450));
}

var Paint = function(element) {
	this.element = element;
	this.shapes = [];
}

Paint.prototype.addShape = function(shape) {
	this.shapes.push(shape);
}

Paint.prototype.render = function() {
	ctx.clearRect(0, 0, this.element.width, this.element.height);

	for (var i=0; i<this.shapes.length; i++) {
		try {
			this.shapes[i].makeSelection();
		}
		catch(err) {}
	}

	ctx.beginPath();
	ctx.fillStyle='#BC77BA';
	ctx.fillRect(0, 450, 750, 50);
	ctx.stroke();

	for (var i=0; i<this.shapes.length; i++) {
		try {
			this.shapes[i].makeInteractiveButton();
		}
		catch(err) {}
	}

	var speaker = new Image(25, 25);
	speaker.src='https://i.stack.imgur.com/lXg2I.png';
	ctx.drawImage(speaker, 162.5, 462.5);
}

Paint.prototype.setHovered = function(shape) {
	for (var i=0; i<this.shapes.length; i++) {
		this.shapes[i].hovered = this.shapes[i] == shape;
	}
	this.render();
}

Paint.prototype.setClicked = function(shape) {
	for (var i=0; i<this.shapes.length; i++) {
		this.shapes[i].clicked = this.shapes[i] == shape;
	}
	this.render();
}

Paint.prototype.setUnclicked = function(shape) {
	for (var i=0; i<this.shapes.length; i++) {
		this.shapes[i].clicked = false;
		if (Number.isInteger(this.shapes[i].id)) {
			this.shapes[i].lastClicked = this.shapes[i] == shape;
		}
	}
	this.render();
}

Paint.prototype.select = function(x, y) {
	for (var i=this.shapes.length-1; i >= 0; i--) {
		if (this.shapes[i].hitTest(x, y)) {
			return this.shapes[i];
		}
	}
	return null
}

var paint = new Paint(c);
var btn = new button('speaker', 175, '#FFFCF8');
var selection = [];
for (i=0; i<15; i++) {
	selection.push(new selectionForMenu(i+1, i, i*30));
}

paint.addShape(btn);
for (i=0; i<15; i++) {
	paint.addShape(selection[i])
}

paint.render();

function mouseDown(event) {
	var x = event.x - canvasX;
	var y = event.y - canvasY;
	var shape = paint.select(x, y);

	paint.setClicked(shape);
}

function mouseUp(event) {
	var x = event.x - canvasX;
	var y = event.y - canvasY;
	var shape = paint.select(x, y);

	paint.setUnclicked(shape);
}

function mouseMove(event) {
	var x = event.x - canvasX;
	var y = event.y - canvasY;
	var shape = paint.select(x, y);

	paint.setHovered(shape);
}

c.addEventListener('mousedown', mouseDown);
c.addEventListener('mouseup', mouseUp);
c.addEventListener('mousemove', mouseMove);

canvas {
  z-index: -1;
  margin: 1em auto;
  border: 1px solid black;
  display: block;
  background: #9F3A9B;
}

img {
  z-index: 0;
  position: absolute;
  pointer-events: none;
}

#speaker {
  top: 480px;
  left: 592px;
}

#snail {
  top: 475px;
  left: 637.5px;
}

<!doctype html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>uTalk Demo</title>
	<link rel='stylesheet' type='text/css' href='wordpractice.css' media='screen'></style>
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
</head>
<body>
	<canvas id="game" width = "350" height = "500"></canvas>
  <script type='text/javascript' src='wordpractice copy.js'></script>
</body>
</html>

推荐答案

当您想要音频响应时,忘记 MediaElements,而使用 Web Audio API.MediaElements(<audio><video>)很慢,而且 http 缓存是一场噩梦.

When you want responsiveness with audio, forget about MediaElements, and go with the Web Audio API. MediaElements (<audio> and <video>) are slow, and http caching is an nightmare.

使用 Web Audio API,您可以首先将所有媒体下载为 arrayBuffers,将其音频数据解码为 AudioBuffers,然后附加到您的 js 对象.从那里,您将能够在 µs 内播放这些媒体的新实例.

With the Web Audio API, you can first download all you media as arrayBuffers, decode their audio data to AudioBuffers, that you'll attach to your js objects. From there, you'll be able to play new instances of these media in µs.

注意,下面的 ES6 语法,对于旧版浏览器,这里是 ES5 重写,还要注意 Internet Explorer <Edge 不支持 Web Audio API,如果您需要支持这些浏览器,则必须使用音频元素进行后备.

Beware, ES6 syntax below, for older browsers, here is an ES5 rewrite, also note that Internet Explorer < Edge does not support the Web Audio API, if you need to support these browsers, you'll have to make an fallback with audio elements.

(function myFirstDrumKit() {

  const db_url = 'https://dl.dropboxusercontent.com/s/'; // all our medias are stored on dropbox

  // we'll need to first load all the audios
  function initAudios() {
    const promises = drum.parts.map(part => {
      return fetch(db_url + part.audio_src) // fetch the file
        .then(resp => resp.arrayBuffer()) // as an arrayBuffer
        .then(buf => drum.a_ctx.decodeAudioData(buf)) // then decode its audio data
        .then(AudioBuf => {
          part.buf = AudioBuf; // store the audioBuffer (won't change)
          return Promise.resolve(part); // done
        });
    });
    return Promise.all(promises); // when all are loaded
  }

  function initImages() {
    // in this version we have only an static image,
    // but we could have multiple per parts, with the same logic as for audios
    var img = new Image();
    img.src = db_url + drum.bg_src;
    drum.bg = img;
    return new Promise((res, rej) => {
      img.onload = res;
      img.onerror = rej;
    });
  }

  let general_solo = false;
  let part_solo = false;

  const drum = {
    a_ctx: new AudioContext(),
    generate_sound: (part) => {
      // called each time we need to play a source
      const source = drum.a_ctx.createBufferSource();
      source.buffer = part.buf;
      source.connect(drum.gain);
      // to keep only one playing at a time
      // simply store this sourceNode, and stop the previous one
      if(general_solo){
        // stop all playing sources
        drum.parts.forEach(p => (p.source && p.source.stop(0)));
        }
      else if (part_solo && part.source) {
        // stop only the one of this part
        part.source.stop(0);
      }
      // store the source
      part.source = source;
      source.start(0);
    },
    parts: [{
        name: 'hihat',
        x: 90,
        y: 116,
        w: 160,
        h: 70,
        audio_src: 'kbgd2jm7ezk3u3x/hihat.mp3'
      },
      {
        name: 'snare',
        x: 79,
        y: 192,
        w: 113,
        h: 58,
        audio_src: 'h2j6vm17r07jf03/snare.mp3'
      },
      {
        name: 'kick',
        x: 80,
        y: 250,
        w: 200,
        h: 230,
        audio_src: '1cdwpm3gca9mlo0/kick.mp3'
      },
      {
        name: 'tom',
        x: 290,
        y: 210,
        w: 110,
        h: 80,
        audio_src: 'h8pvqqol3ovyle8/tom.mp3'
      }
    ],
    bg_src: '0jkaeoxls18n3y5/_drumkit.jpg?dl=0',
  };
  drum.gain = drum.a_ctx.createGain();
  drum.gain.gain.value = .5;
  drum.gain.connect(drum.a_ctx.destination);


  function initCanvas() {
    const c = drum.canvas = document.createElement('canvas');
    const ctx = drum.ctx = c.getContext('2d');
    c.width = drum.bg.width;
    c.height = drum.bg.height;
    ctx.drawImage(drum.bg, 0, 0);
    document.body.appendChild(c);
    addEvents(c);
  }

  const isHover = (x, y) =>
    (drum.parts.filter(p => (p.x < x && p.x + p.w > x && p.y < y && p.y + p.h > y))[0] || false);


  function addEvents(canvas) {
    let mouse_hovered = false;
    canvas.addEventListener('mousemove', e => {
      mouse_hovered = isHover(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop)
      if (mouse_hovered) {
        canvas.style.cursor = 'pointer';
      } else {
        canvas.style.cursor = 'default';
      }
    })
    canvas.addEventListener('mousedown', e => {
      e.preventDefault();
      if (mouse_hovered) {
        drum.generate_sound(mouse_hovered);
      }
    });
    const checkboxes = document.querySelectorAll('input');
    checkboxes[0].onchange = function() {
      general_solo = this.checked;
      general_solo && (checkboxes[1].checked = part_solo = true);
    };
    checkboxes[1].onchange = function() {
      part_solo = this.checked;
      !part_solo && (checkboxes[0].checked = general_solo = false);
    };
  }
  Promise.all([initAudios(), initImages()])
    .then(initCanvas);

})()

/* 
Audio Samples are from https://sampleswap.org/filebrowser-new.php?d=DRUMS+%28FULL+KITS%29%2FSpasm+Kit%2F
Original image is from http://truimg.toysrus.co.uk/product/images/UK/0023095_CF0001.jpg?resize=500:500
*/

<label>general solo<input type="checkbox"></label><br>
<label>part solo<input type="checkbox"></label><br>

相关文章