在一段音频中我们可以获得其音调的高低来进行音频可视化,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);
网友评论