Tone.js icon indicating copy to clipboard operation
Tone.js copied to clipboard

Tone.js not playing audio on iOS

Open groudonzora opened this issue 2 years ago • 18 comments

I tried StackOverflow, but couldn’t get any responses. So I thought I’d try here.

I have a meteor application that uses Tone.js, and I use the GrainPlayer object specifically. It works perfectly fine on different browsers across different OS and devices… except for iOS. I personally don’t have an iOS device, but clients are showing me that iOS isn’t playing the audio. It’s also not restricted to safari; other browsers on their device also won’t play the audio.

I know it’s a Tone.js-specific issue here, because I also use Howler.js for a few of the sounds that don’t require the Tone GrainPlayer class, and those work. It’s also not an AudioContext issue in general, because I have the following code:

Tone.setContext(Howler.ctx);

Which ensures the AudioContext for Tone is routed through the AudioContext for Howler.

Is there a way to get it to play? I need to keep using GrainPlayer because of its unique properties and functions, so switching out will not work for me as a solution unfortunately. I have no way of diagnosing it from a developer standpoint because again I don’t have an iOS device, and even then they don’t have a way to pull up a developer console… But it seems to not break code or anything, just seems like it ‘ignores’ the sound playing, and moves on.

The sounds in question are created as such:

standardChord[0] = new Tone.GrainPlayer(’/A.flac’).connect(Howler.masterGain);

And then when they need to be played:

standardChord[0].start();

Which like I said works fine on other devices.

More information: It's a Meteor application, bundled and deployed as a Node.js The sounds do not start automatically; they start after a button press, and while it does use meteor's setInterval to play them, the Howler.js sounds do too, so that shouldn't be the issue either.

Willing to add code and/or additional details, if needed, not sure exactly what info might be useful (still new to Tone.js!)

groudonzora avatar Mar 12 '22 21:03 groudonzora

People will be able to help you debug this (I could and I will if you want to) but if you do professional client work, you need access to an iOS device because you will have these kinds of errors over and over again.

dirkk0 avatar Mar 13 '22 07:03 dirkk0

I absolutely agree with @dirkk0. However there is one little detail which caught my attention in the few lines of code that you shared. It looks like you're trying to play a FLAC file. I doubt that this can be decoded on mobile Safari. I would be happy to hear that it works, though. You might want to try a different file format.

chrisguttandin avatar Mar 13 '22 13:03 chrisguttandin

People will be able to help you debug this (I could and I will if you want to) but if you do professional client work, you need access to an iOS device because you will have these kinds of errors over and over again.

Yeah, when I do more than just small-scale professional stuff I'm definitely gonna upgrade my tech reach haha. I do appreciate that insight. For now I just have a client I'm in touch with who has a fairly recent iPad, and I just have him check things for me.

I absolutely agree with @dirkk0. However there is one little detail which caught my attention in the few lines of code that you shared. It looks like you're trying to play a FLAC file. I doubt that this can be decoded on mobile Safari. I would be happy to hear that it works, though. You might want to try a different file format.

Ahh! That might be the solution! Let me check

groudonzora avatar Mar 13 '22 18:03 groudonzora

Update: Seems the mp3 issue wasn't the case, so I'm still stumped.

is GrainPlayer simply incompatible with iOS?

groudonzora avatar Mar 19 '22 21:03 groudonzora

Check my problem here https://github.com/Tonejs/Tone.js/issues/1056 I've found a solution

jramassamy avatar Mar 19 '22 22:03 jramassamy

Check my problem here #1056 I've found a solution Might I ask, why does this work? Why can't I get the users to do something related to unmuting instead of a using a hack? And why is it not a problem on other websites/apps?

groudonzora avatar Mar 20 '22 18:03 groudonzora

I've really no clear answers for you, I just did Empirical Testing.

https://github.com/swevans/unmute/blob/master/README.md here he described why he did what he did ^^

