Mac 录制音频,波形图的绘制

开篇

废话不多说,先上效果图。


animation.gif

上面一闪一闪的大概是没录好吧。

需求分析

首先分析这段动画的过程:

  • 第一阶段:点击图标后,图标像泡泡一样收缩放大;
  • 第二阶段:图标缩小,同时向下方移动;
  • 第三阶段:波形图绘制,随着数据的增多,波形图向左移动,同时视图的宽度越来越长
  • 第四阶段:视图宽度达到一个阈值后不再增大,波形图继续向左移动,超出左侧范围的部分不再显示。

PS. 波形图是上下对称的哦。

实现思路

首先看动画,前两个阶段的动画有先后顺序,第二阶段的两个动画同时发生,可以用动画组来做。

然后,第二、第三阶段之间的节点,可以看到图标刚好缩小到波形视图的高度后,波形视图出现,如果分为两个视图,总有一个生硬的过渡过程,为了达到一个流畅的视觉效果,可以使用一个视图来做。

最后,仔细看波形图,新数据是从右侧进入的,由于这是个录音的过程,因此数据是在不断增多的,可以用实时绘图来实现。

接下来上自定义 button 的代码。

  • 第一阶段 ~ 第二阶段

    - (void)startAnimation{
        self.enabled = NO;  //动画过程中禁用
        [self moveAnchorPointToCenter];   //将锚点移到中心 (为了达到围绕中心缩放的效果)
        
        //放大
        CAKeyframeAnimation *scaleToBigAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
        scaleToBigAnimation.values = @[@(1.0), @(.7f), @(1.f), @(1.3f), @(1.7f)];   //先从1.0缩小到0.7,再放大到1.7,这样就实现了泡泡效果
        scaleToBigAnimation.duration = 0.5;
        scaleToBigAnimation.beginTime = 0;
        
        //缩小
        CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
        scaleAnimation.values = @[@(1.7f), @(1)];
        scaleAnimation.duration = 0.75;
        scaleAnimation.beginTime = scaleToBigAnimation.beginTime + scaleToBigAnimation.duration;
        
        //位置下移,与缩小动画同时进行
        CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position.y"];
        positionAnimation.toValue = @(self.layer.position.y - NSWidth(self.frame));
        positionAnimation.duration = scaleAnimation.duration;
        positionAnimation.beginTime = scaleAnimation.beginTime;
        
        //添加动画组
        CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
        animationGroup.delegate = self;
        animationGroup.duration = scaleToBigAnimation.duration + positionAnimation.duration;
        [animationGroup setValue:@"animationGroup" forKey:@"AnimationKey"];
        animationGroup.animations = @[scaleToBigAnimation, scaleAnimation, positionAnimation];
        [self.layer addAnimation:animationGroup forKey:@"animationGroup"];
    }
    
    - (void)moveAnchorPointToCenter{
        //由于图层锚点默认是在原点(0,0),需要让图层围绕中心点缩放
        self.layer.anchorPoint = CGPointMake(0.5, 0.5);
        
        //锚点改变后,为了让图层随着视图移动,将图层的位置也改到锚点的位置
        NSRect rect = self.frame;
        CGFloat centerX = rect.origin.x + rect.size.width / 2.f;
        CGFloat centerY = rect.origin.y + rect.size.height / 2.f;
        self.layer.position = CGPointMake(centerX, centerY);
    }
    
    - (void)resumeAnchorPoint{
        self.layer.anchorPoint = CGPointZero;
        self.layer.position = self.frame.origin;
    }
    

    动画结束后,在代理里触发下一步操作。

    #pragma mark - CAAnimation delegate
    - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
        if (flag) {
            if ([self.delegate respondsToSelector:@selector(voiceRecordingWillBegin)]) {
                  //执行代理方法,准备数据
                [self.delegate voiceRecordingWillBegin];
            }
            [self resumeAnchorPoint];
            [self startRecording];
            
        }
    }
    
  • 第三阶段 ~ 第四阶段

    本文 demo 里所有的音频数据都是随机生成的模拟数据,也没有实现真正的录音哈。

    控制器在 voiceRecordingWillBegin 这个方法里,准备需要绘制的数据。

    #pragma mark - voice recording button delegate
    //录音即将开始
    - (void)voiceRecordingWillBegin{
          //之前的动画只是图层动画,图层已经到了目标位置,但视图的 frame 还在原来的位置,因此要修改视图的位置和尺寸
        NSRect frame = self.voiceRecordBtn.frame;
        CGFloat centerX = frame.origin.x + frame.size.width / 2.f;
        CGFloat centerY = frame.origin.y + frame.size.height / 2.f - frame.size.height;
        frame.size.width = 400; //这是蓝色波形图的最大宽度
        frame.origin.x = centerX - frame.size.width / 2.f;
        frame.origin.y = centerY - frame.size.height / 2.f;
        self.voiceRecordBtn.frame = frame;
        
          //添加计时器,构造模拟数据
        [self addTimer];
    }
    
    - (void)addTimer{
        //添加定时器
        _timer = [NSTimer scheduledTimerWithTimeInterval:.1f target:self selector:@selector(addPoint) userInfo:nil repeats:YES];
    
        [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)addPoint
    {
        //随机点
         NSPoint point = NSMakePoint(self.voiceRecordBtn.bounds.size.height / 2.f, arc4random_uniform(NSHeight(self.voiceRecordBtn.frame) / 4.f) + 0);
        
        //插入到数组(动画视图最右边),array添加CGPoint需要转换一下
    //    [self.pointArray insertObject:[NSValue valueWithPoint:point] atIndex:0];
        [self.pointArray addObject:[NSValue valueWithPoint:point]];
        
        //传值,重绘视图
        self.voiceRecordBtn.pointArray = self.pointArray;
    }
    

    回到自定义按钮的.m 文件

    点数组的 setter 方法

    - (void)setPointArray:(NSArray *)pointArray{
        _pointArray = pointArray;
        [self setNeedsDisplay:YES];
    }
    

    开始录音

    - (void)startRecording{
        self.talking = YES;
        [self setNeedsDisplay:YES];
        
        //延迟_recordingDuration执行,若没有手动停止,则自动停止录音
        [self performSelector:@selector(stopRecording) withObject:nil afterDelay:_recordingDuration];
    }
    

    停止录音

    - (void)stopRecording{
        //取消延迟执行
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(stopRecording) object:nil];
        
        self.enabled = YES;
        self.frame = self.initialFrame; //录音结束后,按钮回到点击前的初始状态
        self.talking = NO;
        
        [self setNeedsDisplay:YES];
        
        if ([self.delegate respondsToSelector:@selector(voiceRecordingDidFinish)]) {
            [self.delegate voiceRecordingDidFinish];
        }
    }
    

    以下是重头戏了,每次调用[self setNeedsDisplay:YES]方法时,系统会自动调用drawRect:(NSRect)dirtyRect 方法,我们在这个方法里绘制数据。

    - (void)drawRect:(NSRect)dirtyRect{
        [super drawRect:dirtyRect];
        
        if (!self.talking) {
            //现在没有在录音,即初始状态
            NSBezierPath *rectPath = [NSBezierPath bezierPathWithOvalInRect:dirtyRect];
            [[NSColor blueColor] setFill];
            [rectPath fill];
            
            NSImage *image = [NSImage imageNamed:@"SideAudio"];
            [image drawInRect:dirtyRect];
            return;
        }
        
        CGFloat midY = NSHeight(dirtyRect) / 2.f;
        CGFloat midX = NSWidth(dirtyRect) / 2.f;
        CGFloat leftX = midX - _pointArray.count / 2.f - _initialWidth / 2.f;
        CGFloat rightX = midX + _pointArray.count / 2.f + _initialWidth / 2.f;
        
        // Drawing code here.
        CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
        
        //绘制初始线型,模拟一般录音场景,刚开始可能没有说话,一条横线
        CGMutablePathRef linePath = CGPathCreateMutable();
        CGPathMoveToPoint(linePath, nil, leftX, midY);
        CGPathAddLineToPoint(linePath, nil, leftX + _initialWidth, midY); //_initialWidth 横线的宽度,这里给了个固定值
        CGContextAddPath(ctx, linePath);
        
        //绘制上半部分波形
        CGMutablePathRef halfPath = CGPathCreateMutable();                 //绘制路径
        CGPathMoveToPoint(halfPath, nil, NSWidth(dirtyRect), midY);
        for (NSInteger i = 0; i < _pointArray.count; i++) {
            NSValue *pointValue = _pointArray[i];
            NSPoint point = pointValue.pointValue;
            NSInteger j = _pointArray.count - i - 1;
            if (point.y == 0) {
                CGPathMoveToPoint(halfPath, nil, rightX - j + 1, midY);
                CGPathAddLineToPoint(halfPath, NULL, rightX - j, midY);
            }else{
                CGPathMoveToPoint(halfPath, nil, rightX - j, midY);
                CGPathAddLineToPoint(halfPath, NULL, rightX - j, midY + point.y);
            }
        }
        
        //实现波形图反转
        CGMutablePathRef fullPath = CGPathCreateMutable();//创建新路径
        CGPathAddPath(fullPath, NULL, halfPath);          //合并路径
        CGAffineTransform transform = CGAffineTransformIdentity; //反转
        //反转配置
        transform = CGAffineTransformTranslate(transform, 0, NSHeight(dirtyRect));
        transform = CGAffineTransformScale(transform, 1.0, -1.0);
        CGPathAddPath(fullPath, &transform, halfPath);
        
        //将路径添加到上下文中
        CGContextAddPath(ctx, fullPath);
        
        //绘制矩形区域,即不断变长的蓝色背景
        CGMutablePathRef rectPath = CGPathCreateMutable();
        CGPathMoveToPoint(rectPath, nil, leftX, 0);
        CGPathAddRoundedRect(rectPath, nil, CGRectMake(leftX, 0, _pointArray.count + _initialWidth, NSHeight(dirtyRect)), NSHeight(dirtyRect) / 2.f, NSHeight(dirtyRect) / 2.f);
        CGContextAddPath(ctx, rectPath);
        
        CGContextSetLineWidth(ctx, 1);
        CGContextSetStrokeColorWithColor(ctx, [NSColor whiteColor].CGColor);
        CGContextSetFillColorWithColor(ctx, [NSColor blueColor].CGColor);
        CGContextDrawPath(ctx, kCGPathFillStroke);
        
        //移除
        CGPathRelease(halfPath);
        CGPathRelease(fullPath);
    
    }
    

结束,明天放播放录音的动画实现。

Demo 地址:https://github.com/YunFei2015/AudioWaveAnimation.git

你可能感兴趣的:(Mac 录制音频,波形图的绘制)