ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Cable Messenger:语音波纹曲线生成策略

2021-06-14 16:32:08  阅读:258  来源: 互联网

标签:Cable self let context PCM Messenger 数据 波纹 CGFloat


文章目录


Cable Messenger 聊天中进行短语音发送时,可以通过对语音文本数据进行实时分析,生成相关的语音波纹起伏曲线。

此篇文章主要为了倡导大家,在项目开发中,要多思考多实践,不要动手就离不开第三方库,没有第三方库就迈不开腿,甚至去找产品经理去改需求。很多东西其实可以自己写自己实现,而且要多了解和学习技术,多了解事物的本身,做项目不是简单的堆第三方库。

波纹数据的生成与分析

PCM(Pulse Code Modulation,脉冲编码调制)音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字音频数据。

如果是单声道的音频文件,采样数据按时间的先后顺序依次存,如果是双声道的话就按照LRLRLR的方式存储,存储的时候与字节序有关。以量化位数为16bit为例,对于双声道的音频文件而言,在每个采样时间间隔内,会同时生成 16 * 2 bit的数字音频数据,以顺序的形式进行存储。

PCM数据,作为设备生成的最原始数据,在进行各种压缩算法和封装格式进行封装后,生成了我们大家所熟知的MP3, AMR 等格式。而当我们接收到各种各样的音频格式,要进行播放前,要反向地对各种格式进行解封装,对相应该压缩后的数据进行算法还原,还原成原始的PCM数据后才能进行播放。因为本编不是对音频格式理论的详细描述,所以只一笔带过。

Cable Messenger 对于短语音文件格式上,采用了AMR 格式进行传输。AMR具有文件小比较适合短语音发送的场景。在具体的选型上,AMR-NB文件更小,AMR-WEB清晰度更高等特点。

在安卓平台上,原生的播放控件已经完美支持AMR下两种格式文件的播放。而在IOS平台上,声称在过去的版本曾经支持过AMR文件格式的播放。但就现时,原生的播放器还是缺少了对于AMR文本进行直接拆封解压生成PCM数据进行播放的能力。于是IOS端在接收到AMR格式文件时,在播放前就对AMR数据进行了自动的转换,生成PCM格式数据,以WAV封装规范对PCM数据进行封装。在播放的时候交由原生音频播放器进行播放。

PCM作为最原始的音频数据,是生成波纹数据的基础。所以第一步我们要做的就是如何分离PCM数据。

在介绍前,先要了解什么是RIFF资源互换文件格式。RIFF文件由一个或多个“块”组成。每个“块”由“块标识”(4Byte)“长度”(4Byte)“数据”(由前面的长度决定)。

就WAV文件而言,它由一个“块标识”值RIFF的“块”进行封装。而在这个“块”内部的“数据”中,由一个标准的块组成,“块标识”值为WAVE

WAVE子块中,又可能存在以fmt(格式信息) data(PCM数据)fact(附加数据)为“块标识”值的三种子块。其中波纹数据就放在 data“块标识”值的的“数据”中。

好了,说到我自己都绕进去了。因为不是一篇理论型的文章,就不长篇大论的说理论了,有兴趣的话,这些资料到处都可以查来。

以下以Objective C 代码为例给出PCM数据的取值方法,因为只关注于PCM数据的取值,其它的块信息就不给出分析代码了