jramassamy avatar Mar 21 '22 15:03 jramassamy

Hey sorry it took so long to get back. Still having an issue

I absolutely agree with @dirkk0. However there is one little detail which caught my attention in the few lines of code that you shared. It looks like you're trying to play a FLAC file. I doubt that this can be decoded on mobile Safari. I would be happy to hear that it works, though. You might want to try a different file format.

You are correct, no Mac or iPad or any apple supports .flac, so I converted it to .mp3 Still not working. Howler.js (also using audiocontext) works but not Tone.js Problem is, I need the tempo change without pitch change, and only Tone does that. There is the possibility of switching to regular HTML5 audio, which can do tempo change without pitch change, but the issue I found with that was that the files would lag before playing on their first playthrough. Perhaps I only do it for iOS and hope that it isn't an issue there?

groudonzora avatar Apr 27 '22 00:04 groudonzora

As an alternative to GrainPlayer you can use Tone.PitchShift in combination with the playbackRate property of a Tone.Player to do a tempo shift/timestretch. It won't sound nearly as perfect as the tempo shift from GrainPlayer but it works in a crunch and I'm pretty sure it's a more resource-lightweight approach. You'd need to convert the desired speed percentage change to pitch semitones and then use PitchShift to compensate and undo the pitch change from playbackRate. So for example if you wanted to increase the speed to 120% of the base speed, you would set player.playbackRate = 1.2, which increases the pitch by about 3.15641 semitones, and then connect it to a PitchShift node with pitchShift.pitch = -3.15641. The basic formula for playbackRate to semitone difference is const playbackRateToSemitones = (playbackRate) => Math.log(playbackRate/1)/0.05776227 You'll probably want to round round the result to defeat JS floating point math quirks, and you'd need to change this result to the opposite sign to arrive at the value you'll want to set pitchShift.pitch.

FWIW, the grainPlayer example page doesn't work on my Android phone.

marcelblum avatar Apr 27 '22 03:04 marcelblum

If you are willing to create a minimal CodePen that showcases the problem on iOS, then I would look at it with an iOS device and see if I can help in solving this. The idea would be to be minimal, so no React etc, just vanilla JS.

dirkk0 avatar Apr 27 '22 05:04 dirkk0

@marcelblum

Odd, because my application using GrainPlayer works on my android device.

Also I believe I tried the PitchShift method, and it unfortunately didn't work so well, it was kind of distorted. I may have to resort to it though.

Is there any reason why it lags the first playthrough of each sound when using HTML5? If I can get that to work then that'd bypass all of this issue. Unfortunately, I'm not a sound engineer

groudonzora avatar Apr 27 '22 20:04 groudonzora

Yeah PitchShift introduces distortion for sure (depending on how far you need to shift) but it's low latency and like I said works in a pinch if you need an alternative. You can play with PitchShift.windowSize to try and get it sounding better. Another alternative (non-Tone) is https://github.com/cutterbl/SoundTouchJS

HTML5 audio is not intended for low latency use. It doesn't decode the entire file to memory like web audio does so it's not suitable for low latency realtime processing. But if you don't need super low latency it could work fine for your situation. Might help to experiment with the preload property and to use an uncompressed wav instead of mp3 if the only problem is initial delay on first play.

marcelblum avatar Apr 27 '22 20:04 marcelblum

Interesting.

Forgive me for my lack of understanding, but why would an uncompressed WAV file be better than mp3 for initial delay?

(I used .flac previously and it still had the problem)

groudonzora avatar Apr 28 '22 22:04 groudonzora

I don't know for sure if using uncompressed wav will alleviate your issue, but it's worth experimenting with, see this for more info. Also uncompressed wav does not incur the added latency of decompressing that mp3 or flac would, though again you'd have to test this theory to determine where the delay is coming from and what threshold of latency is acceptable for your situation. In theory a preloaded uncompressed wav should start faster than any compressed file that hasn't already been decoded to RAM - but it's also possible the difference may be negligible on modern hardware, you need to do some testing to determine the source of the delay.

