英文:
What is the best approach for rapid sound playback on iOS?
问题
I have a UI that allows someone to move a dial and the dial 'snaps' to each 'mark' on the dial.
我有一个界面,允许用户移动一个旋钮,旋钮会“吸附”到旋钮上的每个“标记”。
I want to add sound to this and I've made a very short 'click' sound that is a fraction of a second.
我想为这个功能添加声音,我已经制作了一个非常短的“点击”声音,持续时间很短。
I don't want to restrict how fast the user can rotate the dial, but I want the sound to play as the dial goes to each mark.
我不想限制用户旋转旋钮的速度,但我希望在旋钮移动到每个标记时播放声音。
So I need a fast and responsive Audio library to use, however I also know I need to limit how many times it's played in case they spin it so quickly that otherwise the sound would become a constant noise, rather than distinct clicks.
因此,我需要一个快速响应的音频库来使用,但我也知道我需要限制播放次数,以防他们旋转得太快,否则声音会变成持续的噪音,而不是明显的点击声。
I've seen comments that 'AVFoundation' is too slow and that 'AVAudioEngine' was going to give a better performance, but I'm still not sure if that's the best approach and how to tackle limiting the 'repetitive sound' so it's not just a horrendous noise.
我看到有评论说'AVFoundation'太慢了,而'AVAudioEngine'会提供更好的性能,但我仍然不确定这是否是最佳方法以及如何限制“重复声音”,使其不会成为可怕的噪音。
I realize this is kind of something that games programmers deal with more than non-game iOS app developers deal with but I'm still stuck for an approach.
我意识到这更像是游戏程序员处理的问题,而不是非游戏iOS应用程序开发者处理的问题,但我仍然没有找到解决方案。
英文:
I have a UI that allows someone to move a dial and the dial 'snaps' to each 'mark' on the dial.
I want to add sound to this and I've made a very short 'click' sound that is a fraction of a second.
I don't want to restrict how fast the user can rotate the dial, but I want the sound to play as the dial goes to each mark.
So I need a fast and responsive Audio library to use, however I also know I need to limit how many times it's played in case they spin it so quickly that otherwise the sound would become a constant noise, rather than distinct clicks.
I've seen comments that AVFoundation
is too slow and that AVAudioEngine
was going to give a better performance, but I'm still not sure if that's the best approach and how to tackle limiting the 'repetitive sound' so it's not just a horrendous noise.
I realise this is kind of something that games programmers deal with more than non-game iOS app developers deal with but I'm still stuck for an approach.
答案1
得分: 1
以下是您要翻译的代码部分:
// view with "tick-mark" lines every 20-points
class TickView: UIView {
lazy var tickLayer: CAShapeLayer = self.layer as! CAShapeLayer
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
private func commonInit() {
tickLayer.fillColor = nil
tickLayer.strokeColor = UIColor.red.cgColor
backgroundColor = .yellow
}
override func layoutSubviews() {
super.layoutSubviews()
let y: CGFloat = bounds.maxY * 0.75
let shortTick: CGFloat = bounds.maxY * 0.25
let tallTick: CGFloat = bounds.maxY * 0.5
let bez = UIBezierPath()
var pt: CGPoint = .init(x: bounds.minX, y: y)
// horizontal line full width of view
bez.move(to: pt)
bez.addLine(to: .init(x: bounds.maxX, y: pt.y))
// add vertical "tick" lines every 20-points
// with a taller line every 100-points
bez.move(to: pt)
while pt.x <= bounds.maxX {
bez.move(to: pt)
if Int(pt.x) % 100 == 0 {
bez.addLine(to: .init(x: pt.x, y: pt.y - tallTick))
} else {
bez.addLine(to: .init(x: pt.x, y: pt.y - shortTick))
}
pt.x += 20.0
}
tickLayer.path = bez.cgPath
}
}
// view with single vertical line
// overlay on the scroll view so we have a
// "center-line"
class MidLineView: UIView {
lazy var midLineLayer: CAShapeLayer = self.layer as! CAShapeLayer
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
private func commonInit() {
midLineLayer.fillColor = nil
midLineLayer.strokeColor = UIColor.blue.cgColor
backgroundColor = .clear
}
override func layoutSubviews() {
super.layoutSubviews()
let bez = UIBezierPath()
// we want the mid line to be *about* at the horizontal center
// but at an even 20-points
var x: Int = Int(bounds.midX)
x -= x % 20
bez.move(to: .init(x: CGFloat(x), y: bounds.minY))
bez.addLine(to: .init(x: CGFloat(x), y: bounds.maxY))
midLineLayer.path = bez.cgPath
}
}
请注意,这是Swift代码的一部分,已翻译成中文。如果您需要更多翻译或有其他问题,请告诉我。
英文:
One approach...
Play the "click" sound every time the "current tick mark" changes.
This will be slightly different, depending on how you are animating the "dial" -- but the concept is the same. Let's use a scroll view for example.
For the scrollable content, we'll use a view and draw a vertical "tick mark" every 20-points, taller on even 100-points positions. We'll also overlay a view with a single vertical line near the horizontal center - so we want to play a "click" when a tick hits that line. And we'll size things so we can only scroll horizontally.
It will look like this:
and after scrolling a little:
When implementing scrollViewDidScroll(...)
with a typical scroll view, it is very easy to scroll quickly... so quickly, that the .contentOffset.x
can change 200+ points between calls.
If we try to play the tick sound for every 20-points of change, we could be playing it 10 times at essentially the same time.
So, we could create a class property:
var prevTickMark: Int = 0
then calculate the current tick mark in scrollViewDidScroll(...)
. If the values are different, play a tick sound:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var cx = Int(scrollView.contentOffset.x)
// offset to the first tick-mark
cx += Int(scrollView.contentInset.left)
let curTick: Int = cx / 20
if prevTickMark != curTick {
// we just passed, or we are on, a new "tick"
// so play the tick sound
AudioServicesPlayAlertSound(SystemSoundID(1057))
prevTickMark = curTick
}
}
If we are scrolling / dragging very, very quickly, we don't need a click for every tick mark... because we are not seeing every tick mark cross the center-line.
As the scrolling decelerates -- or when dragging slowly -- we'll get a click on every tick.
Here's some quick example code to try out...
TickView - ticks every 20-points
class TickView: UIView {
lazy var tickLayer: CAShapeLayer = self.layer as! CAShapeLayer
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
private func commonInit() {
tickLayer.fillColor = nil
tickLayer.strokeColor = UIColor.red.cgColor
backgroundColor = .yellow
}
override func layoutSubviews() {
super.layoutSubviews()
let y: CGFloat = bounds.maxY * 0.75
let shortTick: CGFloat = bounds.maxY * 0.25
let tallTick: CGFloat = bounds.maxY * 0.5
let bez = UIBezierPath()
var pt: CGPoint = .init(x: bounds.minX, y: y)
// horizontal line full width of view
bez.move(to: pt)
bez.addLine(to: .init(x: bounds.maxX, y: pt.y))
// add vertical "tick" lines every 20-points
// with a taller line every 100-points
bez.move(to: pt)
while pt.x <= bounds.maxX {
bez.move(to: pt)
if Int(pt.x) % 100 == 0 {
bez.addLine(to: .init(x: pt.x, y: pt.y - tallTick))
} else {
bez.addLine(to: .init(x: pt.x, y: pt.y - shortTick))
}
pt.x += 20.0
}
tickLayer.path = bez.cgPath
}
}
MidLineView - vertical line to overlay on the scroll view
class MidLineView: UIView {
lazy var midLineLayer: CAShapeLayer = self.layer as! CAShapeLayer
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
private func commonInit() {
midLineLayer.fillColor = nil
midLineLayer.strokeColor = UIColor.blue.cgColor
backgroundColor = .clear
}
override func layoutSubviews() {
super.layoutSubviews()
let bez = UIBezierPath()
// we want the mid line to be *about* at the horizontal center
// but at an even 20-points
var x: Int = Int(bounds.midX)
x -= x % 20
bez.move(to: .init(x: CGFloat(x), y: bounds.minY))
bez.addLine(to: .init(x: CGFloat(x), y: bounds.maxY))
midLineLayer.path = bez.cgPath
}
}
ViewController - example controller
class ViewController: UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
// view with "tick-mark" lines every 20-points
let tickView = TickView()
// view with single vertical line
// overlay on the scroll view so we have a
// "center-line"
let midLineView = MidLineView()
// track the previous "tick"
var prevTickMark: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
tickView.translatesAutoresizingMaskIntoConstraints = false
midLineView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(tickView)
view.addSubview(scrollView)
view.addSubview(midLineView)
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.heightAnchor.constraint(equalToConstant: 120.0),
scrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
tickView.topAnchor.constraint(equalTo: cg.topAnchor),
tickView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
tickView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
tickView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
// let's make the "tick" view 2000-points wide
// so we have a good amount of scrolling distance
tickView.widthAnchor.constraint(equalToConstant: 2000.0),
tickView.heightAnchor.constraint(equalTo: fg.heightAnchor, multiplier: 1.0),
midLineView.topAnchor.constraint(equalTo: scrollView.topAnchor),
midLineView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
midLineView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
midLineView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
])
scrollView.delegate = self
// disable interaction on the overlaid view
midLineView.isUserInteractionEnabled = false
// so we can see the framing of the scroll view
scrollView.backgroundColor = .lightGray
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// offsets so the "ticks" start and end near the horiztonal center
// on even 20-points
var x: Int = Int(scrollView.frame.width * 0.5)
x -= x % 20
scrollView.contentInset = .init(top: 0.0, left: CGFloat(x), bottom: 0.0, right: scrollView.frame.width - CGFloat(x))
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var cx: Int = Int(scrollView.contentOffset.x)
// offset to the first tick-mark
cx += Int(scrollView.contentInset.left)
let curTick: Int = cx / 20
if prevTickMark != curTick {
// we just passed, or we are on, a new "tick"
// so play the tick sound
AudioServicesPlayAlertSound(SystemSoundID(1057))
prevTickMark = curTick
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论