#pragma mark - 分析wav的声纹曲线, 返回声纹数据
+ (nullable NSData*) decodePCM:(nonnull NSString*)path{
    NSData* wavData = [NSData dataWithContentsOfFile:path];
    if wavData == nil{
    	return null;
    }
    
    int index 	 	 = 0;
    int dataSize 	 = 0;
    BOOL enable  	 = NO;
    NSData *RIIFData = nil;
 
    //1. 先判断文件是否是标准RIFF格式
    NSData *dType   = [wavData subdataWithRange:NSMakeRange(0, 4)];
    NSString *sType = [[NSString alloc] initWithData:dType encoding:NSUTF8StringEncoding];
    if([@"RIFF" isEqualToString:fileType]){
        enable = YES;
        //2.取得RIFF数据长度
        int RIIFsize;
        [[wavData subdataWithRange:NSMakeRange(4, 4)] getBytes:&RIIFsize length:sizeof(RIIFsize)];
            
        //判断 是否是 WAVE 格, WAVE格式后面会有 format 和 data chunk
        NSData *dWave   = [wavData subdataWithRange:NSMakeRange(8, 4)];
        NSString *sWave = [[NSString alloc] initWithData:dWave encoding:NSUTF8StringEncoding];
        if ([@"WAVE" isEqualToString:sWave] == NO){
            enable = NO;
        }
        //3.截取 WAVE chunk 数据
        if (RIIFsize > 0){
            RIIFsize = RIIFsize - 4;
            RIIFData = [wavData subdataWithRange:NSMakeRange(12, RIIFsize)];
        }
     }
        
     //3.取得 wav 中的数据内容 
     while (enable && RIIFData != nil && RIIFData.length > 0) {
     	NSData *dData 		= [RIIFData subdataWithRange:NSMakeRange(index, 4)];
        NSString *chunkType = [[NSString alloc] initWithData:dData encoding:NSUTF8StringEncoding];
        
        int chunkSize;  //chunk 的数据长度
        [[RIIFData subdataWithRange:NSMakeRange(index + 4, 4)] getBytes:&chunkSize length:sizeof(chunkSize)];
            
        //找到 data 类型
        if([chunkType isEqualToString:@"data"] == YES){
            dataSize = chunkSize;
            break;
        }
        index = index + 8 + chunkSize;
      }
        
      //4.截取PCM数据返回
      if(dataSize != 0){
          NSData *dPCM = [RIIFData subdataWithRange:NSMakeRange(index + 8, dataSize * sizeof(char))];
          return dPCM;
      }
    return nil;
}

波纹曲线控件核心逻辑

波纹曲线控件展示效果如图:

在这里插入图片描述
在取得PCM数据后,为了把数据呈现到有限长度的控件上,要对PCM数据值进行一定比较的二次采样。

量化位数为16bit为例,简单的采样代码如下:

#pragma mark - 对生成的声纹数据进行二次采样
+ (nullable NSMutableArray*)encodeLineValue:(nonnull NSData*)data offset:(int)offset{
    int size = data.length * 0.5;
    NSMutableArray *yPoins = [[NSMutableArray alloc] init];
    for (int i = 0; i < size; i++) {
        if ((i % offset) == 0){
            int16_t value;
            [[data subdataWithRange:NSMakeRange(i * 2 , 2)] getBytes:&value length:sizeof(value)];
            [yPoins addObject:[NSNumber numberWithInt:value]];
        }
    }
    return yPoins;
}

在控件的实现中,控件的长度与音频的长度成一定的比例关系。波纹曲线的宽度为固定值,曲线在控件中的数量可以通过先计算出长度,再整除运算后得出曲线的个数。