marcelblum avatar Apr 28 '22 23:04 marcelblum

I've stumbled on the same issue, and tried to use the unmute library (mentioned in https://github.com/Tonejs/Tone.js/issues/1056), but that unfortunately did not work. Even by following the instructions on https://github.com/swevans/unmute#usage and giving tonejs audioContext instead to the unmute call ( using Transport.context.rawContext as any)._nativeContext as AudioContext, which may very well be incorrect ).

I've also tried with .wav and .mp3, but the issue remained.

What worked for me instead is creating an

Also, the tonejs examples don't seem to work on iOS (I've tried the player example on firefox/chrome/safari), so would it be possible to consider this a bug?

frading avatar Jun 14 '22 16:06 frading

Any progress on this? Cheers

PippoApps avatar Aug 23 '22 12:08 PippoApps

Hi There... we have done some testing on this and eventually resorted to the following because we couldn't auto start the audio on iOS devices ... even when Tone and Context are resumed after user interactions, as detailed in various other responses. Observations and workarounds (...on iOS 15.6.1):

  1. A user interaction followed by any sound played on a native html audio tag activates sound, but starting Tone and resuming context does not.
  2. if a tab is left for any length of time with inactivity, the sound is disabled and has to be re-enabled again via the same user interaction sequence as mentioned in step 1.
  3. We tried various scenarios, but couldn't find an automated and elegant solution unfortunately. What we did was to detect an iOS device and make a note of it on the client-side. When a user interacts, we play a short sound on a native audio player and then start Tone activities and that seems to work.
  4. Unfortunately, this does not cater for the scenario where a tab becomes 'silent' after periods of inactivity. We therefore provide some info re: how to activate and re-activate iOS audio using a manual click of a native audio player.

If you want to try this out on an iOS device, point to dappledark.com/composer and you should observe the mechanism explained above, together with the audio activation messaging sequence. If anyone has a more automated and elegant solution for iOS please let us know, but I hope this is useful. This audio issue is only isolated to iOS devices... Safari, Android and all other modern browsers work fine.

abswiz avatar Aug 31 '22 15:08 abswiz

Using also unmute.js I rely on


document.querySelector('#startaudio')?.addEventListener('click', async () => {
  // Create an audio context instance if WebAudio is supported
  let context = (window.AudioContext || window.webkitAudioContext) ?
    new (window.AudioContext || window.webkitAudioContext)() : null;

  // Decide on some parameters
  let allowBackgroundPlayback = false; // default false, recommended false
  let forceIOSBehavior = false; // default false, recommended false
  // Pass it to unmute if the context exists... ie WebAudio is supported
  if (context)
  {
    // If you need to be able to disable unmute at a later time, you can use the returned handle's dispose() method
    // if you don't need to do that (most folks won't) then you can simply ignore the return value
    let unmuteHandle = unmute(context, allowBackgroundPlayback, forceIOSBehavior);

  }

  await Tone.start()
})

to be able to get going on iOS, tested on Safari and Firefox.

Utopiah avatar Mar 12 '23 14:03 Utopiah

I had the exact same problem, Tone.js doesn't play any sound on the newer iPhones and iPads when the device is muted. My solution was to play a silent mp3 on user interaction, using a native HTML audio element to activate the sound. I do this before playing the sound that I actually want to hear.

    <audio>
    	<source src="/silent.mp3" type="audio/mp3"></source>
    </audio>

You can get the audio element to start playing on user interaction. So select the audio element, and call it's play() method right before starting the audio you actually want to hear.

audioElement.play(); I tested on the following devices on LambdaTest (highly recommended if you don't own an iOS device):

iPhone 8 13.4 iPhone 12 17.2 iPhone 13 Mini 17.2 iPhone 14 17.2 iPhone 15 17.2

MichaelShingo avatar Mar 22 '24 11:03 MichaelShingo