diff --git a/Cartfile.resolved b/Cartfile.resolved index e477dd0..4185b75 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,8 +1,7 @@ github "Alamofire/Alamofire" "4.4.0" -github "Clipy/Magnet" "v2.0.0" -github "OAuthSwift/OAuthSwift" "1.1.1" +github "Clipy/Magnet" "v2.0.1" github "Octree/SwiftyWave" "0.0.2" github "SwiftyJSON/SwiftyJSON" "3.1.4" github "apple/swift-protobuf" "0.9.901" github "audiokit/AudioKit" "v3.6" -github "grpc/grpc-swift" "b570094bd0ac26b3b41d062f1f219c6119d058a3" +github "grpc/grpc-swift" "a8a2c1892b80f961dc2225befccfd1da329a0d4b" diff --git a/Carthage/Build/Mac/SwiftProtobuf.framework.dSYM/Contents/Resources/DWARF/SwiftProtobuf b/Carthage/Build/Mac/SwiftProtobuf.framework.dSYM/Contents/Resources/DWARF/SwiftProtobuf index 74a2040..f41c72d 100644 Binary files a/Carthage/Build/Mac/SwiftProtobuf.framework.dSYM/Contents/Resources/DWARF/SwiftProtobuf and b/Carthage/Build/Mac/SwiftProtobuf.framework.dSYM/Contents/Resources/DWARF/SwiftProtobuf differ diff --git a/Carthage/Build/Mac/SwiftProtobuf.framework/Versions/A/SwiftProtobuf b/Carthage/Build/Mac/SwiftProtobuf.framework/Versions/A/SwiftProtobuf index 72ca5e4..4303654 100755 Binary files a/Carthage/Build/Mac/SwiftProtobuf.framework/Versions/A/SwiftProtobuf and b/Carthage/Build/Mac/SwiftProtobuf.framework/Versions/A/SwiftProtobuf differ diff --git a/MacAssistant.xcodeproj/project.pbxproj b/MacAssistant.xcodeproj/project.pbxproj index 95842b7..0d0b07b 100644 --- a/MacAssistant.xcodeproj/project.pbxproj +++ b/MacAssistant.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ FF17DA081EB56CCC003E1412 /* google.assistant.embedded.v1alpha1.client.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17DA061EB56CCC003E1412 /* google.assistant.embedded.v1alpha1.client.pb.swift */; }; FF17DA0A1EB56CD4003E1412 /* embedded_assistant.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17DA091EB56CD4003E1412 /* embedded_assistant.pb.swift */; }; FF17DA0C1EB56CDB003E1412 /* status.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17DA0B1EB56CDB003E1412 /* status.pb.swift */; }; + FF2652021EC93DEE00ACDBB5 /* begin_prompt.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FF2652011EC93DEE00ACDBB5 /* begin_prompt.mp3 */; }; FF41AB811EB40FB8003E994B /* Magnet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF41AB801EB40FB8003E994B /* Magnet.framework */; }; FF41AB821EB40FB8003E994B /* Magnet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FF41AB801EB40FB8003E994B /* Magnet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; FF41AB841EB40FC6003E994B /* Magnet.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = FF41AB831EB40FC6003E994B /* Magnet.framework.dSYM */; }; @@ -41,6 +42,7 @@ FFA471071EB2D47300A5F89A /* SpeechWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA471061EB2D47300A5F89A /* SpeechWaveformView.swift */; }; FFA471091EB2DF8D00A5F89A /* AudioKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FFA471081EB2DF8D00A5F89A /* AudioKit.framework */; }; FFA4710A1EB2DF8D00A5F89A /* AudioKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FFA471081EB2DF8D00A5F89A /* AudioKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FFCED5F81EC4268B0049676B /* WaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCED5F71EC4268B0049676B /* WaveformView.swift */; }; FFE68A761EB2A81E00F60EF5 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE68A751EB2A81E00F60EF5 /* LoginViewController.swift */; }; FFE68A781EB2A81E00F60EF5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFE68A771EB2A81E00F60EF5 /* Assets.xcassets */; }; FFE9B12F1EC110FC0034E85B /* roots.pem in Resources */ = {isa = PBXBuildFile; fileRef = FFE9B12E1EC110FC0034E85B /* roots.pem */; }; @@ -93,6 +95,7 @@ FF17DA061EB56CCC003E1412 /* google.assistant.embedded.v1alpha1.client.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = google.assistant.embedded.v1alpha1.client.pb.swift; sourceTree = ""; }; FF17DA091EB56CD4003E1412 /* embedded_assistant.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = embedded_assistant.pb.swift; sourceTree = ""; }; FF17DA0B1EB56CDB003E1412 /* status.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = status.pb.swift; sourceTree = ""; }; + FF2652011EC93DEE00ACDBB5 /* begin_prompt.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = begin_prompt.mp3; sourceTree = ""; }; FF41AB801EB40FB8003E994B /* Magnet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Magnet.framework; path = Carthage/Build/Mac/Magnet.framework; sourceTree = ""; }; FF41AB831EB40FC6003E994B /* Magnet.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = Magnet.framework.dSYM; path = Carthage/Build/Mac/Magnet.framework.dSYM; sourceTree = ""; }; FF41AB851EB416F4003E994B /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; @@ -115,6 +118,7 @@ FFA471041EB2CE4500A5F89A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = MacAssistant/Base.lproj/MainMenu.xib; sourceTree = ""; }; FFA471061EB2D47300A5F89A /* SpeechWaveformView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeechWaveformView.swift; sourceTree = ""; }; FFA471081EB2DF8D00A5F89A /* AudioKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioKit.framework; path = Carthage/Build/Mac/AudioKit.framework; sourceTree = ""; }; + FFCED5F71EC4268B0049676B /* WaveformView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaveformView.swift; sourceTree = ""; }; FFE68A701EB2A81E00F60EF5 /* MacAssistant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacAssistant.app; sourceTree = BUILT_PRODUCTS_DIR; }; FFE68A731EB2A81E00F60EF5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; FFE68A751EB2A81E00F60EF5 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; @@ -211,6 +215,7 @@ FFE68A751EB2A81E00F60EF5 /* LoginViewController.swift */, FFA471011EB2C9F200A5F89A /* LoginView.xib */, FFA471061EB2D47300A5F89A /* SpeechWaveformView.swift */, + FFCED5F71EC4268B0049676B /* WaveformView.swift */, FFA471031EB2CE4500A5F89A /* MainMenu.xib */, FF6AA52E1EBA77F60009E342 /* Authenticator.swift */, FF6AA5341EBA8A980009E342 /* Constants.swift */, @@ -218,6 +223,7 @@ FFE9B1321EC189D60034E85B /* CustomPlot.swift */, FFE9B12E1EC110FC0034E85B /* roots.pem */, FFE9B1341EC195AB0034E85B /* google_oauth.json */, + FF2652011EC93DEE00ACDBB5 /* begin_prompt.mp3 */, FFE68A7C1EB2A81E00F60EF5 /* Info.plist */, FFE68A771EB2A81E00F60EF5 /* Assets.xcassets */, ); @@ -289,6 +295,7 @@ FFE9B12F1EC110FC0034E85B /* roots.pem in Resources */, FFA471001EB2C9EB00A5F89A /* AssistantView.xib in Resources */, FFE68A781EB2A81E00F60EF5 /* Assets.xcassets in Resources */, + FF2652021EC93DEE00ACDBB5 /* begin_prompt.mp3 in Resources */, FFF3FAC11EBD5ADD005E87C9 /* LoadingView.xib in Resources */, FFA471021EB2C9F200A5F89A /* LoginView.xib in Resources */, FFA471051EB2CE4500A5F89A /* MainMenu.xib in Resources */, @@ -304,6 +311,7 @@ files = ( FFA470CD1EB2B9B900A5F89A /* AppDelegate.swift in Sources */, FF17DA0A1EB56CD4003E1412 /* embedded_assistant.pb.swift in Sources */, + FFCED5F81EC4268B0049676B /* WaveformView.swift in Sources */, FFA471071EB2D47300A5F89A /* SpeechWaveformView.swift in Sources */, FFF3FABD1EBD545E005E87C9 /* ConversationEntry.swift in Sources */, FF41AB861EB416F4003E994B /* API.swift in Sources */, diff --git a/MacAssistant/API.swift b/MacAssistant/API.swift index e8c36a9..1ca69dc 100644 --- a/MacAssistant/API.swift +++ b/MacAssistant/API.swift @@ -45,18 +45,17 @@ class API { self.converseState = nil do { self.converseState = try ConverseState(serializedData: response.result.conversationState) } catch { print("ConverseState parse error") } - self.delegate.updateRequestText(response.result.spokenRequestText) - self.delegate.updateResponseText(response.result.spokenResponseText.isEmpty ? "Speaking response..." : response.result.spokenResponseText) - // TODO: Save file before playing it? - if response.audioOut.audioData.count > 0 { - buf.append(response.audioOut.audioData) + if !response.result.spokenRequestText.isEmpty { + self.delegate.updateRequestText(response.result.spokenRequestText) } + self.delegate.updateResponseText(response.result.spokenResponseText.isEmpty ? "Speaking response..." : response.result.spokenResponseText) + if response.audioOut.audioData.count > 0 { buf.append(response.audioOut.audioData) } if response.eventType == .endOfUtterance { self.delegate.stopListening() } } if let error = error { print("Initial receive error: \(error)") } } - func initiateRequest() { + func initiateRequest(volumePercent: Int32) { var request = ConverseRequest() request.config = ConverseConfig() @@ -69,7 +68,7 @@ class API { var audioOutConfig = AudioOutConfig() audioOutConfig.sampleRateHertz = Int32(Constants.GOOGLE_SAMPLE_RATE) // TODO: Play back the response and find the appropriate value audioOutConfig.encoding = .mp3 - audioOutConfig.volumePercentage = 50 + audioOutConfig.volumePercentage = volumePercent request.config.audioOutConfig = audioOutConfig do { @@ -92,6 +91,7 @@ class API { func doneSpeaking() { do { try currentCall?.closeSend { print("Closed send") } + // Receive all response audio responses DispatchQueue.global().async { while true { do { @@ -113,6 +113,12 @@ class API { } } + func donePlayingResponse() { + if followUp { + delegate.startListening() + } + } + func debugPrint(result: ConverseReponse) { print("\n++++++++++++++++++++++++++++++") print("Close receive result error: \(result.error.code)") diff --git a/MacAssistant/AppDelegate.swift b/MacAssistant/AppDelegate.swift index 1447f17..76da7bc 100644 --- a/MacAssistant/AppDelegate.swift +++ b/MacAssistant/AppDelegate.swift @@ -27,7 +27,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { super.init() popover.contentViewController = NSViewController(nibName: "LoadingView", bundle: nil) // popover.appearance = NSAppearance(named: NSAppearanceNameVibrantDark) - registerHotkey() +// registerHotkey() // TODO: Proper interaction between hotkey and window } func applicationWillFinishLaunching(_ notification: Notification) { @@ -65,14 +65,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { keyCombo: keyCombo, target: self, action: #selector(AppDelegate.hotkeyPressed)) - hotKey.register() + hotKey.register() } func hotkeyPressed(sender: AnyObject?) { - if (!popover.isShown) { + if !popover.isShown { showPopover(sender: sender) + } else if let controller = popover.contentViewController as? AssistantViewController { + if controller.isListening { + controller.stopListening() + } } - if (isLoggedIn) { (popover.contentViewController as? AssistantViewController)?.startListening() } @@ -84,12 +87,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { func showPopover(sender: AnyObject?) { if let button = statusItem.button { - popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) + popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) } } func closePopover(sender: AnyObject?) { popover.performClose(sender) + if let controller = popover.contentViewController as? AssistantViewController { + controller.stopListening() + } } func togglePopover(sender: AnyObject?) { diff --git a/MacAssistant/AssistantView.xib b/MacAssistant/AssistantView.xib index a15fcd5..2580a8d 100644 --- a/MacAssistant/AssistantView.xib +++ b/MacAssistant/AssistantView.xib @@ -9,9 +9,8 @@ - - - + + @@ -19,15 +18,15 @@ - + - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - + @@ -127,5 +107,6 @@ + diff --git a/MacAssistant/AssistantViewController.swift b/MacAssistant/AssistantViewController.swift index 0c46eaa..f33aaea 100644 --- a/MacAssistant/AssistantViewController.swift +++ b/MacAssistant/AssistantViewController.swift @@ -10,12 +10,13 @@ import Cocoa import AudioKit import AVFoundation -class AssistantViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, ConversationTextDelegate { +class AssistantViewController: NSViewController, ConversationTextDelegate, AVAudioPlayerDelegate { @IBOutlet weak var waveformView: CustomPlot! @IBOutlet weak var microphoneButton: NSButton! - @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var speakerButton: NSButton! + @IBOutlet weak var spokenTextLabel: NSTextField! private var player: AVAudioPlayer? private let googleColors = [NSColor.red, NSColor.blue, NSColor.yellow, NSColor.green] @@ -35,74 +36,40 @@ class AssistantViewController: NSViewController, NSTableViewDelegate, NSTableVie private lazy var outputBuffer: AVAudioPCMBuffer = AVAudioPCMBuffer(pcmFormat: self.desiredFormat, frameCapacity: AVAudioFrameCount(Constants.GOOGLE_SAMPLES_PER_FRAME)) + public var isListening: Bool { get { return AudioKit.engine.isRunning } } + override func viewDidLoad() { super.viewDidLoad() - loadFakeData() setupPlot() - tableView.delegate = self - tableView.dataSource = self AudioKit.output = AKBooster(mic, gain: 0) AudioKit.engine.inputNode?.installTap(onBus: 0, bufferSize: UInt32(Constants.NATIVE_SAMPLES_PER_FRAME), format: nil, block: onTap) } - private func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSCell? { - let convo = conversation[row] - let cell = NSTextFieldCell(textCell: convo.text) - cell.alignment = convo.fromUser ? .right : .left - cell.textColor = NSColor.white - print("configuring") - return cell - } - - - - // TODO -// func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { -// return conversation[row].text -// let convo = conversation[row] -// if tableColumn?.identifier == "rightColumn" { -// if convo.fromUser { -// return convo.text -// } -// } -// if tableColumn?.identifier == "leftColumn" { -// if !convo.fromUser { -// return convo.text -// } -// } -// return nil -// } - - func numberOfRows(in tableView: NSTableView) -> Int { return conversation.count } - public func onTap(buffer: AVAudioPCMBuffer, _: AVAudioTime) { - if let _ = buffer.floatChannelData { - var err: NSError? - converter.convert(to: outputBuffer, error: &err) { packetCount, inputStatusPtr in - inputStatusPtr.pointee = .haveData - return buffer - } - - if let error = err { - print("Conversion error \(error)") - } else { - if let data = outputBuffer.int16ChannelData { - self.api.sendAudio(frame: data, withLength: Int(outputBuffer.frameLength)) - } - } + var error: NSError? + converter.convert(to: outputBuffer, error: &error) { _, inputStatusPtr in + inputStatusPtr.pointee = .haveData + return buffer + } + + if let error = error { print("Conversion error \(error)") } + else if let data = outputBuffer.int16ChannelData { + self.api.sendAudio(frame: data, withLength: Int(outputBuffer.frameLength)) } } func setupPlot() { waveformView.setClickListener(h: buttonAction) - plot.shouldFill = false + plot.shouldFill = true plot.shouldMirror = true plot.color = googleColors[0] plot.backgroundColor = NSColor.clear plot.autoresizingMask = .viewWidthSizable + plot.shouldOptimizeForRealtimePlot = true + plot.plotType = .buffer waveformView.addSubview(plot) Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(self.updatePlotWaveformColor), userInfo: nil, repeats: true); } @@ -115,18 +82,23 @@ class AssistantViewController: NSViewController, NSTableViewDelegate, NSTableVie @IBAction func buttonAction(_ sender: Any) { if AudioKit.engine.isRunning { stopListening() - } else { + } else if !(player?.isPlaying ?? false) { startListening() } } func startListening() { - api.initiateRequest() + api.initiateRequest(volumePercent: Int32(mic.volume * 100)) AudioKit.start() + let file = Bundle.main.url(forResource: "begin_prompt", withExtension: "mp3")! + player = try! AVAudioPlayer(contentsOf: file) + player!.play() DispatchQueue.main.async { self.microphoneButton.isHidden = true self.plot.isHidden = false + self.speakerButton.isHidden = true } + spokenTextLabel.stringValue = "" } func stopListening() { @@ -135,33 +107,47 @@ class AssistantViewController: NSViewController, NSTableViewDelegate, NSTableVie DispatchQueue.main.async { self.microphoneButton.isHidden = false self.plot.isHidden = true + self.speakerButton.isHidden = true } } - // TODO func playResponse(_ data: Data) { do { player = try AVAudioPlayer(data: data, fileTypeHint: AVFileTypeMPEGLayer3) player?.play() - - } catch { print("Audio out error \(error):\(error.localizedDescription)") } + player?.delegate = self + speakerIcon(isShown: true) + } catch { + print("Audio out error \(error):\(error.localizedDescription)") + speakerIcon(isShown: false) + } + } + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + speakerIcon(isShown: false) + api.donePlayingResponse() + } + + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + speakerIcon(isShown: false) } - func loadFakeData() { - for i in 0...10 { - conversation.append(ConversationEntry(text: "User \(i)", fromUser: true)) - conversation.append(ConversationEntry(text: "Response \(i)", fromUser: false)) + func speakerIcon(isShown: Bool) { + DispatchQueue.main.async { + self.speakerButton.isHidden = !isShown + self.microphoneButton.isHidden = isShown + self.plot.isHidden = true } } func updateRequestText(_ text: String) { + print("Request text: \(text)") + spokenTextLabel.stringValue = "\"\(text)\"" conversation.append(ConversationEntry(text: text, fromUser: true)) - tableView.reloadData() } func updateResponseText(_ text: String) { conversation.append(ConversationEntry(text: text, fromUser: false)) - tableView.reloadData() } @IBAction func gearClicked(_ sender: Any) { @@ -169,6 +155,7 @@ class AssistantViewController: NSViewController, NSTableViewDelegate, NSTableVie } @IBAction func actionClicked(_ sender: Any) { + } @IBAction func settingsClicked(_ sender: Any) { diff --git a/MacAssistant/LoginView.xib b/MacAssistant/LoginView.xib index ba46fc3..da32f52 100644 --- a/MacAssistant/LoginView.xib +++ b/MacAssistant/LoginView.xib @@ -16,11 +16,11 @@ - + - + @@ -28,7 +28,7 @@ - + diff --git a/MacAssistant/WaveformView.swift b/MacAssistant/WaveformView.swift index 9a63ad8..9238664 100644 --- a/MacAssistant/WaveformView.swift +++ b/MacAssistant/WaveformView.swift @@ -6,4 +6,89 @@ // Copyright © 2017 vanshgandhi. All rights reserved. // -import Foundation +// +// WaveformView.swift +// WaveformView +// +// Created by Jonathan on 3/14/15. +// Copyright (c) 2015 Underwood. All rights reserved. +// + +import Cocoa +import Darwin + +let pi = Double.pi + +public class WaveformView: NSView { + + fileprivate var _phase: CGFloat = 0.0 + fileprivate var _amplitude: CGFloat = 0.3 + + @IBInspectable public var waveColor: NSColor = .black + @IBInspectable public var numberOfWaves = 5 + @IBInspectable public var primaryWaveLineWidth: CGFloat = 3.0 + @IBInspectable public var secondaryWaveLineWidth: CGFloat = 1.0 + @IBInspectable public var idleAmplitude: CGFloat = 0.01 + @IBInspectable public var frequency: CGFloat = 1.25 + @IBInspectable public var density: CGFloat = 5 + @IBInspectable public var phaseShift: CGFloat = -0.15 + + @IBInspectable public var amplitude: CGFloat { + get { + return _amplitude + } + } + + public func updateWithLevel(_ level: CGFloat) { + _phase += phaseShift + _amplitude = fmax(level, idleAmplitude) + needsDisplay = true + } + + override public func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let context = NSGraphicsContext.current()!.cgContext + context.clear(bounds) + + // backgroundColor?.set() + context.fill(dirtyRect) + + // Draw multiple sinus waves, with equal phases but altered + // amplitudes, multiplied by a parable function. + for waveNumber in 0...numberOfWaves { + context.setLineWidth((waveNumber == 0 ? primaryWaveLineWidth : secondaryWaveLineWidth)) + + let halfHeight = bounds.height / 2.0 + let width = bounds.width + let mid = width / 2.0 + + let maxAmplitude = halfHeight - 4.0 // 4 corresponds to twice the stroke width + // Progress is a value between 1.0 and -0.5, determined by the current wave idx, + // which is used to alter the wave's amplitude. + let progress: CGFloat = 1.0 - CGFloat(waveNumber) / CGFloat(numberOfWaves) + let normedAmplitude = (1.5 * progress - 0.5) * amplitude + + let multiplier: CGFloat = 1.0 + waveColor.withAlphaComponent(multiplier * waveColor.cgColor.alpha).set() + + var x: CGFloat = 0.0 + while x < width + density { + // Use a parable to scale the sinus wave, that has its peak in the middle of the view. + let scaling = -pow(1 / mid * (x - mid), 2) + 1 + let tempCasting: CGFloat = 2.0 * CGFloat(pi) * CGFloat(x / width) * frequency + _phase + let y = scaling * maxAmplitude * normedAmplitude * CGFloat(sinf(Float(tempCasting))) + halfHeight + + if x == 0 { + context.move(to: CGPoint(x: x, y: y)) + } else { + context.addLine(to: CGPoint(x: x, y: y)) + } + + x += density + } + + context.strokePath() + } + } +}