I had my little workout timer app all written out nice and neat, with little beeps for breaks and starts. But I heard nothing on my iPad. I find out mobile Safari doesn't support automatically played audio, it must be initialized by the user. Soon after, I see there are ways around the limitation. With other people providing more details. My thanks for their help and input which I rely on below.
I wanted it simple and "easy", so I boiled it all down.
The basic audio limitations of Mobile Safari:
(see more details in the links above)
- Can only start play from a user prompt like a "onclick" event .
- Only allows one audio file loaded at a time.
The basic workarounds are:
- Push Once: Make the user push a button just once.
- Never Stop: After that first click, use play, pause, and adjust playback time however many times you like. But never actually finish playing the whole audo file.
- Use Sprites: Use audio sprites, i.e. use one audio file with all the clips you want to play in one file and keep track of at what time each clip is located an how long it is.
Creating the Audio
Using Audacity or other audio editor, record/combine/edit/track your audio clips all together in one file such that:
- First 1 sec is silence.
- Keep track of where each clip starts and how long it is, leaving at least about 0.5sec of silence between clips.
- End with 1sec of silence.
Export the audio sprites file to a compatible format, e.g., .mp3 or .m4a (I used .m4a in the example).
The Code
The HTML includes assigning the audio source (here the file is named "12345.m4a")
[code language="html"]<audio id="audio"> <source src="12345.m4a" type="audio/mp4" /> </audio>
[/code]
The Javascript has the following commented setup near the top. The "spriteData" contains the names of the clips/sprites, their start time in sec, and their length. This particular audio sprite file is a sequence of counts from one to five, each starting at their respective second and 0.5sec long. The "currentSprite" will be used to keep track of the current sprite begin played or most recently played.
[code language="javascript"]// Audio Sprite Setup
// Get the sprite element
var audioSprite = document.getElementById('audio');
// Assign the sprite data (field names for id, start time of the clip in sec and length in sec)
// Place a silence at the beginning and end of the clip (about a second each) as a buffer to avoid undesired playback or termination due to inaccuracy in audio timing due to the browser
var spriteData = {
// Always have the first second silence and named "silence"
silence: {start: 0.0, length: 0.5},
one: {start: 1, length: 0.5},
two: {start: 2, length: 0.5},
three: {start: 3, length: 0.5},
four: {start: 4, length: 0.5},
five: {start: 5, length: 0.5}
};
// current sprite being played
var currentSprite = {};
// End Audio Sprite Setup
[/code]
Then somewhere below it has the remaining setup for playing the sprite, commented for clarity.
[code language="javascript"]// AUDIO SPRITE HANDLING
// time update handler to ensure we are in the current sprite and stop when it is complete
var onTimeUpdate = function() {
var timing_tolerance = 0.2; // .currentTime is not accurate, set this as the tolerance for its inaccuracy
// If at the end of the current sprite, then pause
if (this.currentTime >= currentSprite.start + currentSprite.length) {
this.pause();
}
// If playing but not in the current sprit, then go there (this can occure due to iOS timing issues and start playing from the start of the sprite file unintentionally)
else if (this.currentTime < currentSprite.start-timing_tolerance) {
audioSprite.currentTime = currentSprite.start;
}
};
// Set listener for timeupdate to have the currentTime be in the current sprite
audioSprite.addEventListener('timeupdate', onTimeUpdate, false);
// in mobile Safari, the first time this is called will load the audio. Ideally, we'd load the audio file completely before doing this.
// This function needs to be called first from a user touch event such as onmousedown, onmouseup, onclick, or ontouchstart.
var playSprite = function(id) {
if (spriteData[id] && spriteData[id].length) {
currentSprite = spriteData[id];
audioSprite.currentTime = currentSprite.start;
audioSprite.play();
}
};
var playSpriteInitialize = function(){
playSprite('silence');
// Whatever code you want first executed
//...
};
[/code]
That's all. As long as "playSprite" or "playSpriteInitialize" are called using a button first (or other compatible event), then the sprites can be played programmatically from that point on using
[code language="javascript"]playSprite["name_of_clip"][/code]
Note that the "currentTime" is not accurate to within about 0.2 or maybe even more seconds. So this solution may have some limits if vary rapid audio transitions are needed.
Example
Here is a little code snip wrapped with HTML/CSS to play a count from 1 to 5 using text and audio. The proof of concept is in that once you push the start button once, the program is able to use "setInterval" to automatically/programmatically play the count from the audio sprite file.
Here is the complete code for the example:
[code language="javascript"]
<!DOCTYPE html>
</html>
<audio id="audio"> <source src="12345.m4a" type="audio/mp4" /> </audio>
<button onclick="start_count();">Start Counting</button>
<button onclick="stop_count();">Stop Counting</button>
<div id="count_txt"> --- </div>
<script>
// Audio Sprite Setup
// Get the sprite element
var audioSprite = document.getElementById('audio');
// Assign the sprite data (field names for id, start time of the clip in sec and length in sec)
// Place a silence at the beginning and end of the clip (about a second each) as a buffer to avoid undesired playback or termination due to inaccuracy in audio timing due to the browser
var spriteData = {
// Always have the first second silence and named "silence"
silence: {start: 0.0, length: 0.5},
one: {start: 1, length: 0.5},
two: {start: 2, length: 0.5},
three: {start: 3, length: 0.5},
four: {start: 4, length: 0.5},
five: {start: 5, length: 0.5}
};
// current sprite being played
var currentSprite = {};
// End Audio Sprite Setup
// DEMO ------------------------------------------
var myTimer = {}; // Initialize variable used for timers
var counter = 0; // Initialize counter
var Ncounter = 5; // Up to how many counts
// Assign audio sprite according to the counter, and increment
function sound_count(){
var ids = ["one","two","three","four","five"]; // Array of the sprite names
counter = (counter%Ncounter); // make sure counter is in bounds
playSprite(ids[counter]); // play sprite
document.getElementById('count_txt').innerHTML = ids[counter]; // show counter
counter++; // increment counter
}
// Start a count on a timer
function start_count(){
clearInterval(myTimer); // Clear timer if in case it was already running
myTimer = setInterval(sound_count,1000); // Set timer to run the sound count every 1000ms
}
// End a count
function stop_count(){
clearInterval(myTimer);
}
// End DEMO ------------------------------------------
// AUDIO SPRITE HANDLING
// time update handler to ensure we are in the current sprite and stop when it is complete
var onTimeUpdate = function() {
var timing_tolerance = 0.2; // .currentTime is not accurate, set this as the tolerance for its inaccuracy
// If at the end of the current sprite, then pause
if (this.currentTime >= currentSprite.start + currentSprite.length) {
this.pause();
}
// If playing but not in the current sprit, then go there (this can occure due to iOS timing issues and start playing from the start of the sprite file unintentionally)
else if (this.currentTime < currentSprite.start-timing_tolerance) {
audioSprite.currentTime = currentSprite.start;
}
};
audioSprite.addEventListener('timeupdate', onTimeUpdate, false);
// in mobile Safari, the first time this is called will load the audio. Ideally, we'd load the audio file completely before doing this.
// This function needs to be called first from a user touch event such as onmousedown, onmouseup, onclick, or ontouchstart.
var playSprite = function(id) {
if (spriteData[id] && spriteData[id].length) {
currentSprite = spriteData[id];
audioSprite.currentTime = currentSprite.start;
audioSprite.play();
}
};
var playSpriteInitialize = function(){
playSprite('silence');
// Whatever code you want first executed
//...
};
// End Audio Sprite Setup
</script>
</html>
[/code]