How to use Audiokit to call a function based on frequency - function

I've seen that it's possible to use the frequency tracker in a function, but I can't pull it off myself. There's an example file that can display frequency, but I haven't found anything that calls a function when the tracker hits a certain frequency.
I'm trying to call the AKAudioPlayer.pause() function when the Frequency drops below 40hz. I edited a test .mp3 file so that there is a section of 32hz in the audio file. I can even see on the frequency tracker text, that the tracker is reading the 32 hz, but I don't have any success in getting it to pause, even when I write the function to be dependent on this text.
This is a modified version of the microphone tracker, for tracking an audio file.
I receive the error: "Unexpectedly found nil while unwrapping an Optional value" both when trying to form the function directly with the tracker.frequency value, and when using a variable, converted to a string based off of the tracker.frequency.
This leads me to believe that the tracker frequency is my problematic nil optional value and to the conclusion that, that's what brings the whole thing down in flames. But I can't figure out what to do about it. I know there is a function that works to display the frequency, but I can't recreate its success.
In the below code (mostly an Audiokit example file) I've tried to use the Tracker.Frequency to trigger the pause function:
import AudioKit
import AudioKitUI
import UIKit
class ViewController: UIViewController {
#IBOutlet private var frequencyLabel: UILabel!
#IBOutlet private var amplitudeLabel: UILabel!
#IBOutlet private var noteNameWithSharpsLabel: UILabel!
#IBOutlet private var noteNameWithFlatsLabel: UILabel!
#IBOutlet private var audioInputPlot: EZAudioPlot!
#IBAction func Play(_ sender: Any) {
if input.isStarted == false
{
input.play()
}
}
#IBAction func Pause(_ sender: Any)
{
if input.isStarted
{
input.pause()
}
}
var input: AKAudioPlayer!
var song = try! AKAudioFile(readFileName: "TEST.mp3")// ... the error is here
var tracker: AKFrequencyTracker!
var silence: AKBooster!
let noteFrequencies = [16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87]
let noteNamesWithSharps = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]
let noteNamesWithFlats = ["C", "D♭", "D", "E♭", "E", "F", "G♭", "G", "A♭", "A", "B♭", "B"]
func setupPlot() {
let plot = AKNodeOutputPlot(input, frame: audioInputPlot.bounds)
plot.plotType = .rolling
plot.shouldFill = true
plot.shouldMirror = true
plot.color = UIColor.blue
audioInputPlot.addSubview(plot)
}
override func viewDidLoad() {
super.viewDidLoad()
pauseOnQueue()
AKSettings.audioInputEnabled = true
do {input = try AKAudioPlayer(file: song)}
catch {print("ERROR")}
tracker = AKFrequencyTracker(input)
silence = AKBooster(tracker, gain: 1)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
AudioKit.output = silence
AudioKit.start()
setupPlot()
Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(ViewController.updateUI),
userInfo: nil,
repeats: true)
}
#objc func updateUI() {
if tracker.amplitude > 0.1 {
frequencyLabel.text = String(format: "%0.1f", tracker.frequency)
var frequency = Float(tracker.frequency)
while frequency > Float(noteFrequencies[noteFrequencies.count - 1]) {
frequency /= 2.0
}
while frequency < Float(noteFrequencies[0]) {
frequency *= 2.0
}
var minDistance: Float = 10_000.0
var index = 0
for i in 0..<noteFrequencies.count {
let distance = fabsf(Float(noteFrequencies[i]) - frequency)
if distance < minDistance {
index = i
minDistance = distance
}
}
let octave = Int(log2f(Float(tracker.frequency) / frequency))
noteNameWithSharpsLabel.text = "\(noteNamesWithSharps[index])\(octave)"
noteNameWithFlatsLabel.text = "\(noteNamesWithFlats[index])\(octave)"
}
amplitudeLabel.text = String(format: "%0.2f", tracker.amplitude)
}
func pauseOnQueue() {
frequencyLabel.text = String(format: "%0.1f", tracker.frequency)
let frequency = Float(tracker.frequency)
if frequency < 50 && frequency > 20 && input.isStarted == true
{ input.pause() }
}
}

I've answered a similar question here: AudioKit (iOS) - Add observer for frequency / amplitude change
There's a decision that you have to make about how you want to detect this frequency - is it something that happens fast and needs to be in the DSP code or can you just poll for frequency at semi-regular intervals from Swift.
I don't think you should add !s to the AudioKit node definitions because they might not exist at the time you're expecting them to even though it seems like it should from the view controller life cycle. In general, don't rely on view controller code to controller your audio. Perhaps put all the audio related stuff in a singleton and let it manage itself?

