swiftswiftuiaudiokit

Store slider value in view to global variable


I have several views nested within each other in my Swift program. In the lowest level view, I have a slider. I'd like to pass it's value from this view to the main Swift file where the global variable is stored within a struct. This value will be used to control the volume of the audio being played using AudioKit.

Code for slider (not included the full view here as the rest is irrelevant):

struct MasterFaderView: View {
    var body: some View {
        Slider(
            value: master.Value,
            in: 0...127,
            step: 1.0)
    }
}

Top Level global variable being set:

struct channel {
    var Value: Double = 0.0             // Default fader value
    ... // Other variables being stored
}

var master = channel()

@main
struct MEngProjectApp: App {
    
    var body: some Scene {
        WindowGroup {
            FreePlayView()
        }
    }
}

FreePlayView() is the main view which contains the audioKit stuff and will control the volume:

class MixerClass: ObservableObject {
    let engine = AudioEngine()
    var fullTrack = AudioPlayer()
    
    init() {
        engine.output = fullTrack
        loadFiles()
        try? engine.start()
    }
    
    func loadFiles() {
        do {
            if let fileURL = Bundle.main.url(forResource: "DQ_FullTrack", withExtension: "wav") {
                try fullTrack.load(url: fileURL)
            } else {
                Log("Could not find file")
            }
        } catch {
            Log("Could not load full track")
        }
    }
}

struct FreePlayView: View {
    @StateObject var conductor = MixerClass()
    @State var trackPlaying: Bool = false
    
    var body: some View {
        Button(action: {
        if trackPlaying == false {
            conductor.fullTrack.start()
            conductor.fullTrack.volume = master.Value
            trackPlaying = true
        } else {
            conductor.fullTrack.stop()
            trackPlaying = false
        }
   }) {
      Text("Start Audio")
   }

The error code I get within MasterFaderView is Cannot convert value of type 'Double' to expected argument type 'Binding<Double>'

Anyone any ideas how to fix this?

Thanks in advance.


Solution

  • @Wpitchy,

    In your code snippets, I'm not seeing where you're updating the fullTrack.volume via the Slider's value.

    I would also keep the @Published var fullTrackVolume variable in the Conductor, instead of in its own ChannelMode class, because it affects the fullTrack AudioPlayer that was instantiated there.

    Here's what I would change:

    MEngProjectApp.swift

    import SwiftUI
    
    @main
    struct MEngProjectApp: App {
    
        // If you want the MixerClass to be available in multiple views (not just FreePlayView), you will want to instantiate it as an EnvironmentObject in the App level.
        @StateObject var conductor = MixerClass()
    
        var body: some Scene {
            WindowGroup {
                FreePlayView()
                    .environmentObject(conductor) // <- Passing this object into this parent view will allow the MixerClass to be accessible from the FreePlayView and any of its child views.
            }
        }
    } 
    

    MixerClass.swift

    import AudioKit
    import AVFoundation
    import Foundation
    
    final class MixerClass: ObservableObject {
    
        let engine = AudioEngine()
    
        @Published var fullTrack = AudioPlayer() // <- Published vars broadcast any updates that occur, such as volume level changes.
        @Published var isTrackPlaying: Bool = false
    
        init() {
    
            // Importing AVFoundation is required in order to access the AVAudioSession settings.
            do {
                Settings.bufferLength = .medium
                try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(Settings.bufferLength.duration)
                try AVAudioSession.sharedInstance().setCategory(.playAndRecord,
                                                                options: [.defaultToSpeaker,
                                                                          .mixWithOthers,
                                                                          .allowBluetoothA2DP])
                try AVAudioSession.sharedInstance().setActive(true)
            } catch let err {
                print(err)
            }
    
            loadFiles()
            fullTrack.isLooping = true // <- Set this to true, if you don't want the audio clip to stop playing.
            engine.output = fullTrack
            try? engine.start()
        }
    
        func loadFiles() {
            do {
                if let fileURL = Bundle.main.url(forResource: "DQ_FullTrack", withExtension: "wav") {
                    try fullTrack.load(url: fileURL)
                } else {
                    Log("Could not find file")
                }
            } catch {
                Log("Could not load full track")
            }
        }
    
        func convertVolumeToPercent() -> String {
            "\(Int(fullTrack.volume * 100))%" // <- Converts 0.5 to 50% and 1.0 to 100%, etc.
        }
    
        // MARK: User Intent Actions from any or multiple views.
        // This way, the business logic "decision-making" from triggered events will remain outside of any of the views. These actions can be repurposed, and not tied to any view code.
    
        func playStopToggle() {
            isTrackPlaying ? fullTrack.stop() : fullTrack.start()
            isTrackPlaying.toggle()
        }
    }
    

    FreePlayView.swift

    import SwiftUI
    
    struct FreePlayView: View {
    
        @EnvironmentObject var conductor: MixerClass
    
        var body: some View {
            VStack {
                MasterFaderView()
                playStopButton
            }
        }
    
        private var playStopButton: some View {
            Button(action: {
                conductor.playStopToggle()
            }) {
                Text("\(Image(systemName: conductor.isTrackPlaying ? "stop.fill" : "play.fill")) \(conductor.isTrackPlaying ? "Stop" : "Start") Audio")
            }
        }
    }
    
    struct FreePlayView_Previews: PreviewProvider {
        static var previews: some View {
            FreePlayView()
                .environmentObject(MixerClass()) // <- Pass in the MixerClass in order for the SwiftUI Preview to work without crashing.
        }
    }
    

    MasterFaderView.swift

    import SwiftUI
    
    struct MasterFaderView: View {
    
        @EnvironmentObject var conductor: MixerClass
    
        var body: some View {
            VStack {
                Text(conductor.convertVolumeToPercent())
                    .font(.title).fontWeight(.thin)
                Slider(value: $conductor.fullTrack.volume, in: 0...1) // <- Volume in the AudioPlayer is 0.0-1.0, not 0-127.
                .padding()
            }
        }
    }
    
    struct MasterFaderView_Previews: PreviewProvider {
        static var previews: some View {
            MasterFaderView()
                .environmentObject(MixerClass()) // <- Pass in the MixerClass in order for the SwiftUI Preview to work without crashing.
        }
    }
    

    Please let me know if this helps.

    If you'd like to see this in a larger Xcode project, please check out the following:

    https://github.com/markjeschke/AKVolumeControl