最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - How can I visualize audio data amplitude graphically using UIGraphics iOS - Stack Overflow

programmeradmin1浏览0评论

I want to show an interactive audio waveform like this.

I've extracted the sample data using AVAssetReader. Using this data, I'm drawing a UIBezierPath in a Scrollview's contentView. Currently, when I pinch zoom-in or zoom-out the scrollView, I'm downsampling the sample data to determine how many samples are to be shown.

class WaveformView: UIView {
    var amplitudes: [CGFloat] = [] {
        didSet {
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext(), !amplitudes.isEmpty else { return }

        // Set up drawing parameters
        context.setStrokeColor(UIColor.black.cgColor)
        context.setLineWidth(1.0)
        context.setLineCap(.round)

        let midY = rect.height / 2
        let widthPerSample = rect.width / CGFloat(amplitudes.count)

        // Draw waveform
        let path = UIBezierPath()

        for (index, amplitude) in amplitudes.enumerated() {
            let x = CGFloat(index) * widthPerSample
            let height = amplitude * rect.height * 0.8

            // Draw vertical line for each sample
            path.move(to: CGPoint(x: x, y: midY - height))
            path.addLine(to: CGPoint(x: x, y: midY + height))
        }

        path.stroke()
    }
}

Added gesture handle

@objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) {
        switch gesture.state {
        case .began:
            initialPinchDistance = gesture.scale
            
        case .changed:
            let scaleFactor = gesture.scale / initialPinchDistance
            var newScale = currentScale * scaleFactor
            newScale = min(max(newScale, minScale), maxScale)
            
            // Update displayed samples with new scale
            updateDisplayedSamples(scale: newScale)
            print(newScale)
            // Maintain zoom center point
            let pinchCenter = gesture.location(in: scrollView)
            let offsetX = (pinchCenter.x - scrollView.bounds.origin.x) / scrollView.bounds.width
            let newOffsetX = (totalWidth * offsetX) - (pinchCenter.x - scrollView.bounds.origin.x)
            scrollView.contentOffset.x = max(0, min(newOffsetX, totalWidth - scrollView.bounds.width))
            
            view.layoutIfNeeded()
            
        case .ended, .cancelled:
            currentScale = scrollView.contentSize.width / (baseWidth * widthPerSample)
            
        default:
            break
        }
    }
private func updateDisplayedSamples(scale: CGFloat) {
        let targetSampleCount = Int(baseWidth * scale)
        displayedSamples = downsampleWaveform(samples: rawSamples, targetCount: targetSampleCount)
        waveformView.amplitudes = displayedSamples
        
        totalWidth = CGFloat(displayedSamples.count) * widthPerSample
        contentWidthConstraint?.constant = totalWidth
        scrollView.contentSize = CGSize(width: totalWidth, height: 300)
    }
private func downsampleWaveform(samples: [CGFloat], targetCount: Int) -> [CGFloat] {
        guard samples.count > 0, targetCount > 0 else { return [] }
        
        if samples.count <= targetCount {
            return samples
        }
        
        var downsampled: [CGFloat] = []
        let sampleSize = samples.count / targetCount
        
        for i in 0..<targetCount {
            let startIndex = i * sampleSize
            let endIndex = min(startIndex + sampleSize, samples.count)
            let slice = samples[startIndex..<endIndex]
            
            // For each window, take the maximum value to preserve peaks
            if let maxValue = slice.max() {
                downsampled.append(maxValue)
            }
        }
        
        return downsampled
    }

The following approach works very inefficiently as everytime gesture.state is changed I'm calculating the downsampled data and performs UI operation based on that. How can I implement this functionality more efficiently for smooth interaction?

I want to show an interactive audio waveform like this.

I've extracted the sample data using AVAssetReader. Using this data, I'm drawing a UIBezierPath in a Scrollview's contentView. Currently, when I pinch zoom-in or zoom-out the scrollView, I'm downsampling the sample data to determine how many samples are to be shown.

