美文网首页
音频可视化

音频可视化

作者: 浪淘沙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