after many hours looking for a solution by myself, I'm asking for your help.
I'd like to play audio by clicking on an img and this is how I organised my code in HTML, audio will be different for each class : a_ru.mp3, b_ru.mp3 etc.
<span class="btn-audio-lexique">
<audio src="http://localhost/linguami-offline/fr/mp3/a_ru.mp3"></audio>
<img src="http://localhost/linguami-offline/img/circled-play.png">
</span>
<span class="btn-audio-lexique">
<audio src="http://localhost/linguami-offline/fr/mp3/b_ru.mp3"></audio>
<img src="http://localhost/linguami-offline/img/circled-play.png">
</span>
JS part which is unfortunately not workings looks like that
var classname = document.getElementsByClassName("btn-audio-lexique");
function playSound() {
var audio = classname.firstElementChild;
audio.play();
};
Array.from(classname).forEach(function(element) {
element.addEventListener('click', playSound);
});
I also tried with a for loop
for (var i = 0; i < classname.length; i++) {
classname[i].addEventListener('click', playSound);
}
console returns me a "audio is undefined error".
I would like to avoid creating a different id for all the audio in my page.
does anyone have an idea how should I organise my code for this works?
Thanks in advance !
You have few mistakes in your code:
I. Do not add event listener to each action element. It's bad for performance reasons. You should add event listener to some common parent element, which holds all the children buttons.
document.querySelector('.myAwesomeContainer').addEventListener('click', function(e) {
var target = e.target;
if (target && target.classList.contains('btn-audio-lexique')) {
var audio = target.firstElementChild;
// var audio = target.querySelector('audio');
if (audio) {
audio.play();
}
}
});
II. You don't necessarily need to mess up with audio elements. You can use AudioAPI and play sounds like:
var audio = new Audio('audio_file.mp3');
audio.play();
III. DO NOT use <span>, <div> or any HTML tag as clickable stuff except <button>, <a> Because you're destroing semantic markup and reducing accessability of you're application to the ground.
The actual reason of error in your current code, is hidden in that row:
var audio = classname.firstElementChild;
You're trying to get audio element not from the actual SPAN which user clicked, but from the array-like object of all your spans. Because classname is a LIST of all spans. What can you do in that situation? Just add event param to your playSound function. Because addEventListener will pass that param when event will be triggered:
var classname = document.getElementsByClassName("btn-audio-lexique");
function playSound(e) {
// Get actual clicked element
var target = e.target;
var audio = target.firstElementChild;
if (audio) {
audio.play();
}
};
Array.from(classname).forEach(function(element) {
element.addEventListener('click', playSound);
});
Related
EDIT #1 with connexo’s solution
Within the following HTML, I want to click in the parent 'li' to play the child 'audio'
The problem is noted in the alert calls within the JS; i.e., trying to compare a HTMLLIElement with a HTMLAudioElement.
How can I successfully do that?
HTML:
I have several:
<li>
text here<br>
<audio controls preload="auto">
<source src="aSong.mp3">
</audio>
</li>
JS:
$('li:has(audio)').on("click", function(evt) {
var m, theSongInArray,
// thisSong = $(this)[0]; // HTMLLIElement
// now a HTMLAudioElement thanks to connexo
$thisLI = $(this);
thisSong = $thisLI.find('audio')[0];
// itsAllSongs defined elsewhere
for (m=0; m < itsAllSongs.length; m++)
{
// HTMLAudioElement
theSongInArray = itsAllSongs[m];
if (thisSong == theSongInArray)
{
if ( thisSong.paused )
thisSong.play();
else
{
thisSong.pause();
thisSong.currentTime = 0;
}
}
}
});
Note: connexo's solution is perfect - but now I have a new problem =
My intent above, which connexo has solved, was to be able to click anywhere within 'li:has(audio)' and have the toggling effect between .play() and .pause() .. and it now does that.
BUT, now the anywhere within 'li:has(audio)' seems to not include the play icon of:
If I click on the slider, the progress bar, the time-stamp, or anywhere else on the long <audio> control it works great, but not on the play icon on the far left of the above control.
SUPER wierd - this 2nd challenge goes away (I can then click just on the play icon) if I remove:
$('li:has(audio)').on("click", function(evt) {
})
Hopefully, connexo has another brilliant solution.
EDIT #2 = solution of the current problem:
Now, I can click on either the li area surrounding the <audio> element, or on just the <audio> element itself. As a matter of interest, when I click on just the play icon of <audio>, the song will play and, if I click on the position slider, the slider will move without playing/pausing the song itself:
/*
alert(evt.target); // = HTMLLIElement, or HTMLAudioElement
*/
if (evt.target.id.length == 0)
thisSong = $(this).find('audio')[0]; // HTMLLIElement -> HTMLAudioElement
else // >= 3, e.g., = "SSB" or "AIRFORCE"
thisSong = $(this)[0]; // [object HTMLAudioElement]
I have some dynamic cards:
and I want to add eventListener to all play buttons (their code is):
<a href="link">
<img src="play.svg">
<audio src="myMusic.mp3"></audio>
</a>
When I click to "play", it should look like below:
(it should change image and should also play audio)
My JS code so far done is:
<script>
var imgsPlay = document.getElementsByClassName('play-img-loop');
var audioPlayers = document.getElementsByClassName('audio-player');
window.onload = function() {
for(var i = 0; i < imgsPlay.length; i++) {
imgsPlay[i].addEventListener('click', function(event) {
if(this.getAttribute('src') == "img/icons/play.svg") {
this.setAttribute('src', "img/icons/play_red.svg");
audioPlayers[i].play();
} else if(this.getAttribute('src') == "img/icons/play_red.svg") {
this.setAttribute('src', "img/icons/play.svg");
audioPlayers[i].pause();
}
});
}
}
</script>
(I can do it manually, but) can not dynamically. How can I do this ?
You need to delegate! If not, then your question is just a dupe of JavaScript closure inside loops – simple practical example
Here I assume you have a container called cardContainer
I am a little confused as to _red means playing or paused, so change the code below to match. Your example HTML does not match the code you show, there are no classes on the player and image, I therefor assume you do have those in the actual code
window.addEventListener('load', function() {
document.getElementById('cardContainer').addEventListener('click', function(e) {
const tgt = e.target;
if (tgt.classList.contains('play-img-loop')) {
const src = tgt.getAttribute('src');
const running = src.includes('_red');
const audioPlayer = tgt.closest('a').querySelector('audio');
tgt.setAttribute('src', `img/icons/play$(running?'':'_red').svg`);
if (running) audioPlayer.pause();
else audioPlayer.play();
}
});
});
Your problem is the incorrect handling of closures. The event listener call back will never get the correct value of i by the way you have written it. The value of i will always be the last one when the events are fired.
The easiest workaround is to define the variable i only for the scope of a single iteration - which is what the let do.
for(let i = 0; i < imgsPlay.length; i++) {
imgsPlay[i].addEventListener('click', function(event) {
if(this.getAttribute('src') == "img/icons/play.svg") {
...
}
Useful reference: setTimeout in for-loop does not print consecutive values
What I want to achieve: To stop a previously started sound when a new is started. What I have now: The sounds plays simultanously, none of them is stopping. The probably reason: Wrong logic statement in a line if (audio.played && audio.paused){. Before you judge me for not trying hard enough - I am trying to solve this from 3 days, I am a beginner. It should take me a few minutes, even an hour. I tried in several combinations.At the end I listed several websites which I tried and I still haven't solved it. In all answers is something similar but still I can't made a browser to chose, always only one part is executed either audio.play() or audio.pause() in the log. It works but not as I want and these logical statements are like on other informations on the forum. At the end of the message you can see all similar topics I already tried several times. I kept just as clear code as possible and I want to do it this way, in vanilla javascript because I won't deal for now with anything more complicated. An audio url is taken from modified id on click, the audios are on my disk. It works, I made some mistake in line if (audio.played && audio.paused){ Any ideas except giving up and changing a hobby?
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Timeout</title>
</head>
<script>
function playAudio3(clicked_id) {
var OriginalID = (clicked_id);
var res = OriginalID.slice(5,-1);
var audioID=res+'.mp3';
var url =audioID;
var audio = new Audio(url);
if (audio.played && audio.paused){
audio.play();
console.log('audio.play was used.');
}else {
audio.currentTime = 0
audio.pause();
console.log('audio.pause was used.');
}
}
</script>
<body>
<span id="guita2x"onclick="playAudio3(this.id);"> Guitar. </span>
<span id="piano2x" onclick="playAudio3(this.id);"> Piano. </span>
<span id="basse15x" onclick="playAudio3(this.id);"> Basse. </span>
</body>
</html>
how do i stop a sound when a key is released in JavaScript
Javascript Audio Play on click
Javascript to stop playing sound when another starts
HTML5 Audio stop function
cancel an if statement if another statement comes true
Stopping audio when another audio file is clicked using jQuery while obscuring link
https://www.w3schools.com/jsref/dom_obj_audio.asp
HTML5 Audio pause not working
Never give up, and don't change a hobby. :)
Possible solution from one hobbyist, too:
files = ['guitar', 'piano', 'bass']; // name of your files in array. You can get this array from looping through your html/spans id's too, but, this is basic logic
audios = []; //empty audios object array
for (i = 0; i < files.length; i++) {
var audioID = files[i] + '.mp3';
var url = audioID;
var audio = new Audio(url);
audios.push(audio); //place all audio objects into array
}
console.log(audios);
//now - basic logic to check which audio element to play, and to stop others
function playAudio3(clicked_id) {
for (i = 0; i < audios.length; i++) {
if (audios[i].src.includes(clicked_id)) {
audios[i].play();
} else {
audios[i].currentTime = 0
audios[i].pause();
}
}
}
So, you are creating all audio objects at the page load, and then choose which one to play. Of course, this will play all audio files from the start. If you want to keep the track of the audio progress, and play it from the last pause, you will need some additions, but, this could be the start, i hope.
OK, updated, i kept some of your code, and slightly changed some things in HTML.
Your complete code now should look like this: (HTML)
<span class='aud' id="guita1x" onclick="playAudio3(this.id);"> Guitar. </span>
<span class='aud' id="piano2x" onclick="playAudio3(this.id);"> Piano. </span>
<span class='aud' id="basse3x" onclick="playAudio3(this.id);"> Basse. </span>
Js:
spans=document.querySelectorAll('.aud');
console.log(spans);
audios = []; //empty audios object array
for (i = 0; i < spans.length; i++) {
var OriginalID = spans[i].id;
var res = OriginalID.slice(5,-1);
var audioID=res+'.mp3';
var url =audioID;
var audio = new Audio(url);
audios.push(audio); //place all audio objects into array
}
console.log(audios);
//now - basic logic to check which audio element to play, and to stop others
function playAudio3(clicked_id) {
for (i = 0; i < audios.length; i++) {
clickid=clicked_id.replace(/[A-Za-z]/g,'')+'.mp3';
if (audios[i].src.includes(clickid)) {
audios[i].play();
} else {
audios[i].currentTime = 0
audios[i].pause();
}
}
}
Now, i have just added class 'aud' to your spans, for easier targeting.
Then, i have collected id's from the spans and placed it in the audios array. (Not sure why you are slicing names/ids, you can simplify things by adding just numbers: 1, 2, 3 and so on).
NOW, this MUST work, IF
you have 3 .mp3 files in the same folder as your html/js file, called: 1.mp3, 2.mp3, 3.mp3.
if you place your javascript bellow HTML, BEFORE closing 'body' tag.
I am making a web app that can be open for a long time. I don't want to load audio at load time (when the HTML gets downloaded and parsed) to make the first load as fast as possible and to spare precious resources for mobile users. Audio is disabled by default.
Putting the audio in CSS or using preload is not appropriate here because I don't want to load it at load time with the rest.
I am searching for the ideal method to load audio at run time, (after a checkbox has been checked, this can be after 20 minutes after opening the app) given a list of audio elements.
The list is already in a variable allSounds. I have the following audio in a webpage (there are more):
<audio preload="none">
<source src="sound1.mp3">
</audio>
I want to keep the same HTML because after second visit I can easily change it to (this works fine with my server-side HTML generation)
<audio preload="auto">
<source src="sound1.mp3">
</audio>
and it works.
Once the option is turned on, I want to load the sounds, but not play them immediately. I know that .play() loads the sounds. But I want to avoid the delay between pressing a button and the associated feedback sound.
It is better to not play sound than delayed (in my app).
I made this event handler to load sounds (it works) but in the chrome console, it says that download was cancelled, and then restarted I don't know exactly what I am doing wrong.
Is this is the correct way to force load sounds? What are the other ways? If possible without changing the HTML.
let loadSounds = function () {
allSounds.forEach(function (sound) {
sound.preload = "auto";
sound.load();
});
loadSounds = function () {}; // Execute once
};
here is playSound function, but not very important for the questions
const playSound = function (sound) {
// PS
/* only plays ready sound to avoid delays between fetching*/
if (!soundEnabled)) {
return;
}
if (sound.readyState < sound.HAVE_ENOUGH_DATA) {
return;
}
sound.play();
};
Side question: Should there be a preload="full" in the HTML spec?
See also:
Preload mp3 file in queue to avoid any delay in playing the next file in queue
how we can Play Audio with text highlight word by word in angularjs
To cache the audio will need to Base64 encode your MP3 files, and start the Base64 encoded MP3 file with data:audio/mpeg;base64,
Then you can pre-load/cache the file with css using something like:
body::after {
content:url(myfile.mp3);
display:none;
}
I think I would just use the preloading functionality without involving audio tag at all...
Example:
var link = document.createElement('link')
link.rel = 'preload'
link.href = 'sound1.mp3'
link.as = 'audio'
link.onload = function() {
// Done loading the mp3
}
document.head.appendChild(link)
I'm quite sure that I've found a solution for you. As far as I'm concerned, your sounds are additional functionality, and are not required for everybody. In that case I would propose to load the sounds using pure javascript, after user has clicked unmute button.
A simple sketch of solution is:
window.addEventListener('load', function(event) {
var audioloaded = false;
var audioobject = null;
// Load audio on first click
document.getElementById('unmute').addEventListener('click', function(event) {
if (!audioloaded) { // Load audio on first click
audioobject = document.createElement("audio");
audioobject.preload = "auto"; // Load now!!!
var source = document.createElement("source");
source.src = "sound1.mp3"; // Append src
audioobject.appendChild(source);
audioobject.load(); // Just for sure, old browsers fallback
audioloaded = true; // Globally remember that audio is loaded
}
// Other mute / unmute stuff here that you already got... ;)
});
// Play sound on click
document.getElementById('playsound').addEventListener('click', function(event) {
audioobject.play();
});
});
Of course, button should have id="unmute", and for simplicity, body id="body" and play sound button id="playsound. You can modify that of course to suit your needs. After that, when someone will click unmute, audio object will be generated and dynamically loaded.
I didn't try this solution so there may be some little mistakes (I hope not!). But I hope this will get you an idea (sketch) how this can be acomplished using pure javascript.
Don't be afraid that this is pure javascript, without html. This is additional functionality, and javascript is the best way to implement it.
You can use a Blob URL representation of the file
let urls = [
"https://upload.wikimedia.org/wikipedia/commons/b/be/Hidden_Tribe_-_Didgeridoo_1_Live.ogg"
, "https://upload.wikimedia.org/wikipedia/commons/6/6e/Micronesia_National_Anthem.ogg"
];
let audioNodes, mediaBlobs, blobUrls;
const request = url => fetch(url).then(response => response.blob())
.catch(err => {throw err});
const handleResponse = response => {
mediaBlobs = response;
blobUrls = mediaBlobs.map(blob => URL.createObjectURL(blob));
audioNodes = blobUrls.map(blobURL => new Audio(blobURL));
}
const handleMediaSelection = () => {
const select = document.createElement("select");
document.body.appendChild(select);
const label = new Option("Select audio to play");
select.appendChild(label);
select.onchange = () => {
audioNodes.forEach(audio => {
audio.pause();
audio.currentTime = 0;
});
audioNodes[select.value].play();
}
select.onclick = () => {
const media = audioNodes.find(audio => audio.currentTime > 0);
if (media) {
media.pause();
media.currentTime = 0;
select.selectedIndex = 0;
}
}
mediaBlobs.forEach((blob, index) => {
let option = new Option(
new URL(urls[index]).pathname.split("/").pop()
, index
);
option.onclick = () => console.log()
select.appendChild(option);
})
}
const handleError = err => {
console.error(err);
}
Promise.all(urls.map(request))
.then(handleResponse)
.then(handleMediaSelection)
.catch(handleError);
I have a little html5 application where you can play a sound by clicking a button.
I have a function that adds an <audio> tag to a <div> with an id "playing." The sound removes itself when it is done.
function sound(track){
$("#playing").append("<audio src=\"" + track + "\" autoplay onended=\"$(this).remove()\"></audio>");
}
For the button I have:
<button onclick="sound('sounds/tada.mp3')">Tada</button>
When I click the button, an <audio> briefly appears in the element inspector and disappears when it is finished, just the way I want it, but after triggering it two times, it just stops working in Chrome, at least. There are no errors in the console either.
What is going on?
Get rid of the onclick/onend in your HTML and reference the button in your js:
HTML
<button id='tada' sound_url='sounds/tada.mp3'>Tada</button>
And the JS
var sound = function(track){
$("#playing").append("<audio id='played_audio' src='\" + track + \"' autoplay='true'></audio>");
}
$('#tada').on('click', function () {
var sound_url = $(this).attr('sound_url');
sound(sound_url);
});
$('#playing').on('end', 'played_audio', function() {
$(this).remove();
});
Okay, lets see..
var audioURL = "http://soundbible.com/mp3/Canadian Geese-SoundBible.com-56609871.mp3";
var audioEl = null;
function removeAudio() {
if (audioEl && audioEl.parentNode)
audioEl.parentNode.removeChild(audioEl);
}
function sound() {
removeAudio();
audioEl = document.createElement("audio");
audioEl.src = audioURL;
audioEl.controls = true;
audioEl.addEventListener("ended", removeAudio); // <-- Note it's ended, not end!
document.getElementById("playing").appendChild(audioEl);
audioEl.play();
}
document.getElementById("tada").addEventListener("click", sound);
<div id="playing">
</div>
<button id="tada">Tada</button>
I'm not seeing any problems with this script.
Decide audioURL, set audioEl to null as it will be used later
When the element with ID "tada" is clicked, run our sound function.
Remove the audio.
Create the audio element.
When the audio is finished, remove the audio.
Append the audio to the element with ID "playing".
Play the audio.
One thing to note is that I use the ended event, not the end event.
(This answer is here because Andrew really wants us to answer it.)