Related

Swift detect json parsing detect end

I am parsing a certain json url data to plot in a map and I need to detect that I have all the data to show a spinner while nothing is happening. I have created a variable that goes from false to true after I have all the data but that variable only exists as true inside the for loop
This is part of the code that gets the data
import SwiftUI
import MapKit
var locationsFillTest : Int = 0
var allLocations = [MKPointAnnotation]()
var doneGettingData : Bool = false
struct MapView: UIViewRepresentable {
var startdate : String
func makeUIView(context: Context) -> MKMapView{
MKMapView(frame: .zero)
}
func makeCoordinator() -> MapViewCoordinator{
MapViewCoordinator(self)
}
func updateUIView(_ uiView: MKMapView, context: Context){
uiView.removeAnnotations(allLocations)
allLocations = []
doneGettingData = false
print("Done = \(doneGettingData)")
let url = URL(string: "https://XXXXXX")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode(emsc.self, from: d)
DispatchQueue.main.async {
locationsFillTest = allLocations.count
doneGettingData = false
for locations in decodedLists.features {
let lat = Double(locations.properties.lat)
let long = Double(locations.properties.lon)
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: lat , longitude: long )
if locationsFillTest == 0 {
allLocations.append(annotation)}
}
uiView.addAnnotations(allLocations)
uiView.delegate = context.coordinator
uiView.showAnnotations(allLocations, animated: true)
doneGettingData = true
print("Done = \(doneGettingData)")
}
}else {
print("No Data")
}
} catch {
print("Error decoding JSON: ", error, response!)
}
}.resume()
}
}
The variable doneGettingData becomes false and true by watching the print but if I need to use it for example to create a spinner its false all the time since its only true inside.
How can I make it global ?
Thank you
Unless you have another declaration for doneGettingData inside the closure the instance level property is getting set to true. It may be getting set later than you expect though. Try the following to see when it changes (and to get you setup to react to those changes):
var doneGettingData : Bool = false {
didSet {
if doneGettingData {
print("The instance property doneGettingData is now true.")
} else {
print("The instance property doneGettingData is now false.")
}
}
}
You may want to make this into a custom enum though with cases along the lines of fetching, done, noData, and jsonError. Right now if there is no data you will never have a trigger to either retry, move on, notify the user, etc. The same applies when there is a decoding error. Or at the very least set the flag to true at the very end of the loop so something happens no matter what.
Something like:
enum DataCollectionState {
case fetching, done, noData, jsonError
var doneGettingData : DataCollectionState = fetching {
didSet {
switch doneGettingData {
case fetching:
// Show a spinner or something
case done:
// Hide the spinner
case noData:
// Tell the user there was no data? Try again?
case jsonError:
// Tell the user there was an error? Try again?
}
}
}
Note: I don't have Xcode open right now so syntax may not be exact.

Swift HTML Parser

I am having trouble posting the outputs to a label. I have to covert it to a String? The error it seems to give me is "Cannot subscript a value of type JiNode? with an index of type 'Int'" Please help!
var meter = ""
#IBAction func calculate(sender: AnyObject) {
print("start scraping...")
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let url = NSURL(string: "http://uberestimate.com/costs.php")
let jiDoc = Ji(htmlURL: url!)
if jiDoc != nil {
print("html retrived.\n")
self.scrapeHTML(jiDoc!)
}
}
}
#IBOutlet weak var resultLabel: UILabel!
#IBOutlet weak var endingPoint: UITextField!
#IBOutlet weak var startingpoint: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
private func scrapeHTML(jiDoc: Ji) {
println("parsing...\n")
let bodyNode = jiDoc.xPath("//body")!.first!
var contentDivNode = bodyNode.xPath("//span[#style='font-size:1.3em']").first
if contentDivNode == nil {
print("unexpected format!")
}else{
var cdnArray = contentDivNode[1]
var cdn = cdnArray[0]! as String
self.resultLabel.text = cdn
// println(contentDivNode)
}
return
}
}
You can do something like:
#IBAction func calculate(sender: AnyObject) {
print("start scraping...")
let url = NSURL(string: "http://uberestimate.com/costs.php")
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { data, response, error in
if data == nil || error != nil { // in Swift 2, I'd use `guard`, but I tried to keep this backward compatible
print(error)
return
}
dispatch_async(dispatch_get_main_queue()) {
self.scrapeHTML(data!)
}
}
task.resume()
}
private func scrapeHTML(data: NSData) {
print("parsing...\n")
let jiDoc = Ji(data: data, isXML: false)
if let contentDivNode = jiDoc?.xPath("//span[#style='font-size:1.3em']")?.first {
self.resultLabel.text = contentDivNode.content
}
}
I'm using the Swift 2.1 (and I infer from the presence of println that you must be using an earlier version), but I think this is largely the same regardless of Swift version.
BTW, notice that I am dispatching the update of the label back to the main queue (as you called scrapeHTML from a global queue, but UI updates must happen on the main thread). I'm also using NSURLSession rather than dispatching a synchronous network request to a global queue.

