最快的iOS声音播放方法是什么?

huangapple go评论188阅读模式
英文:

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:

最快的iOS声音播放方法是什么?

and after scrolling a little:

最快的iOS声音播放方法是什么?

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 &quot;tick&quot;
//	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 &quot;tick&quot; lines every 20-points
//	with a taller line every 100-points
bez.move(to: pt)
while pt.x &lt;= 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 &quot;tick-mark&quot; lines every 20-points
let tickView = TickView()
// view with single vertical line
//	overlay on the scroll view so we have a
//	&quot;center-line&quot;
let midLineView = MidLineView()
// track the previous &quot;tick&quot;
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&#39;s make the &quot;tick&quot; 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 &quot;ticks&quot; 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 &quot;tick&quot;
//	so play the tick sound
AudioServicesPlayAlertSound(SystemSoundID(1057))
prevTickMark = curTick
}
}
}

huangapple
  • 本文由 发表于 2023年5月30日 07:52:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76360874.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定