曲线高度的计算会稍微复杂。要先对二次有采样的数据进行扫描,取出绝对值最大的数据作为参考。曲线高度最大值为固定值,先计算出两个值间的比例。然后遍历二次有采样的数据,通过生成的比例值,计算出各个曲线的真实的高度。相关代码如下:

	///
	/// 生成声纹坐标
	///
    public func createPCMLineData(path:String, width:CGFloat) -> Data?{
        ///线条高度最大值
        let maxHeight:CGFloat = self.Max 
        let height:CGFloat    = self.frame.size.height - self.paddingBotton;                           
        var lineCount:Int     = Int(width / (self.lineWidth + self.linePadding))
        lineCount             = lineCount > 0 ? lineCount : 1 
        guard let dPCM:Data = self.decodePCM(path) else{
        	return nil
        } 
        self.points.removeAll()
        ///以16位为例,两个字节为一个单完
        let unitCount:Int = Int(dPCM.count / 2) 
        ///采样间隔               
        let offset:Int32  = Int32(unitCount / lineCount) 
        // 1. ========== 生成二次采样数据 ============
		guard let values:[Int16] = self.encodeLineValue(dPCM, offset:offset) as? [Int16]{ 
			return nil
		}       
        // 2. ============== 取出最大值 =============
        var maxValue:Int16 = 0
        for item in 0..<values.count{
            if abs(values[item]) > maxValue{
               maxValue = Int16(abs(values[item]))
            }
        }      
        // 3. ============ 计算缩放比例 ==============
        let scrol:CGFloat      = maxHeight  / CGFloat(maxValue)
        // 4. ============ 生成线条坐标 ==============
        let totalWidth:CGFloat = self.lineWidth + self.linePadding
        for i in 0..<values.count{
            ///水平方向坐标
            let x:CGFloat     = totalWidth * CGFloat(i + 1) - (self.lineWidth + self.linePadding) * 0.5  
            var value:CGFloat = CGFloat(fabsf(Float(values[i]))) * scrol
            if value < self.Min{  // 最少值为 1
                value = self.Min
            }
            if value >= height * 0.5 - 1{
                value = height * 0.5 - 2
            }
            ///竖直方向坐标
            let y:CGFloat = CGFloat(fabsf(Float(height * 0.5) - Float(value))) 
                self.points.append(CGPoint(x: x, y: y))
            }
         }
         ///生成数据后,主动刷新,触发绘制
         self.setNeedsDisplay()
        }
        ///返回声纹数据
        return NSKeyedArchiver.archivedData(withRootObject:self.points)
    }

波纹曲线从标生成后,就可以进行控件描制逻辑的编写,代码如下:

	override public func draw(_ rect: CGRect) {
        objc_sync_enter(self)
        let width:CGFloat      			= rect.size.width;
        let height:CGFloat 				= rect.size.height;
        let progressBackCGColor:CGColor = self.progressBC.cgColor
        let progressFormCGColor:CGColor = self.progressFC.cgColor
        let defaultCGolor:CGColor 		= self.defaultColor.cgColor
        
        if let context:CGContext = UIGraphicsGetCurrentContext(){
            if self.progress == 0 || self.isAnimationRunning == false{
                context.setStrokeColor(defaultCGolor)
            }else{
                context.setStrokeColor(progressBackCGColor)
            }
            context.setLineWidth(lineWidth)
            for i in 0..<points.count {
                //点设置
                let point:CGPoint = points[i]
                context.move(to: CGPoint(x: point.x, y: point.y))
                context.addLine(to: CGPoint(x: point.x, y: height  - point.y))
                context.strokePath()
            }
            context.saveGState()
            // 2. 设置裁剪区域
            context.beginPath()
            context.addRect(CGRect(x: 0, y: 0, width: width * self.progress, height: height))
            context.closePath()
            context.clip()
            
            // 3. 画【上层进度】浅色层
            context.setStrokeColor(progressFormCGColor)
            context.setLineWidth(lineWidth)
            for i in 0..<points.count {
                //点设置
                let point:CGPoint = points[i]
                context.move(to: CGPoint(x: point.x, y: point.y))
                context.addLine(to: CGPoint(x: point.x, y: height - point.y))
                context.strokePath()
            }
            context.saveGState()
        }
        objc_sync_exit(self)
    }

绘制过程代码仅供参考,因为控件还牵涉到很多操作,如播放中的进度显示,手势左右拖动的进度响应,播放完后的颜色切换等,所以就不一一上完整的代码了。以上代码为绘制的核心逻辑。有时间的话,会整理出一个模块放到我自己的开源项目中去供大家参考。

世上无难事,只怕有心人。很多优秀的第三方库固然很值处我们使用,但更重要的是学会思考,深入探究别人的库是怎么实现的。这才是一个合格程序员的基本修养。

标签:Cable,self,let,context,PCM,Messenger,数据,波纹,CGFloat
来源: https://blog.csdn.net/pritywawa/article/details/117902818

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有