2026-01-16 13:38:03 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBAiWaveformView.m
|
|
|
|
|
|
// keyBoard
|
|
|
|
|
|
//
|
|
|
|
|
|
// Created by Mac on 2026/1/15.
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBAiWaveformView.h"
|
|
|
|
|
|
|
|
|
|
|
|
@interface KBAiWaveformView ()
|
|
|
|
|
|
@property(nonatomic, strong) NSMutableArray<CAShapeLayer *> *barLayers;
|
|
|
|
|
|
@property(nonatomic, strong) NSMutableArray<NSNumber *> *barHeights;
|
|
|
|
|
|
@property(nonatomic, strong) CADisplayLink *displayLink;
|
|
|
|
|
|
@property(nonatomic, assign) float currentRMS;
|
|
|
|
|
|
@property(nonatomic, assign) float targetRMS;
|
|
|
|
|
|
@property(nonatomic, assign) BOOL isAnimating;
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBAiWaveformView
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
|
|
|
|
self = [super initWithFrame:frame];
|
|
|
|
|
|
if (self) {
|
|
|
|
|
|
[self setup];
|
|
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
|
|
|
|
self = [super initWithCoder:coder];
|
|
|
|
|
|
if (self) {
|
|
|
|
|
|
[self setup];
|
|
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setup {
|
|
|
|
|
|
_waveColor = [UIColor systemBlueColor];
|
|
|
|
|
|
_barCount = 5;
|
|
|
|
|
|
_barWidth = 4;
|
|
|
|
|
|
_barSpacing = 3;
|
|
|
|
|
|
_barLayers = [[NSMutableArray alloc] init];
|
|
|
|
|
|
_barHeights = [[NSMutableArray alloc] init];
|
|
|
|
|
|
_currentRMS = 0;
|
|
|
|
|
|
_targetRMS = 0;
|
|
|
|
|
|
|
|
|
|
|
|
self.backgroundColor = [UIColor clearColor];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)layoutSubviews {
|
|
|
|
|
|
[super layoutSubviews];
|
|
|
|
|
|
[self setupBars];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setupBars {
|
|
|
|
|
|
// 移除旧的图层
|
|
|
|
|
|
for (CAShapeLayer *layer in self.barLayers) {
|
|
|
|
|
|
[layer removeFromSuperlayer];
|
|
|
|
|
|
}
|
|
|
|
|
|
[self.barLayers removeAllObjects];
|
|
|
|
|
|
[self.barHeights removeAllObjects];
|
|
|
|
|
|
|
|
|
|
|
|
// 计算总宽度
|
|
|
|
|
|
CGFloat totalWidth =
|
|
|
|
|
|
self.barCount * self.barWidth + (self.barCount - 1) * self.barSpacing;
|
|
|
|
|
|
CGFloat startX = (self.bounds.size.width - totalWidth) / 2;
|
|
|
|
|
|
CGFloat maxHeight = self.bounds.size.height;
|
|
|
|
|
|
CGFloat minHeight = maxHeight * 0.2;
|
|
|
|
|
|
|
|
|
|
|
|
for (NSInteger i = 0; i < self.barCount; i++) {
|
|
|
|
|
|
CAShapeLayer *barLayer = [CAShapeLayer layer];
|
|
|
|
|
|
barLayer.fillColor = self.waveColor.CGColor;
|
|
|
|
|
|
barLayer.cornerRadius = self.barWidth / 2;
|
|
|
|
|
|
|
|
|
|
|
|
CGFloat x = startX + i * (self.barWidth + self.barSpacing);
|
|
|
|
|
|
CGFloat height = minHeight;
|
2026-02-02 19:07:00 +08:00
|
|
|
|
if (self.barHeightPattern.count > i) {
|
|
|
|
|
|
CGFloat base = [self.barHeightPattern[i] floatValue];
|
|
|
|
|
|
base = MIN(MAX(base, 0.15), 0.9);
|
|
|
|
|
|
height = MAX(minHeight, maxHeight * base);
|
|
|
|
|
|
}
|
2026-01-16 13:38:03 +08:00
|
|
|
|
CGFloat y = (maxHeight - height) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
barLayer.frame = CGRectMake(x, y, self.barWidth, height);
|
|
|
|
|
|
barLayer.backgroundColor = self.waveColor.CGColor;
|
|
|
|
|
|
|
|
|
|
|
|
[self.layer addSublayer:barLayer];
|
|
|
|
|
|
[self.barLayers addObject:barLayer];
|
|
|
|
|
|
[self.barHeights addObject:@(height)];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Public Methods
|
|
|
|
|
|
|
|
|
|
|
|
- (void)updateWithRMS:(float)rms {
|
|
|
|
|
|
self.targetRMS = MIN(MAX(rms, 0), 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)startIdleAnimation {
|
|
|
|
|
|
if (self.isAnimating)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
self.isAnimating = YES;
|
|
|
|
|
|
self.displayLink =
|
|
|
|
|
|
[CADisplayLink displayLinkWithTarget:self
|
|
|
|
|
|
selector:@selector(updateAnimation)];
|
|
|
|
|
|
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
|
|
|
|
|
|
forMode:NSRunLoopCommonModes];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)stopAnimation {
|
|
|
|
|
|
self.isAnimating = NO;
|
|
|
|
|
|
[self.displayLink invalidate];
|
|
|
|
|
|
self.displayLink = nil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)reset {
|
|
|
|
|
|
self.currentRMS = 0;
|
|
|
|
|
|
self.targetRMS = 0;
|
|
|
|
|
|
[self updateBarsWithRMS:0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Animation
|
|
|
|
|
|
|
|
|
|
|
|
- (void)updateAnimation {
|
|
|
|
|
|
// 平滑过渡到目标 RMS
|
|
|
|
|
|
CGFloat smoothing = 0.3;
|
|
|
|
|
|
self.currentRMS =
|
|
|
|
|
|
self.currentRMS + (self.targetRMS - self.currentRMS) * smoothing;
|
|
|
|
|
|
|
|
|
|
|
|
[self updateBarsWithRMS:self.currentRMS];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)updateBarsWithRMS:(float)rms {
|
|
|
|
|
|
CGFloat maxHeight = self.bounds.size.height;
|
|
|
|
|
|
CGFloat minHeight = maxHeight * 0.2;
|
|
|
|
|
|
CGFloat range = maxHeight - minHeight;
|
|
|
|
|
|
|
|
|
|
|
|
// 为每个条添加略微不同的高度和相位
|
|
|
|
|
|
NSTimeInterval time = CACurrentMediaTime();
|
|
|
|
|
|
|
|
|
|
|
|
for (NSInteger i = 0; i < self.barLayers.count; i++) {
|
|
|
|
|
|
CAShapeLayer *layer = self.barLayers[i];
|
|
|
|
|
|
|
|
|
|
|
|
// 添加基于时间的波动效果
|
|
|
|
|
|
CGFloat phase = (CGFloat)i / self.barLayers.count * M_PI * 2;
|
|
|
|
|
|
CGFloat wave = sin(time * 3 + phase) * 0.3 + 0.7; // 0.4 - 1.0
|
|
|
|
|
|
|
2026-02-02 19:07:00 +08:00
|
|
|
|
CGFloat baseFactor = 0.2;
|
|
|
|
|
|
if (self.barHeightPattern.count > i) {
|
|
|
|
|
|
baseFactor = [self.barHeightPattern[i] floatValue];
|
|
|
|
|
|
baseFactor = MIN(MAX(baseFactor, 0.15), 0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 计算高度:基准高度 + 随 RMS 波动
|
|
|
|
|
|
CGFloat dynamicFactor = rms * (0.35 + 0.15 * wave); // 0.35~0.5
|
|
|
|
|
|
CGFloat heightFactor = MIN(1.0, baseFactor + dynamicFactor * (1.0 - baseFactor));
|
|
|
|
|
|
CGFloat height = maxHeight * heightFactor;
|
2026-01-16 13:38:03 +08:00
|
|
|
|
height = MAX(minHeight, MIN(maxHeight, height));
|
|
|
|
|
|
|
|
|
|
|
|
// 更新位置
|
|
|
|
|
|
CGFloat y = (maxHeight - height) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
[CATransaction begin];
|
|
|
|
|
|
[CATransaction setDisableActions:YES];
|
|
|
|
|
|
layer.frame = CGRectMake(layer.frame.origin.x, y, self.barWidth, height);
|
|
|
|
|
|
[CATransaction commit];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)dealloc {
|
|
|
|
|
|
[self stopAnimation];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|