class WaveformView: UIView {
    var amplitudes: [CGFloat] = [] {
        didSet {
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext(), !amplitudes.isEmpty else { return }

        // Set up drawing parameters
        context.setStrokeColor(UIColor.black.cgColor)
        context.setLineWidth(1.0)
        context.setLineCap(.round)

        let midY = rect.height / 2
        let widthPerSample = rect.width / CGFloat(amplitudes.count)

        // Draw waveform
        let path = UIBezierPath()

        for (index, amplitude) in amplitudes.enumerated() {
            let x = CGFloat(index) * widthPerSample
            let height = amplitude * rect.height * 0.8

            // Draw vertical line for each sample
            path.move(to: CGPoint(x: x, y: midY - height))
            path.addLine(to: CGPoint(x: x, y: midY + height))
        }

        path.stroke()
    }
}

Added gesture handle

@objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) {
        switch gesture.state {
        case .began:
            initialPinchDistance = gesture.scale
            
        case .changed:
            let scaleFactor = gesture.scale / initialPinchDistance
            var newScale = currentScale * scaleFactor
            newScale = min(max(newScale, minScale), maxScale)
            
            // Update displayed samples with new scale
            updateDisplayedSamples(scale: newScale)
            print(newScale)
            // Maintain zoom center point
            let pinchCenter = gesture.location(in: scrollView)
            let offsetX = (pinchCenter.x - scrollView.bounds.origin.x) / scrollView.bounds.width
            let newOffsetX = (totalWidth * offsetX) - (pinchCenter.x - scrollView.bounds.origin.x)
            scrollView.contentOffset.x = max(0, min(newOffsetX, totalWidth - scrollView.bounds.width))
            
            view.layoutIfNeeded()
            
        case .ended, .cancelled:
            currentScale = scrollView.contentSize.width / (baseWidth * widthPerSample)
            
        default:
            break
        }
    }
private func updateDisplayedSamples(scale: CGFloat) {
        let targetSampleCount = Int(baseWidth * scale)
        displayedSamples = downsampleWaveform(samples: rawSamples, targetCount: targetSampleCount)
        waveformView.amplitudes = displayedSamples
        
        totalWidth = CGFloat(displayedSamples.count) * widthPerSample
        contentWidthConstraint?.constant = totalWidth
        scrollView.contentSize = CGSize(width: totalWidth, height: 300)
    }
private func downsampleWaveform(samples: [CGFloat], targetCount: Int) -> [CGFloat] {
        guard samples.count > 0, targetCount > 0 else { return [] }
        
        if samples.count <= targetCount {
            return samples
        }
        
        var downsampled: [CGFloat] = []
        let sampleSize = samples.count / targetCount
        
        for i in 0..<targetCount {
            let startIndex = i * sampleSize
            let endIndex = min(startIndex + sampleSize, samples.count)
            let slice = samples[startIndex..<endIndex]
            
            // For each window, take the maximum value to preserve peaks
            if let maxValue = slice.max() {
                downsampled.append(maxValue)
            }
        }
        
        return downsampled
    }

The following approach works very inefficiently as everytime gesture.state is changed I'm calculating the downsampled data and performs UI operation based on that. How can I implement this functionality more efficiently for smooth interaction?

Share Improve this question asked Jan 30 at 4:14 Ahsan Habib SwassowAhsan Habib Swassow 214 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

To speed up calculations on large vectors of samples you can make use of a dedicated vDSP component of Accelerate framework.

Take a look at vDSP.downsample(_:decimationFactor:filter:result:) function.

In your case:

import Accelerate

func downsample(samples: [Float], targetCount: Int) -> [Float] {
    // Calculate the decimation factor
    let decimationFactor = max(1, samples.count / targetCount)
    
    // Create a result array for the downsampled data
    let downsampledSize = (samples.count + decimationFactor - 1) / decimationFactor
    var downsampledData = [Float](repeating: 0.0, count: downsampledSize)
    
    // Create an identity filter (not really used, but required by the function)
    var filter = [Float](repeating: 1.0, count: 1) // Identity filter
    
    // Use vDSP to downsample the data
    vDSP.downsample(samples, decimationFactor, filter, &downsampledData)
    
    return downsampledData
}
发布评论

评论列表(0)

  1. 暂无评论