如何实现音频播放速率从1.0到2.0的平滑提升(类似AVAudioPlayer.setVolume方法)
Nice one! I get it, the setVolume(_:fadeDuration:) method is such a clean, built-in way to handle smooth volume fades, so it’s frustrating there’s no equivalent for playback rate ramping out of the box. But don’t worry—we can replicate that exact gradual behavior ourselves using incremental updates over time, just like how Apple’s volume fade works under the hood. Let’s break this down step by step.
The Core Idea
The setVolume fade works by making tiny, frequent adjustments to the volume value until it hits the target. We’ll do the same thing for playback rate: use a repeating timer (or a CADisplayLink for even smoother results) to nudge the rate up in small increments over your desired duration.
Implementation with Timer (Simple Approach)
First, let’s start with a straightforward Timer-based solution that’s easy to understand and integrate:
import AVFoundation class AudioPlayerController: NSObject, AVAudioPlayerDelegate { private var audioPlayer: AVAudioPlayer? private var rateRampTimer: Timer? private var currentRampRate: Float = 1.0 // Customize these values to fit your needs private let targetRate: Float = 2.0 private let rampDuration: TimeInterval = 2.0 // 2 seconds to ramp from 1.0 to 2.0 // Helper to initialize your player (add your own audio file setup here) func setupPlayer(with audioURL: URL) throws { audioPlayer = try AVAudioPlayer(contentsOf: audioURL) audioPlayer?.delegate = self audioPlayer?.prepareToPlay() } // Start the smooth rate ramp func startSmoothRateRamp() { guard let player = audioPlayer, player.isPlaying, player.rate == 1.0 else { print("Player isn't ready to start ramping") return } // Calculate how much to increment the rate each timer tick // We use 60 ticks per second for smoothness (matches screen refresh rate) let totalTicks = rampDuration * 60 let rateIncrement = (targetRate - currentRampRate) / Float(totalTicks) // Clean up any existing timer first rateRampTimer?.invalidate() // Start the repeating timer rateRampTimer = Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { [weak self] timer in guard let self = self else { timer.invalidate() return } // Update the current rate self.currentRampRate += rateIncrement // Check if we've hit or exceeded the target rate if self.currentRampRate >= self.targetRate { self.currentRampRate = self.targetRate timer.invalidate() self.rateRampTimer = nil } // Apply the new rate to the player player.rate = self.currentRampRate } } // Optional: Stop the ramp early if needed func stopRateRamp() { rateRampTimer?.invalidate() rateRampTimer = nil } }
Optimized with CADisplayLink (Smoother Results)
For even more precise, jitter-free ramping, use CADisplayLink instead of a Timer. It syncs with the screen’s refresh rate, so updates happen at the exact same interval as UI refreshes—resulting in an ultra-smooth audio transition:
import AVFoundation import QuartzCore extension AudioPlayerController { private var rateRampDisplayLink: CADisplayLink? private var rateIncrementPerFrame: Float = 0.0 func startSmoothRateRampWithDisplayLink() { guard let player = audioPlayer, player.isPlaying, player.rate == 1.0 else { print("Player isn't ready to start ramping") return } currentRampRate = 1.0 // Calculate increment per screen frame (~1/60th of a second) rateIncrementPerFrame = (targetRate - currentRampRate) / Float(rampDuration * 60) // Clean up existing link rateRampDisplayLink?.invalidate() // Create and start the display link rateRampDisplayLink = CADisplayLink(target: self, selector: #selector(updateRateRamp)) rateRampDisplayLink?.add(to: .main, forMode: .common) } @objc private func updateRateRamp() { guard let player = audioPlayer else { rateRampDisplayLink?.invalidate() rateRampDisplayLink = nil return } currentRampRate += rateIncrementPerFrame if currentRampRate >= targetRate { currentRampRate = targetRate rateRampDisplayLink?.invalidate() rateRampDisplayLink = nil } player.rate = currentRampRate } }
Key Notes to Remember
- Avoid Memory Leaks: Always use
[weak self]in timer closures to prevent strong reference cycles. - Player State Checks: Make sure the player is actually playing before starting the ramp—adjusting rate on a paused player won’t have any effect until playback resumes.
- Customization: Tweak
rampDurationandtargetRateto match your app’s needs. You could even make these parameters dynamic in thestartSmoothRateRampmethod. - Cleanup: Always invalidate timers/display links when they’re no longer needed (e.g., when the player stops, or when the view controller deallocates).
This approach mimics the exact logic behind setVolume(_:fadeDuration:)—small, frequent adjustments that create a seamless, natural-sounding transition instead of a jarring jump from 1.0 to 2.0.
内容的提问来源于stack exchange,提问作者Connor Laganà

