Created
February 28, 2018 13:55
-
-
Save Ivorforce/5a5e08b42437f2bb8fc8c52e8e285e85 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// TrackSpectrumView.swift | |
// TenTunes | |
// | |
// Created by Lukas Tenbrink on 17.02.18. | |
// Copyright © 2018 ivorius. All rights reserved. | |
// | |
import Cocoa | |
import QuartzCore | |
func lerp(_ left: [CGFloat], _ right: [CGFloat], _ amount: CGFloat) -> [CGFloat] { | |
return zip(left, right).map { (cur, dest) in | |
return cur * (CGFloat(1.0) - amount) + dest * amount | |
} | |
} | |
class TrackSpectrumView: NSControl { | |
var location: Double? { | |
didSet { | |
self.setNeedsDisplay() | |
} | |
} | |
var _barLayers: [CALayer] = [] | |
var _positionLayer = CALayer() | |
var analysis: Analysis? = nil | |
var _curValues: [[CGFloat]] = Array(repeating: Array(repeating: CGFloat(0), count: Analysis.sampleCount), count: 4) | |
var timer: Timer? = nil | |
var updateTime = 1.0 / 20.0 | |
var lerpRatio = CGFloat(1.0 / 5.0) | |
override func awakeFromNib() { | |
postsFrameChangedNotifications = true | |
NotificationCenter.default.addObserver(self, selector: #selector(updateBounds), name: NSView.frameDidChangeNotification, object: self) | |
self.wantsLayer = true | |
let bgLayer = CAGradientLayer() | |
bgLayer.colors = [ | |
NSColor.black.withAlphaComponent(0.4), | |
NSColor.clear | |
] | |
// bgLayer = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1) | |
self.layer = bgLayer | |
_positionLayer = CALayer() | |
_positionLayer.backgroundColor = CGColor.white | |
self.layer!.addSublayer(_positionLayer) | |
// Initial update bars | |
self.resize(withOldSuperviewSize: self.bounds.size) | |
self.timer = Timer.scheduledTimer(withTimeInterval: updateTime, repeats: true) { _ in | |
let drawValues = self.analysis?.values ?? Array(repeating: Array(repeating: CGFloat(0), count: Analysis.sampleCount), count: 4) | |
self._curValues = (0..<4).map { lerp(self._curValues[$0], drawValues[$0], self.lerpRatio) } | |
self.updateLayers() | |
} | |
} | |
@objc func updateBounds() { | |
_positionLayer.frame = CGRect(x: 0, y: 0, width: 1, height: bounds.height) | |
// Clean Up | |
for layer in _barLayers { layer.removeFromSuperlayer() } | |
let barWidth = 2 | |
let segmentWidth = barWidth + 2 | |
let numBars = Int(self.bounds.width / CGFloat(segmentWidth)) | |
let start = bounds.minX + (bounds.width - CGFloat(numBars * segmentWidth)) / 2 | |
_barLayers = Array(0..<numBars).map { idx in | |
let bar = CALayer() | |
bar.zPosition = -1 | |
bar.actions = [ | |
"onOrderIn": NSNull(), | |
"onOrderOut": NSNull(), | |
"sublayers": NSNull(), | |
"contents": NSNull(), | |
"bounds": NSNull() | |
] | |
bar.frame = CGRect( | |
x: start + CGFloat(idx * segmentWidth) + 1, | |
y: self.bounds.minY, | |
width: CGFloat(barWidth), | |
height: 1 // Changed in update | |
) | |
self.layer!.addSublayer(bar) | |
return bar | |
} | |
updateLayers() | |
} | |
func updateLayers() { | |
let numBars = _barLayers.count | |
let values = _curValues.map { wf in wf.remap(toSize: numBars) } | |
let waveform = values[0], lows = values[1], mids = values[2], highs = values[3] | |
for run in 0...1 { | |
if run == 0 { | |
CATransaction.begin() | |
CATransaction.setAnimationDuration(updateTime) | |
} | |
for (idx, bar) in _barLayers.enumerated() { | |
// Height | |
let h = waveform[idx] | |
bar.frame.size.height = CGFloat(h * self.bounds.height) | |
// Color | |
let low = lows[idx] * lows[idx], mid = mids[idx] * mids[idx], high = highs[idx] * highs[idx] | |
let val = low + mid + high | |
// Don't go the full way so we don't loop back to red | |
bar.backgroundColor = NSColor(hue: (mid / val / 2 + high / val) * 0.8, saturation: CGFloat(0.3), brightness: CGFloat(0.8), alpha: CGFloat(1.0)).cgColor | |
} | |
if let location = location { | |
_positionLayer.frame.origin.x = CGFloat(location) * self.bounds.width | |
_positionLayer.isHidden = false | |
} | |
else { | |
_positionLayer.isHidden = true | |
} | |
if run == 0 { | |
CATransaction.commit() | |
} | |
} | |
} | |
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { | |
return true | |
} | |
func click(at: NSPoint) { | |
self.location = (0.0...1.0).clamp(Float(at.x) / Float(self.bounds.width)) | |
if let action = self.action, let target = self.target { | |
NSApp.sendAction(action, to: target, from: self) | |
} | |
} | |
override func mouseDragged(with event: NSEvent) { | |
self.click(at: self.convert(event.locationInWindow, from: nil)) | |
} | |
override func mouseDown(with event: NSEvent) { | |
self.click(at: self.convert(event.locationInWindow, from: nil)) | |
} | |
} | |
import AudioKit | |
import AudioKitUI | |
extension TrackSpectrumView { | |
func setBy(player: AKPlayer) { | |
if player.audioFile != nil { | |
self.setBy(time: player.currentTime, max: player.duration) | |
} | |
else { | |
self.location = nil | |
} | |
} | |
func setBy(time: Double, max: Double) { | |
self.location = time / max | |
} | |
func getBy(player: AKPlayer) -> Double? { | |
return player.audioFile != nil ? self.getBy(max: player.duration) : nil | |
} | |
func getBy(max: Double) -> Double? { | |
return self.location != nil ? self.location! * max : nil | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment