HTML Canvas & Javascript - Triggering Audio by Sel

2019-01-20 06:50发布

问题:

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>

回答1:

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.

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.

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>



回答2:

You could create an Audio Loader, that loads all the audios and keeps track of them:

function load(srcs){
  var obj={};
  srcs.forEach(src=>obj[src]=new Audio(src));
  return obj;
}

Then you could do sth like this onload:

var audios=load(["audio1.mp3", "audio2.mp3"]);

And later:

(audios[src] || new Audio(src)).play();

This will just load the audio if it isnt already in the audios object.