Dynamic sounds not playing after two plays - javascript

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.)

Related

jQuery click Handler disables play button functionality in audio element

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]

Pause previous audio when new audio is played

When user clicks on an item, I am dynamically adding a unique audio element like this and playing an audio.
let sound = document.createElement("audio");
sound.id = "audio" + e.target.id;
app.audioBeingPlayed = sound.id;
sound.controls = "controls";
sound.src = selectedVoice[0][1].sample;
sound.type = "audio/mpeg";
sound.play()
Before doing this I am checking if an audio is already being played and want to pause that audio.
I tried doing this
if(app.audioBeingPlayed) {
$('audio #'+ app.audioBeingPlayed).pause()
}
before the code above.
When one item is clicked, it plays an audio. But when another item is clicked it gives an error saying
$(...).pause is not a function
I just want to pause the audio being played and play the new audio as created in the code in the first block.
Any help will be appreciated.
you need to change your code from
if(app.audioBeingPlayed) {
$('audio #'+ app.audioBeingPlayed).pause() // $('audio #'+ app.audioBeingPlayed) is not the audio object.
}
to this
....
....
app.audioBeingPlayed = e.target.id; // set the correct target id.
if(app.audioBeingPlayed) {
$('#'+ app.audioBeingPlayed).pause() // $('#'+ app.audioBeingPlayed) is the audio object so play/pause will work.
//or directly use the id to pause.
$('#'+ e.target.id).pause()
}

Play different audio by clicking on class element

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);
});

How to force a media to be loaded at run time, without playing it now?

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);

Google Chrome, audio, setTimeout, replace audio src, restarts mid-audio

This is only an issue in Chrome and runs fine in the other browsers.
I click a speaker which plays a welcome message, if the welcome message is not finished, I keep checking until it is finished, then I change the source to another message.
If I then click the speaker again to hear the new message, this message starts and after a few seconds stops and starts again, then stops (my estimate is that it stops when the duration of that message is passed)
In my html:
<audio id="shop_audio" preload="auto" type="audio/mpeg" src="/audios/helper/shop/en/welcome.mp3"></audio>
<p id="shop_speaker">
<i class="fa fa-volume-up"></i>
</p>
In my javascript:
Doc ready:
$("#shop_speaker").click(function(){
document.getElementById("shop_audio").play();
});
Then:
$(window).load(function(){
sl.utils.t_shopInstructions = setTimeout(sl.shop.audio.setAudio, 5000, 0);
});
And:
sl.shop.audio={
setAudio: function(which){
clearTimeout(sl.utils.t_shopInstructions);
var audio = document.getElementById("shop_audio");
if(audio.paused==true){
sl.shop.audio.newSrc(which);
}else{
sl.utils.t_shopAudioPlaying = setInterval(sl.shop.audio.waitForPaused, 500, which);
}
},
waitForPaused: function(which){
if(document.getElementById("shop_audio").paused==true){
clearInterval(sl.utils.t_shopAudioPlaying);
sl.shop.audio.newSrc(which);
}
},
newSrc: function(which){
document.getElementById("shop_audio").src = '/audios/helper/shop/en/help_instruction_'+which+'.mp3';
}
}
And a variant that also works everywhere, except chrome:
(I thought that setInterval was causing trouble, and/or its clearing)
sl.shop.audio={
setAudio: function(which){
clearTimeout(sl.utils.t_shopInstructions);
var audio = document.getElementById("shop_audio");
if(audio.paused==true){
sl.shop.audio.newSrc(which);
}else{
(function looping(){
clearTimeout(sl.utils.t_shopAudioPlaying);
if(audio.paused==false){
sl.utils.t_shopAudioPlaying=setTimeout(looping, 500);
}else{
sl.shop.audio.newSrc(which);
}
})()
}
},
newSrc: function(which){
document.getElementById("shop_audio").src = '/audios/helper/shop/en/help_instruction_'+which+'.mp3';
}
}
Placing the clearTimeout() in different places makes a difference, so I pursued that, but the result is not consistent.
I have now removed all the clearTimeouts - they make no difference whatsoever. The problem stays exactly the same.
Thanks for your suggestions!
I click a speaker which plays a welcome message, if the welcome
message is not finished, I keep checking until it is finished, then I
change the source to another message.
You can substitute ended event for setTimeout. Since you are using jQuery use .one() instead of .click().
var audio = $("#shop_audio")[0];
var speaker = $("#shop_speaker");
function resetAudio() {
audio.play();
audio.onended = function() {
this.onended = null;
this.src = "/path/to/a/different/src";
speaker.one("click", resetAudio);
}
}
speaker.one("click", resetAudio);

Categories