When I asked whether the AVKit Foundation was suitable for seamless looping, I received the following answer from ChatGPT:
The AVKit framework is primarily intended for video playback and management. For seamless looping of audio files, the AVFoundation framework is more suitable. Specifically, the AVAudioPlayer or AVPlayer object from AVFoundation can be used for audio playback.
Here is a simple example using AVAudioPlayer in Swift to seamlessly loop an audio file:
import AVFoundation
class AudioLooper: NSObject, AVAudioPlayerDelegate {
var audioPlayer: AVAudioPlayer?
override init() {
super.init()
guard let audioFileURL = Bundle.main.url(forResource: "YourAudioFile", withExtension: "caf") else {
print("Audio file not found.")
return
}
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioFileURL)
audioPlayer?.delegate = self
audioPlayer?.numberOfLoops = -1 // -1 means infinite looping
audioPlayer?.prepareToPlay()
} catch {
print("Error initializing AVAudioPlayer: \(error.localizedDescription)")
}
}
func play() {
audioPlayer?.play()
}
func stop() {
audioPlayer?.stop()
}
// AVAudioPlayerDelegate method for seamless looping
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if flag {
player.play()
}
}
}
// Using the class
let audioLooper = AudioLooper()
audioLooper.play()
Replace "YourAudioFile" with the name of your audio file and adjust the file extension. This implementation uses AVAudioPlayer and sets numberOfLoops to -1 to enable infinite looping. The delegate is used to implement the audioPlayerDidFinishPlaying method, which triggers replay when playback finishes.
Keep in mind that this is a simple example and may not be suitable for all use cases. Depending on your application's requirements, you might also consider using AVPlayer and AVPlayerLooper for more advanced features.
And this is pretty similar to how I did it with Xcode & Swift.
Do you see an opportunity to consider this in the new year? Of course, the Player component should also be able to play in the background (as is the case with Android). In addition, the problem with seamless looping could also be (easily) solved with the AVFoundation framework.
It would be nice to hear from you about this before Christmas.
We may end up needing different logic in the companion versus not, but we will see. In particular, I would be concerned that Apple might decide that background audio support is not necessary in the companion and reject an update that includes that. If that ends up being the case, we will have to wire things up in some way that compiled apps can take the background audio capability.
Of course it doesn't matter how it's arranged with Companion. In any case it should work with the compiled app. And where is the problem in using the AVFoundation framework also for the Player component (and not just for the VideoPlayer component)?
Nothing precludes us from switching other than the time to update and test everything. The two components were implemented by two different individuals at different times and so different frameworks ended up being used.
Btw, the VideoPlayer also has some issues.
I tried to play a loopable sound (caf/IM4A format, duration 11 sec) with the VideoPlayer:
.GetDuration and .SeekTo_ms are in sec (not millis).
And unfortunately there is no loop setting (unlike the Player), because since the VideoPlayer uses AVFoundation, looping would most likely work with the VideoPlayer component.
However, as expected, the VideoPlayer does not play in the background (which usually makes sense, as long as you don't use it as an audio player).
I just tested it with a perfectly looped sound. It works perfectly, even in the background. Tested with Companion 2.64.6 and the compiled app (IPA). Thank you so much!
Another important note:
Seamless looping now also seems to work for compressed audio formats (accepted by Apple / iOS such as M4A, AAC, MP3, ...).
I have no idea since which iOS version this has been possible, as I have always had to use the IM4A (caf) format in Xcode/Swift. This is of course a big advantage because it makes app packages significantly smaller.
If I download a sound like this, I get this path with which the sound can be played without any problems. This path doesn't change with Companion when I repeat this process. However, with the compiled app (IPA) that doesn't seem to be the case. At least not when I reinstall the IPA (ad hoc).
How can I find out if the file already exists or gets a new path when the app is reinstalled. Unfortunately, the File component (File.Exists) doesn't work with iOS (as many other methods also don't work)?
/private/var/mobile/Containers/Data/Application/77E6D664-136A-4501-BDE4-97596C422A1E/tmp/Musik-1.m4a (The path is almost the same with the IPA.)
The file/storage system is even more mysterious on iOS than Android.
I usually package all the required (audio/media) files into the assets (i.e. the app package), but in this case I want to avoid it, otherwise I would have to re-sign the app (aia limit: 30 MB) via "codesign" / Xcode. So some files have to be downloaded when you first start the app.
Since you store the pathname in TinyDB, it seems like you would just need to check if that key is present or not to determine whether the download succeeded on future runs. We will work on making the File component maximally compatible with the Android version, but it seems like this problem can be solved without adding an implementation of File.Exists on the critical path.
The path stored in TinyDB is present, but this path no longer seems to work with the IPA after a reinstallation (ad hoc). I conclude that this file (i.e. the downloaded m4a file) no longer exists. The download process would have to be repeated in this case. And to be able to check this, I only see 2 options:
There is a way to check this via the File component (File.Exists) or
If the sound is not playing (if Player.IsNotPlaying, download...).
Interesting. Naturally, I expect that a reinstallation would result in a different UUID for the sandbox, so the stored file name in TinyDB wouldn't be valid. But TinyDB itself is a file stored in the sandbox so in theory a reinstallation should result in it being cleared (which would trigger a re-download). I will review the File component and see what resources we have to bring it more current with the Android functionality.