I’ve been using the agora SDK (audio only) for a while now, and I’m very pleased! Users can enter rooms of max 8 people and talk to each other. Now, I’m supposed to add a video feature, so they can enable their video stream at any time they want.
I’ve added the AgoraRtcEngine_iOS 3.7.0 pod and this is how I initialize the agora engine:
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: AppKeys.agoraAppId, delegate: self)
agoraKit?.setChannelProfile(.liveBroadcasting)
agoraKit?.setClientRole(.broadcaster)
agoraKit?.enableVideo()
agoraKit?.muteLocalVideoStream(true)
agoraKit?.muteLocalAudioStream(true)
agoraKit?.enableAudioVolumeIndication(1000, smooth: 3, report_vad: true)
This way, when a user joins a room, they have muted their audio and video streams, so they can enable it whenever they’re ready. The thing is, I’m using a UICollectionView to present the broadcasters. (everyone's user cell is visible at all times, so no reuse takes place, but collection view reloads happen constantly)
This is part of the cell setup (cellForItemAt), that handles the video view using a delegate:
private func setupVideoView(uid: UInt, isOfCurrentUser: Bool) {
videoView.frame.size = avatarBackgroundView.frame.size
videoView.layer.cornerRadius = avatarBackgroundView.layer.cornerRadius
if isOfCurrentUser {
delegate?.localVideoSetupWasRequested(videoView: videoView)
} else {
delegate?.remoteVideoSetupWasRequested(uid: uid, videoView: videoView)
}
}
And this is the conformance to the protocol:
extension RealTimeAudioAgoraService: NewVoiceRoomCellsDelegate {
func localVideoSetupWasRequested(videoView: UIView) {
guard !enabledVideoUIds.contains(currentUserRTCServiceId) else { return }
let videoCanvas = setUpVideoView(uid: currentUserRTCServiceId, videoView: videoView)
agoraKit?.setupLocalVideo(videoCanvas)
}
func remoteVideoSetupWasRequested(uid: UInt, videoView: UIView) {
guard !enabledVideoUIds.contains(uid) else { return }
let videoCanvas = setUpVideoView(uid: uid, videoView: videoView)
agoraKit?.setupRemoteVideo(videoCanvas)
}
private func setUpVideoView(uid: UInt, videoView: UIView) -> AgoraRtcVideoCanvas {
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = uid
videoCanvas.view = videoView
videoCanvas.renderMode = .hidden
// ------------------------------------
enabledVideoUIds.append(uid)
// ------------------------------------
return videoCanvas
}
}
As you can see, I keep track of all the uids I have enabled the video canvas for, so I do it only once for each user. The thing is something messes up the UI (see attached video) and even users with muted videos show the video canvas of others. It’s like the video canvas of each user is being cycled over every other user.
Any help will be much appreciated!
Related
In a SwiftUI app I want to do the following and I am wondering how to tackle the problem. I hope someone can give me a hint.
I have a list with about seven or eight rows and below the list is a button. When I tap the button the app plays audio files (one for each row). This is already working.
And here is what I want to implement:
When I tap the button, I want each row to change color one after the other while the matching audio file is playing.
The sound files are handled by a class like the one below with an audioPlayerDidFinishPlaying method to detect when a file play has ended.
class AudioEngine: NSObject,ObservableObject,AVAudioPlayerDelegate {
#Published var isPlaying:Bool = false {
willSet {
if newValue == true {
playAudio()
}
}
}
....
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
....
}
}
An instance of AudioEngine is created with:
let soundManager = AudioEngine()
........
And this shows the view with the list and the button:
VStack {
// Display the list of items:
ListItemsView(sentenceList: sntncLst, moc: moc)
// Display the button to play all the items:
Button(action: {
if !soundManager.isPlaying {self.playAllAudio(sntncLst)}
else {soundManager.stopAudio()}
})
{
Image(uiImage: UIImage(named: "speaker.png")!)
.padding(.bottom, 17)
}
}
....
func playAllAudio(_ sndList:[AudioList]) {
for sentence in sndList {
soundManager.filesArray.append(sentence.soundFile!)
}
soundManager.serialPlayAudio()
}
I'd like to be able to loop a live photo, for continuous playback.
So far, I'm trying to use the PHLivePhotoViewDelegate to accomplish this.
import Foundation
import SwiftUI
import PhotosUI
import iOSShared
struct LiveImageView: UIViewRepresentable {
let view: PHLivePhotoView
let model:LiveImageViewModel?
let delegate = LiveImageLargeMediaDelegate()
init(fileGroupUUID: UUID) {
let view = PHLivePhotoView()
// Without this, in landscape mode, I don't get proper scaling of the image.
view.contentMode = .scaleAspectFit
self.view = view
// Using this to replay live image repeatedly.
view.delegate = delegate
model = LiveImageViewModel(fileGroupUUID: fileGroupUUID)
guard let model = model else {
return
}
model.getLivePhoto(previewImage: nil) { livePhoto in
view.livePhoto = livePhoto
}
}
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let model = model else {
return
}
guard !model.started else {
return
}
model.started = true
view.startPlayback(with: .full)
}
}
class LiveImageLargeMediaDelegate: NSObject, PHLivePhotoViewDelegate {
func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
livePhotoView.stopPlayback()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
livePhotoView.startPlayback(with: .full)
}
}
}
But without full success. It seems the audio does play again, but not the video. The livePhotoView.stopPlayback and the async aspect are just additional changes I was trying. I've tried it without those too.
Note that I don't want the user to have to manually change the live photo (e.g., see NSPredicate to not include Loop and Bounce Live Photos).
Thoughts?
ChrisPrince I tried your code and it works fine for me, I just add delegate and start playback inside of it and everything runs well and smoothly. I thought that there is no point in using stop playback because the function itself says that the playback ended.
func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
livePhotoView.startPlayback(with: .full)
}
I am working with iOS 11 (for ARKit) and while many point to a sample app for SceneKit from Apple with a Fox, I am having problem with the extension it uses in that sample project (file) to add animations:
extension CAAnimation {
class func animationWithSceneNamed(_ name: String) -> CAAnimation? {
var animation: CAAnimation?
if let scene = SCNScene(named: name) {
scene.rootNode.enumerateChildNodes({ (child, stop) in
if child.animationKeys.count > 0 {
animation = child.animation(forKey: child.animationKeys.first!)
stop.initialize(to: true)
}
})
}
return animation
}
}
It seems that this extension is very handy but I am not sure how to migrate this now that it is deprecated? Is it built into SceneKit by default now?
The documentation didn't really show much info on why it was deprecated or where to go from here.
Thanks
TL;DR: examples of how to use new APIs can be found in Apple's sample game (search for SCNAnimationPlayer)
Even though animation(forKey:) and its sister methods that work with CAAnimation have been deprecated in iOS11, you can continue using them – everything will work.
But if you want to use new APIs and don't care about backwards compatibility (which you wouldn't need in the case of ARKit anyway, because it's only available since iOS11), read on.
The newly introduced SCNAnimationPlayer provides a more convenient API compared to its predecessors. It is now easier to work with animations in real time.
This video from WWDC2017 would be a good starting point to learn about it.
As a quick summary: SCNAnimationPlayer allows you to change animation's speed on the fly. It provides a more intuitive interface for animation playback using methods such as play() and stop() compared to adding and removing CAAnimations.
You also can blend two animations together which, for example, can be used to make smooth transitions between them.
You can find examples of how to use all of this in the Fox 2 game by Apple.
Here's the extension you've posted adapted to use SCNAnimationPlayer (which you can find in the Character class in the Fox 2 sample project):
extension SCNAnimationPlayer {
class func loadAnimation(fromSceneNamed sceneName: String) -> SCNAnimationPlayer {
let scene = SCNScene( named: sceneName )!
// find top level animation
var animationPlayer: SCNAnimationPlayer! = nil
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
return animationPlayer
}
}
You can use it as follows:
Load the animation and add it to the corresponding node
let jumpAnimation = SCNAnimationPlayer.loadAnimation(fromSceneNamed: "jump.scn")
jumpAnimation.stop() // stop it for now so that we can use it later when it's appropriate
model.addAnimationPlayer(jumpAnimation, forKey: "jump")
Use it!
model.animationPlayer(forKey: "jump")?.play()
Lësha Turkowski's answer without force unwraps.
extension SCNAnimationPlayer {
class func loadAnimationPlayer(from sceneName: String) -> SCNAnimationPlayer? {
var animationPlayer: SCNAnimationPlayer?
if let scene = SCNScene(named: sceneName) {
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
}
return animationPlayer
}
}
Here's an example of SwiftUI and SceneKit
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
#Binding var isPlayingAnimation: Bool
let scene = SCNScene(named: "art.scnassets/TestScene.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
controlAnimation(isAnimating: isPlayingAnimation, nodeName: "TestNode", animationName: "TestAnimationName")
}
func controlAnimation(isAnimating: Bool, nodeName: String, animationName: String) {
guard let node = scene.rootNode.childNode(withName: nodeName, recursively: true) else { return }
guard let animationPlayer: SCNAnimationPlayer = node.animationPlayer(forKey: animationName) else { return }
if isAnimating {
print("Play Animation")
animationPlayer.play()
} else {
print("Stop Animation")
animationPlayer.stop()
}
}
}
struct DogAnimation_Previews: PreviewProvider {
static var previews: some View {
ScenekitView(isPlayingAnimation: .constant(true))
}
}
A 2023 example.
I load typical animations like this:
func simpleLoadAnim(filename: String) -> SCNAnimationPlayer {
let s = SCNScene(named: filename)!
let n = s.rootNode.childNodes.filter({!$0.animationKeys.isEmpty}).first!
return n.animationPlayer(forKey: n.animationKeys.first!)!
}
So,
laugh = simpleLoadAnim(filename: "animeLaugh") // animeLaugh.dae
giggle = simpleLoadAnim(filename: "animeGiggle")
You then, step one, have to add them to the character:
sally.addAnimationPlayer(laugh, forKey: "laugh")
sally.addAnimationPlayer(giggle, forKey: "giggle")
very typically you would have only one going at a time. So set the weights, step two.
laugh.blendFactor = 1
giggle.blendFactor = 0
to play or stop an SCNAnimationPlayer it's just step three
laugh.play()
giggle.stop()
Almost certainly, you will have (100s of lines) of your own code to blend between animations (which might take only a short time, 0.1 secs, or may take a second or so). To do so, you would use SCNAction.customAction.
If you prefer you can access the animation, on, the character (sally) using the keys. But really the "whole point" is you can just start, stop, etc etc the SCNAnimationPlayer.
You will also have lots of code to set up the SCNAnimationPlayer how you like (speed, looping, mirrored, etc etc etc etc etc etc etc etc)
You will need THIS very critical answer to get collada files (must be separate character / anim files) working properly https://stackoverflow.com/a/75093081/294884
Once you have the various SCNAnimationPlayer animations working properly, it is quite easy to use, run, blend etc animes.
The essential sequence is
each anime must be in its own .dae file
load each anime files in to a SCNAnimationPlayer
"add" all the animes to the character in question
program the blends
then simply play() or stop() the actual SCNAnimationPlayer items (don't bother using the keys on the character, it's a bit pointless)
In a swift game using UIKit I am writing, a human player will interact with UIKit UIButtons, GUI elements to take actions.
In the game, the player will play against AI players.
But here's the thing; the human player presses buttons and interacts and the AI player does not.
Given a simple UIViewController;
class SampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buyBtnPressed(_ sender: UIButton) {
print ("pressed")
}
}
So what I'm trying to ascertain is, how does the AI player itself take actions and handling turns within the context of the current view controller?
I believe the best way to do this is that there should be a loop that will wait until all players have completed their respective turns.
But where does this loop go? In the view did load?
If so, won't it eat up memory, or potentially lead (if not careful) to an endless loop?
I'm finding it hard to ascertain how an AI player can take actions within the given context of a UIViewController considering GUI elements are for human interaction.
I don't mean the AI should be animating pressing buttons or interacting with the screen, I mean; I have a UIViewController, it has a view did load; what is the strategy of implementing AI turns and whether or not this should be be achieved in a "game loop" in the View did load or can this be achieved in another way?
My question is; given the context of a UIViewController; how can I code the handling of an AI player taking turns and can this be achieved with a loop or another strategy?
Many thanks
edit: Code is now added
I have written out a turn base manager using Swift playgrounds, and 2 examples one using a UIViewController and another is a loop.
code now follows;
import Foundation
import GameplayKit
class Player {
var name: String
public private(set) var isAI: Bool = false
public private(set) var turnOrder: Int = 0
init(name: String, isAI: Bool?) {
self.name = name
if let hasAI = isAI {
self.isAI = hasAI
}
}
func setTurnOrderIndex(number: Int) {
self.turnOrder = number
}
}
let p1 = Player.init(name: "Bob", isAI: false)
let p2 = Player.init(name: "Alex", isAI: true)
protocol TurnOrderManagerDelegate: NSObjectProtocol {
func turnOrderWasSet()
}
protocol TurnDelegate: class {
func turnIsCompleted()
}
class Turn: NSObject {
weak var player: Player?
weak var delegate: TurnDelegate?
public private(set) var completed: Bool = false {
didSet {
delegate?.turnIsCompleted()
}
}
init(player:Player, delegate: TurnDelegate) {
self.player = player
self.delegate = delegate
}
func setAsComplete() {
self.completed = true
}
}
class TurnOrderManager: NSObject, TurnOrderManagerDelegate, TurnDelegate {
static var instance = TurnOrderManager()
public private(set) var turnOrderIndex: Int = 0
public private(set) var turnOrder: [Turn] = [Turn]() {
didSet {
self.turnOrderWasSet()
}
}
var playerOnTurn: Player? {
let turnObj = self.turnOrder[turnOrderIndex]
return (turnObj.player)
}
var allTurnsCompleted: Bool {
let filtered = turnOrder.filter { (turnObj:Turn) -> Bool in
return (turnObj.completed)
}.count
return (filtered == turnOrder.count)
}
func setTurnOrder(players:[Player]) {
if (self.turnOrder.count == 0) {
for playerObj in players {
let turnObj = Turn.init(player: playerObj, delegate: self)
self.turnOrder.append(turnObj)
}
}
}
func turnOrderWasSet() {
for (index, turnObj) in self.turnOrder.enumerated() {
turnObj.player?.setTurnOrderIndex(number: index)
}
}
func next() {
if (turnOrderIndex < (self.turnOrder.count - 1)) {
turnOrderIndex += 1
}
else {
turnOrderIndex = 0
}
}
internal func turnIsCompleted() {
print (" - turnIsCompleted")
TurnOrderManager.instance.next()
}
}
class GameModel {
var turnOrderManager: TurnOrderManager
init() {
self.turnOrderManager = TurnOrderManager.instance
self.turnOrderManager.setTurnOrder(players:[p1,p2])
}
// other game model stuff [...]
}
class Phase1State : GKState {
var gameModel: GameModel!
init(gameModel:GameModel) {
super.init()
self.gameModel = gameModel
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool
{
return false
}
override func didEnter(from previousState: GKState?) {
}
override func willExit(to nextState: GKState) {
}
// MARK: - Action
func buy() {
let index = self.gameModel.turnOrderManager.turnOrderIndex
let turn = self.gameModel.turnOrderManager.turnOrder[index]
turn.setAsComplete()
}
}
class SomeViewController: UIViewController
{
var gameModel: GameModel?
weak var gamePhase: Phase1State?
var isPhaseComplete: Bool {
return self.gameModel?.turnOrderManager.allTurnsCompleted ?? false
}
override func viewDidLoad() {
super.viewDidLoad()
self.gameModel = GameModel.init()
self.gamePhase = Phase1State.init(gameModel: self.gameModel!)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func buyButtonPressed() {
self.gamePhase?.buy()
self.finishTurn()
}
func finishTurn() {
guard let turnIndex = self.gameModel?.turnOrderManager.turnOrderIndex else {
return
}
guard let turn = self.gameModel?.turnOrderManager.turnOrder[turnIndex] else {
return
}
turn.setAsComplete()
if (self.isPhaseComplete)
{
print ("All turns are completed")
}
else {
//self.gameModel?.turnOrderManager.next()
self.gamePhase?.buy()
guard let playerOnTurn = self.gameModel?.turnOrderManager.playerOnTurn else {
print ("No player is on turn")
return
}
print ("\(playerOnTurn.name) is on turn")
if (playerOnTurn.isAI)
{
self.gamePhase?.buy()
self.finishTurn()
}
}
}
}
// EXAMPLE 1 -- first attempt ...
let vc = SomeViewController()
vc.viewDidLoad()
vc.buyButtonPressed()
// EXAMPLE 2 -- another attempt ....
let gameModel: GameModel = GameModel.init()
let gamePhase = Phase1State.init(gameModel: gameModel)
// player then takes an action
while (gameModel.turnOrderManager.allTurnsCompleted == false)
{
let turnIndex = gameModel.turnOrderManager.turnOrderIndex
let turnObj = gameModel.turnOrderManager.turnOrder[turnIndex]
guard let playerOnTurn = turnObj.player else {
break
}
print ("Player \(playerOnTurn.name) is on turn")
gamePhase.buy()
}
print ("All turns are completed, advance to next phase")
The issue is;
On the finishTurn, it only seems to work if it relies on the first player in the index is a human player. If its not, I have no idea how to make it fire the buy action.
On the second example, I use a loop; but I'm concerned using a loop could end up just looping forever.
My query is therefore clarifyed, how can I ensure my view controller will fire actions for AI players when they don't press buttons and loop through each player and execute their respective turn.
Many thanks
Further edit:
I do not know if I should have the while (gameModel.turnOrderManager.allTurnsCompleted == false) loop inside my viewDidLoad() to act like a game loop.
There is no need to specifically use Sprite Kit for this. SpriteKit would be more to do with how the UI is made rather than how the logic of the game works.
However, I would recommend looking at GameplayKit. It's a framework that contains lots of built in game logic tools. Specifically you want something like the GKDecisionTree. There are a few WWDC videos about it too. GameplayKit can be used with SpriteKit, UIKit, SSceneKit or any other game engine that you decide to use (or not).
Also, the question you're asking is a very general question about game development. Having the computer "decide" to do something is quite a complex subject.
I'd also suggest having a quick watch of this video from AI & Games and other videos from that channel.
It'll give you an idea of how to approach your problem.
Session 609 and 608 from WWDC 2015 and 2016 are prob good :D
Regarding updating the AI.
Your AI should be event driven. You have the concept of "turns" and "players". There is a point in the game at which it becomes a "player's" "turn". (Even at the very beginning of the game it is either Player 1 or Player 2's turn.
At this time there are two possibilities. Either the player is an AI, or the player is a person.
As soon as this happens there should be some sort of trigger (like a function call or something) that tells the player its turn has started.
If that player is the AI then you need to start some sort of calculation (maybe with a built in delay to make it realistic) so that it decides what to do.
Look, I'm not sure on what kind of game you're making, buy you should probably learn SpriteKit, specially SKActions. With that, you can easily control the flow of events from your game.
With that said, how is your AI implementation? Based on your code, I would begin with something like this:
class AI {
enum Decision {
case doSomething
case doAnotherThing
case dontDoAnything
}
public func decide() -> Decision {
// Decide which action the AI will take...
return .doSomething // This return is just a example!
}
public func act(on : Decision) {
// Do whatever the AI needs based on a decision...
}
}
Then, in your ViewController:
class SampleViewController: UIViewController {
var ai = AI()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buyBtnPressed(_ sender: UIButton) {
print ("pressed")
ai.act(on: ai.decide())
}
}
I hope that helps!
I am using a standard Master-Detail project, listing songs in the Master and playing them in Detail. Each song has up to four parts playing simultaneously, with independent volume control, so I have four AVAudioPlayer objects in Detail, each with a slider with an IBOutlet and an IBAction to implement the volume control.
The problem is that when you click on a song (in the list on the Master), the previous song doesn't stop. Both songs play, although the volume controls now only control the most recent song. This can go on for any number of songs.
I want to get rid of the currently playing song when a new song is clicked on.
I thought I might be able to accomplish this by creating the players inside a Singleton, in such a way that there would only ever be four players. Since, according to the documentation, each player can only play one sound file at a time, I hoped the previous sound file would stop playing when the new one started. But it's not working. The same behavior described above is still happening: multiple songs can play simultaneously, with the volume controls only controlling the most recent song. Any suggestions would be greatly appreciated.
Here is the code for the Singleton:
import Foundation
import AVFoundation
class FourPlayers {
static let audioPlayers = [one, two, three, four]
static let one = AVAudioPlayer()
static let two = AVAudioPlayer()
static let three = AVAudioPlayer()
static let four = AVAudioPlayer()
private init() {} //This prevents others from using the default '()' initializer for this class.
}
(Initially, I had just made audioPlayers static, but when that didn't work, I decided to make each individual player static as well.)
Then, in the DetailViewController:
var audioPlayers = FourPlayers.audioPlayers
Here's code for one of the four volume controls:
#IBOutlet weak var vol1: UISlider!
#IBAction func volAdjust1(sender: AnyObject) {
audioPlayers[0].volume = vol1.value
}
Playing a song looks like this (the audioFiles array is populated when the song info is passed in from the Master):
var audioFiles = []
func playAudioFiles() {
var i = 0
for _ in audioFiles {
audioPlayers[i].play()
i+=1
}
}
This is the code telling the players which file to play:
func prepareAudioFiles () {
var i = 0;
for audioFile in audioFiles {
let s = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(audioFile as? String, ofType: "mp3")!)
do {
audioPlayers[i] = try AVAudioPlayer(contentsOfURL:s)
} catch {
print("Error getting the audio file")
}
audioPlayers[i].prepareToPlay()
self.audioPlayers[i].delegate = self
}
}
It appears the the volume control is only adjusting the first player (index 0)
#IBAction func volAdjust1(sender: AnyObject) {
audioPlayers[0].volume = vol1.value
}
maybe you could add a variable to keep track of the currently playing index
var currentlyPlayingIndex = 0
#IBAction func volAdjust1(sender: AnyObject) {
audioPlayers[currentlyPlayingIndex].volume = vol1.value
}
And update it when needed
When you play the audio you could use the same variable
func playAudioFiles() {
var i = 0
for _ in audioFiles {
if i == currentlyPlayingIndex {
audioPlayers[i].play()
} else {
audioPlayers[i].stop()
}
i+=1
}
}
The problem is the following line:
var audioPlayers = FourPlayers.audioPlayers
You are actually creating a new copy of the FourPlayers.audioPlayers array into the audioPlayers variable. So you are not reusing the AVAudioPlayers but creating independent ones for each song.
See the "Assignment and Copy Behavior for Strings, Arrays, and Dictionaries" section in the Swift Programming Documentation, Classes and Structures
You could use the static variable directly in your code instead of making a copy, for example:
#IBOutlet weak var vol1: UISlider!
#IBAction func volAdjust1(sender: AnyObject) {
FourPlayers.audioPlayers[0].volume = vol1.value
}
and:
var audioFiles = []
func playAudioFiles() {
var i = 0
for _ in audioFiles {
FourPlayers.audioPlayers[i].play()
i+=1
}
}
and update the rest of your code accordingly.