Slider Not Excepting Current Default Value in Swift

Sorry to do this again, but my code has yet ANOTHER problem. When I run the program in the simulator, if I move the slider from its default position and hit the submit button, it will calculate the mpg. However, if I leave the slider at its default position of 500, then choose how many gallons I used (like normal), and hit "Submit", I get a value of 0.0 for the mpg. I know that it isn't right. My instructor suggested that I put the values that were originally in the milesDrivenSliderValueChanged function and put them in their own function. When I did, I got a bunch of errors. I fixed them, but I still get the same results. I would like to know what I am doing wrong.
Here is my code for the slider and the new function it calls:
class SecondViewController: UIViewController,UIPickerViewDataSource,UIPickerViewDelegate
{
var BlopSoundURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Blop", ofType: "mp3")!)
var soundAudioPlayer = AVAudioPlayer()
var miles = 0.0
var gallonsUsed = 0.0
var mpg: Double = 0.0
var currentSelection: Double?
#IBOutlet weak var milesDrivenLabel: UILabel!
#IBOutlet weak var milesDrivenSlider: UISlider!
#IBAction func milesDrivenSliderValueChanged(sender: UISlider)
{
currentSelection = Double(sender.value)
sliderMoved()
}
func sliderMoved()
{
milesDrivenLabel.text = "\(round(currentSelection! * 100) / 100)"
println("Current Selection = \(currentSelection)")
miles = currentSelection!
}
The rest of the code is for the Picker (which I don't think has any problems), the submit button, and the other functions:
#IBOutlet weak var gallonsUsedPicker: UIPickerView!
let pickerData = [1,2,3,4,5,6,7,8,9,10,11,
12,13,14,15,16,17,18,19,20,21,
22,23,24,25,26,27,28,29,30,31,
32,33,34,35,36,37,38,39,40,41,
42,43,44,45,46,47,48,49,50,51,
52,53,54,55,56,57,58,59,60,61,
62,63,64,65,66,67,68,69,70,71,
72,73,74,75,76,77,78,79,80,81,
82,83,84,85,86,87,88,89,90,91,
92,93,94,95,96,97,98,99,100]
#IBAction func submitButton(sender: UIButton)
{
soundAudioPlayer.play()
let rowValue = gallonsUsedPicker.selectedRowInComponent(0) + 1
gallonsUsed = Double(rowValue)
mpg = Double(round((miles * 100) / gallonsUsed) / 100)
let message = "Your MPG is approximately: \(mpg)."
let alert = UIAlertController(
title: "Your MPG",
message: message,
preferredStyle: .Alert)
let action = UIAlertAction(
title: "Ok",
style: .Default,
handler: nil)
alert.addAction(action)
presentViewController(alert, animated: true, completion: nil)
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return "\(row+1)"
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 100
}
override func viewDidLoad()
{
super.viewDidLoad()
gallonsUsedPicker.dataSource = self
gallonsUsedPicker.delegate = self
soundAudioPlayer = AVAudioPlayer(contentsOfURL: BlopSoundURL, error: nil)
// Do any additional setup after loading the view, typically from a nib.
} // End of viewDidLoad
This is really frustrating. It's probably a simple fix, but I just can't see it. I spent 3 hours on this last night and 2 more hours just this morning. Any help would be appreciated. Thank you for your time and efforts.
I'm a doofus. I just had to change miles to 500, instead of zero. Now it works. Sorry, guys.

iOS Swift "Extra argument in 'type' call" error when creating and calling a function

I'm having trouble understanding why I'm getting an error when creating and calling the function below. I used two argument both of type String. Why is this producing an error?
import UIKit
import AVFoundation
class PlaySoundsViewController: UIViewController {
func prepareAudio(sound: String, type: String) -> AVAudioPlayer {
var sound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(sound, ofType: type)!)
var error:NSError?
return AVAudioPlayer(contentsOfURL: sound, error: &error)
}
var audioPlayer = prepareAudio(sound: "movie_quote", type: "mp3")
I receive the Extra argument in 'type' call error when trying to set the audioPlayer variable to the result of the prepareAudio function.
The rest of the controller is below. Ultimately, I am trying to open up the mp3 file "movie_quote" and play it at a slower speed.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func slowSpeed(sender: UIButton) {
audioPlayer.enableRate = true
audioPlayer.rate = 0.5
audioPlayer.prepareToPlay()
audioPlayer.play()
}
In order for the code to work the way you have it, you need prepareAudio to be a class method, rather than an instance method. So the resulting code should look like this:
class PlaySoundsViewController: UIViewController {
class func prepareAudio(sound: String, type: String) -> AVAudioPlayer {
var sound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(sound, ofType: type)!)
var error:NSError?
return AVAudioPlayer(contentsOfURL: sound, error: &error)
}
var audioPlayer: AVAudioPlayer = PlaySoundsViewController.prepareAudio("movie_quote", type: "mp3")
}
Notice also that I changed the function call to prepareAudio("movie_quote", type: "mp3") because the first parameter doesn't have an external name by default. To change this, you can write class func prepareAudio(#sound: String, type: String) when defining the method.

Accessing Variables outside of a function in swift

Im getting coordinates from , but I'd like to be able to use them outside the Location Manager Function. Is it possible to make them instance variables? If so, could someone give me some example code for that? In the code below I print within the func and it works, but I'd like to be able to, for example print outside it, and otherwise use the coordinates. Perhaps as a global variable? How would i code that? I cant quite make it out from the documentation.
import UIKit
import CoreLocation
import MapKit
class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
var locationManager: CLLocationManager!
var seenError : Bool = false
var theSpan: MKCoordinateSpan = MKCoordinateSpanMake(0.1, 0.1)
var locationStatus : NSString = "Not Started"
var initialLoc:Int = 1
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.requestAlwaysAuthorization()
locationManager.delegate = self
var initialLoc:Int = 1
// Do any additional setup after loading the view, typically from a nib.
}
#IBOutlet weak var Map: MKMapView!
#IBAction func Here(sender: AnyObject) {
initLocationManager()
}
func initLocationManager() {
seenError = false
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.startUpdatingLocation()
Map.showsUserLocation = true
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var locationArray = locations as NSArray
var locationObj = locationArray.lastObject as CLLocation
var coord = locationObj.coordinate
if initialLoc == 1 {
var Region:MKCoordinateRegion = MKCoordinateRegionMake(coord, theSpan)
self.Map.setRegion(Region, animated:true)
initialLoc = 0
}
println(coord.latitude)
println(coord.longitude)
}
// Location Manager Delegate stuff
// If failed
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
locationManager.stopUpdatingLocation()
if (error) {
if (seenError == false) {
seenError = true
print(error)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
If I understand you correctly, I think the best option would be to make it an instance variable, as you said. This would be done by declaring it outside of the function with your other instance variables at the top of your class (the asterisks are used to show you what I added):
class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
var locationManager: CLLocationManager!
var seenError : Bool = false
var theSpan: MKCoordinateSpan = MKCoordinateSpanMake(0.1, 0.1)
var locationStatus : NSString = "Not Started"
var initialLoc:Int = 1
// Declare coordinate variable
***var coord: CLLocationCoordinate2D?***
The question mark declares the variable as an optional, so you don't have to immediately assign a value to it. Then, you assign the locationObj.coordinate value to coord in your locationManager function, however since you already declared the coord variable outside your function as an instance variable you can remove the var in var coord = locationObj.coordinate :
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var locationArray = locations as NSArray
var locationObj = locationArray.lastObject as CLLocation
//Assign value to coord variable
***coord = locationObj.coordinate***
if initialLoc == 1 {
var Region:MKCoordinateRegion = MKCoordinateRegionMake(coord, theSpan)
self.Map.setRegion(Region, animated:true)
initialLoc = 0
}
Then you can use the variable normally in the rest of your function, in addition to any other function in the class (like a global variable).
Best of luck!
P.S. Learn how to do this well, as it is a method used all the time when working with functions and variables
At the point where you read the location into a local variable, instead read it into an instance variable. So instead of:
var locationObj = locationArray.lastObject as CLLocation
use
self.locationObj = locationArray.lastObject as CLLocation
and declare
var locationObj : CLLocation?
in your ViewController. Then you can access locationObj with 'dot' notation.