美文网首页
音频可视化

音频可视化

作者: 浪淘沙008 | 来源:发表于2023-04-19 09:38 被阅读0次

    在一段音频中我们可以获得其音调的高低来进行音频可视化,AVFoundation为我们提供了获取音频信息的相应api,如下可将音频中的数据进行逐段分解以获取相应时间的音调高低

    + (void)loadAudioSamplesFromAsset:(AVAsset *)asset
                      completionBlock:(SampleDataCompletionBlock)completionBlock {
        
        NSString *tracks = @"tracks";
        // 对资源所需的键执行标准的异步载入操作,这样在访问资源的tracks属性时就不会遇到阻碍
        [asset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler:^{   // 1
            
            AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];
            
            NSData *sampleData = nil;
            
            if (status == AVKeyValueStatusLoaded) {                             // 2
                // 当tracks键成功载入,则调用readAudioSamplesFromAsset方法从资源音轨中读取样本
                sampleData = [self readAudioSamplesFromAsset:asset];
            }
            
            // 由于载入操作可能发生在任意后台队列上,所以要返回主队列,并携带检索到的音频样本
            dispatch_async(dispatch_get_main_queue(), ^{                        // 3
                completionBlock(sampleData);
            });
        }];
        
    }
    
    + (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {
        
        NSError *error = nil;
        
        // 创建一个新的AVAssetReader实例,并赋给它一个资源来读取。,初始化失败时则报错返回
        AVAssetReader *assetReader =                                            // 1
            [[AVAssetReader alloc] initWithAsset:asset error:&error];
        
        if (!assetReader) {
            NSLog(@"Error creating asset reader: %@", [error localizedDescription]);
            return nil;
        }
        
        // 获取资源中找到的第一个音频轨道,包含在实例项目中的音频文件中只包函有一个轨道,不过最好总是根据期望的媒体类型获取轨道
        AVAssetTrack *track =                                                   // 2
            [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
        
        // 创建一个Dic来保存从资源轨道读取音频样本时使用的解压设置。样本需要以未压缩的格式被读取,所以需要制定kAudioFormatLinearPCM作为格式键。
        // 我们还希望以16位、little-endian字节顺序的有符号整形方式读取。其余设置可以在AVAudioSetting中查找
        NSDictionary *outputSettings = @{                                       // 3
            AVFormatIDKey               : @(kAudioFormatLinearPCM),
            AVLinearPCMIsBigEndianKey   : @NO,
            AVLinearPCMIsFloatKey        : @NO,
            AVLinearPCMBitDepthKey        : @(16)
        };
        
        // 创建一个新的AVAssetRenderTrackOutput实例,并将上一步创建的输出设置传递给它,将其作为AVAssetRender的输出并调用startRending来允许资源读取器开始预收取样本数据
        AVAssetReaderTrackOutput *trackOutput =                                 // 4
            [[AVAssetReaderTrackOutput alloc] initWithTrack:track
                                             outputSettings:outputSettings];
        
        [assetReader addOutput:trackOutput];
        
        [assetReader startReading];
        
        NSMutableData *sampleData = [NSMutableData data];
        
        while (assetReader.status == AVAssetReaderStatusReading) {
            // 调用跟踪输出的copyNextSampleBuffer方法开始每个迭代,每次都返回一个包含音频样本的下一个可用样本buffer
            CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];// 5
            
            if (sampleBuffer) {
                // CMSampleBufferRef中的音频样本被包含在一个CMBlockBufferRef类型中,通过CMSampleBufferGetDataBuffer函数访问block buffer。
                CMBlockBufferRef blockBufferRef =                               // 6
                    CMSampleBufferGetDataBuffer(sampleBuffer);
                
                size_t length = CMBlockBufferGetDataLength(blockBufferRef);
                SInt16 sampleBytes[length];
                CMBlockBufferCopyDataBytes(blockBufferRef,                      // 7
                                           0,
                                           length,
                                           sampleBytes);
                
                [sampleData appendBytes:sampleBytes length:length];
                
                CMSampleBufferInvalidate(sampleBuffer);                         // 8
                CFRelease(sampleBuffer);
            }
        }
        
        if (assetReader.status == AVAssetReaderStatusCompleted) {               // 9
            return sampleData;
        } else {
            NSLog(@"Failed to read audio samples from asset");
            return nil;
        }
    }
    

    由于获取音频声调的样本很多,可以通过设置数量以及展示时的最大值来进行过滤及转化,过滤代码如下:

    - (id)initWithData:(NSData *)sampleData {
        self = [super init];
        if (self) {
            _sampleData = sampleData;
        }
        return self;
    }
    
    - (NSArray *)filteredSamplesForSize:(CGSize)size {
    
        NSMutableArray *filteredSamples = [[NSMutableArray alloc] init];        // 1
        // 根据数据的大小计算出数据取样数
        NSUInteger sampleCount = self.sampleData.length / sizeof(SInt16);
        // 由于数据过多,需要根据界面宽度获取可展示的数据量,然后根据步幅来获取对应数据
        NSUInteger binSize = sampleCount / size.width;
    
        SInt16 *bytes = (SInt16 *) self.sampleData.bytes;
        
        SInt16 maxSample = 0;
        
        for (NSUInteger i = 0; i < sampleCount; i += binSize) {
    
            SInt16 sampleBin[binSize];
    
            for (NSUInteger j = 0; j < binSize; j++) {                          // 2
                // 通过偏移获取相应数据的大小
                sampleBin[j] = CFSwapInt16LittleToHost(bytes[i + j]);
            }
            
            // 获取样本数据中的最大值
            SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize];     // 3
            [filteredSamples addObject:@(value)];
    
            if (value > maxSample) {                                            // 4
                maxSample = value;
            }
        }
    
        CGFloat scaleFactor = (size.height / 2) / maxSample;                    // 5
    
        for (NSUInteger i = 0; i < filteredSamples.count; i++) {                // 6
            filteredSamples[i] = @([filteredSamples[i] integerValue] * scaleFactor);
        }
    
        return filteredSamples;
    }
    
    - (SInt16)maxValueInArray:(SInt16[])values ofSize:(NSUInteger)size {
        SInt16 maxValue = 0;
        for (int i = 0; i < size; i++) {
            if (abs(values[i]) > maxValue) {
                maxValue = abs(values[i]);
            }
        }
        return maxValue;
    }
    

    将波形以一个圆形半径大小的方式展示在空间中,代码如下:

    import UIKit
    import AVFoundation
    import ARKit
    
    class ViewController: UIViewController, AVAudioPlayerDelegate {
        var player:AVAudioPlayer!
        var scnView: ARSCNView!
        var planMaterial:SCNMaterial!
        var arr:NSArray?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let url = Bundle.main.url(forResource: "2", withExtension: "mp3")
            do {
                self.player = try AVAudioPlayer.init(contentsOf: url!)
            } catch {
                fatalError("player init failed")
            }
            self.player.prepareToPlay()
            self.player.delegate = self
            self.player.play()
            self.player.numberOfLoops = 1000;
            print("音轨数:\(self.player.numberOfChannels)")
            
            let asset = AVURLAsset(url: url!)
            SampleDataProvider.loadAudioSamples(from: asset) { data in
                let filter = SampleDataFilter.init(data: data!)
                self.arr = filter.filteredSamples(for: CGSize(width: 1000, height: 1)) as NSArray
            }
            
            let displayLink = CADisplayLink(target: self, selector: #selector(refershFunction))
            displayLink.add(to: .current, forMode: .common)
        }
         
        @objc func refershFunction() {
            guard let arr = self.arr else {
                return
            }
            let v = arr[Int(floor((self.player.currentTime / self.player.duration) * 1000))]
            print((v as AnyObject).floatValue as Any)
            planMaterial.setValue((v as AnyObject).floatValue / 10.0, forKey: "audioValue")
        }
        
        override func viewWillLayoutSubviews() {
            super.viewWillLayoutSubviews()
            self.scnView = ARSCNView(frame: view.frame)
            
            // 设置配置模式为WorldTracking
            let config = ARWorldTrackingConfiguration()
            config.planeDetection = .horizontal // 设置识别水平平面
            self.scnView.session.run(config)
            self.view.addSubview(self.scnView)
            
            let orthographicCameraNode = SCNNode()
            orthographicCameraNode.camera = SCNCamera()
            orthographicCameraNode.camera?.usesOrthographicProjection = true
            orthographicCameraNode.position = SCNVector3(0, 0, 1)
            makePlane()
        }
        
        // 创建展示材质的平面
        func makePlane() {
            let plane = SCNPlane(width: 0.5, height: 0.5)
            
            let planeNode = SCNNode(geometry: plane)
            planMaterial = planeNode.geometry!.firstMaterial
            planeNode.position = SCNVector3(0, -0.5, -1)
            setDefaultShader()
            self.scnView.scene.rootNode.addChildNode(planeNode)
        }
        
        func setDefaultShader() {
            let mapFragment = try! String(contentsOf: Bundle.main.url(forResource: "AudioVisualizationFragment", withExtension: "shader")!, encoding: String.Encoding.utf8)
            let shaderModifiers = [SCNShaderModifierEntryPoint.fragment:mapFragment];
            planMaterial?.shaderModifiers = shaderModifiers
            
        }
    
    }
    

    相应的shader代码

    uniform float audioValue;
    
    #pragma transparent | opaque
    
    #pragma body
             
    vec2 coord = fract(_surface.diffuseTexcoord);
    
    vec2 st = coord - 0.5;
    float len = length(st);
    float a = 0.0;
    if(len < audioValue + 0.1) {
        a = 1.0;
    }
    
    _output.color.rgba = vec4(vec3(1.0), a);
    
    

    项目地址

    相关文章

      网友评论

          本文标题:音频可视化

          本文链接:https://www.haomeiwen.com/subject/lfxkjdtx.html