import { IjkMediaPlayer } from '@ohos/ijkplayer' import { PreferencesUtil } from '@pura/harmony-utils' import { FFTAnalyzer } from '../../common/util/FFTAnalyzer' import { SpectrumRenderer } from './SpectrumRenderer' import { SpectrumEffectDefinition, findSpectrumEffectById, fromLegacySpectrumMode, getDefaultSpectrumEffectId, getSpectrumEffects, normalizeSpectrumEffectId } from './SpectrumEffectRegistry' import { normalizeHexColor } from './SpectrumRenderer' import Logger from '../../common/util/Logger' const TAG: string = 'heanup-SpectrumView' const BAND_COUNT: number = 48 // 频段数量 const PREFERENCE_SELECTED_EFFECT: string = 'selectedSpectrumEffectId' @Component export struct SpectrumView { @Prop @Watch('onPlayingChange') isPlaying: boolean = false @StorageProp('themeColor') themeColor: string = '#FF0050' @Prop @Watch('onVisibilityChange') isVisible: boolean = false @StorageProp('spectrumMode') @Watch('onSpectrumModeChange') spectrumMode: number = 0 @State private selectedEffectId: string = getDefaultSpectrumEffectId() @State private canvasWidth: number = 0 @State private canvasHeight: number = 0 private readonly effects: SpectrumEffectDefinition[] = getSpectrumEffects() private readonly renderSettings: RenderingContextSettings = new RenderingContextSettings(true) private readonly canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderSettings) private readonly fftAnalyzer: FFTAnalyzer = new FFTAnalyzer() private readonly renderers: Map = new Map() private timerId: number = -1 private currentFps: number = 30 private complexityScale: number = 1.0 private highLoadStreak: number = 0 private lowLoadStreak: number = 0 onVisibilityChange(): void { this.updateRenderingState() } onPlayingChange(): void { this.updateRenderingState() } onSpectrumModeChange(): void { const effects = getSpectrumEffects(); if (this.spectrumMode >= 0 && this.spectrumMode < effects.length) { const targetEffectId = effects[this.spectrumMode].effectId; if (targetEffectId !== this.selectedEffectId) { this.switchEffect(targetEffectId); } } } aboutToAppear(): void { this.ensureRenderersReady() this.restoreSelectedEffect() this.resetPerformanceGuard() this.updateRenderingState() Logger.info(TAG, 'SpectrumView aboutToAppear, effect=' + this.selectedEffectId) } aboutToDisappear(): void { this.stopRendering() } private ensureRenderersReady(): void { if (this.renderers.size > 0) { return } for (let i = 0; i < this.effects.length; i++) { const effect: SpectrumEffectDefinition = this.effects[i] this.renderers.set(effect.effectId, effect.createRenderer()) } } private restoreSelectedEffect(): void { // 首先尝试从 Preferences 读取保存的索引 const storedIndex: number = PreferencesUtil.getNumberSync('spectrumModeIndex', -1) if (storedIndex >= 0 && storedIndex < this.effects.length) { this.selectedEffectId = this.effects[storedIndex].effectId Logger.info(TAG, 'Restored spectrum mode from preferences: ' + storedIndex + ' (' + this.selectedEffectId + ')') return } // 其次尝试从 AppStorage 读取 spectrumMode if (this.spectrumMode >= 0 && this.spectrumMode < this.effects.length) { this.selectedEffectId = this.effects[this.spectrumMode].effectId Logger.info(TAG, 'Restored spectrum mode from AppStorage: ' + this.spectrumMode + ' (' + this.selectedEffectId + ')') return } // 再次尝试从 Preferences 读取 effectId const storedEffectId: string = PreferencesUtil.getStringSync(PREFERENCE_SELECTED_EFFECT, '') if (storedEffectId) { this.selectedEffectId = normalizeSpectrumEffectId(storedEffectId) Logger.info(TAG, 'Restored spectrum effectId from preferences: ' + this.selectedEffectId) return } // 使用默认值 this.selectedEffectId = getDefaultSpectrumEffectId() Logger.info(TAG, 'Using default spectrum effect: ' + this.selectedEffectId) } private startRendering(): void { if (this.timerId !== -1) { return } const intervalMs: number = Math.max(16, Math.floor(1000 / this.currentFps)) Logger.info(TAG, 'startRendering fps=' + this.currentFps + ', interval=' + intervalMs) this.timerId = setInterval(() => { this.renderFrame() }, intervalMs) } private restartRenderingIfNeeded(nextFps: number): void { if (nextFps === this.currentFps) { return } this.currentFps = nextFps if (this.timerId === -1) { return } clearInterval(this.timerId) this.timerId = -1 this.startRendering() } private stopRendering(): void { if (this.timerId !== -1) { clearInterval(this.timerId) this.timerId = -1 Logger.info(TAG, 'stopRendering') } } private getCurrentEffect(): SpectrumEffectDefinition { const effect: SpectrumEffectDefinition | undefined = findSpectrumEffectById(this.selectedEffectId) if (effect) { return effect } return this.effects[0] } private getCurrentRenderer(): SpectrumRenderer { const effectId: string = this.getCurrentEffect().effectId const renderer: SpectrumRenderer | undefined = this.renderers.get(effectId) if (renderer) { return renderer } return this.renderers.get(getDefaultSpectrumEffectId()) as SpectrumRenderer } private renderFrame(): void { if (this.canvasWidth <= 0 || this.canvasHeight <= 0) { return } const player: IjkMediaPlayer = IjkMediaPlayer.getInstance() const pcmData: number[] = player.getSpectrumData() if (pcmData.length === 0) { return } const effect: SpectrumEffectDefinition = this.getCurrentEffect() const beginMs: number = Date.now() const spectrumData = this.fftAnalyzer.analyze(pcmData, 44100) const scaledBandCount: number = Math.max(8, Math.floor(effect.performance.maxBandCount * this.complexityScale)) const rawBands: number[] = spectrumData.getBands(Math.min(BAND_COUNT, scaledBandCount)) const bands: number[] = this.fftAnalyzer.smooth(rawBands) const energy: number = spectrumData.getEnergy() const renderer: SpectrumRenderer = this.getCurrentRenderer() const brandColor: string = normalizeHexColor(this.themeColor, '#FF0050') renderer.render( this.canvasContext, bands, energy, this.canvasWidth, this.canvasHeight, brandColor, { complexityScale: this.complexityScale } ) const renderCostMs: number = Date.now() - beginMs this.updatePerformanceGuard(effect, renderCostMs) } private updatePerformanceGuard(effect: SpectrumEffectDefinition, renderCostMs: number): void { const frameBudget: number = 1000 / this.currentFps if (renderCostMs > frameBudget * 1.15) { this.highLoadStreak += 1 this.lowLoadStreak = 0 if (this.highLoadStreak >= 6) { this.highLoadStreak = 0 this.complexityScale = Math.max(effect.performance.complexityFloor, this.complexityScale - 0.1) const nextFps: number = Math.max(effect.performance.minFps, this.currentFps - 2) this.restartRenderingIfNeeded(nextFps) } return } if (renderCostMs < frameBudget * 0.65) { this.lowLoadStreak += 1 this.highLoadStreak = 0 if (this.lowLoadStreak >= 14) { this.lowLoadStreak = 0 this.complexityScale = Math.min(1.0, this.complexityScale + 0.05) const nextFps: number = Math.min(effect.performance.targetFps, this.currentFps + 1) this.restartRenderingIfNeeded(nextFps) } return } this.highLoadStreak = 0 this.lowLoadStreak = 0 } private updateRenderingState(): void { if (this.isVisible && this.isPlaying) { this.startRendering() } else { this.stopRendering() } } private resetPerformanceGuard(): void { const effect: SpectrumEffectDefinition = this.getCurrentEffect() this.currentFps = effect.performance.targetFps this.complexityScale = 1.0 this.highLoadStreak = 0 this.lowLoadStreak = 0 } private persistLegacyMode(): void { let modeIndex: number = 0 for (let i = 0; i < this.effects.length; i++) { if (this.effects[i].effectId === this.selectedEffectId) { modeIndex = i break } } // 同时保存到 AppStorage 和 Preferences AppStorage.setOrCreate('spectrumMode', modeIndex) PreferencesUtil.put('spectrumModeIndex', modeIndex) Logger.info(TAG, 'Persisted spectrum mode index: ' + modeIndex) } private switchEffect(effectId: string): void { const normalizedId: string = normalizeSpectrumEffectId(effectId) if (normalizedId === this.selectedEffectId) { return } const previousRenderer: SpectrumRenderer = this.getCurrentRenderer() previousRenderer.reset() this.selectedEffectId = normalizedId this.fftAnalyzer.reset() this.resetPerformanceGuard() this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight) PreferencesUtil.putSync(PREFERENCE_SELECTED_EFFECT, this.selectedEffectId) this.persistLegacyMode() Logger.info(TAG, 'switchEffect to ' + this.selectedEffectId) } private getCurrentEffectName(): string { return this.getCurrentEffect().displayName } private nextEffect(): void { if (this.effects.length <= 1) { return } let currentIndex: number = 0 for (let i = 0; i < this.effects.length; i++) { if (this.effects[i].effectId === this.selectedEffectId) { currentIndex = i break } } const nextIndex: number = (currentIndex + 1) % this.effects.length this.switchEffect(this.effects[nextIndex].effectId) } build() { Stack({ alignContent: Alignment.TopEnd }) { Canvas(this.canvasContext) .width('100%') .height('100%') .backgroundColor(Color.Transparent) .onReady(() => { this.canvasContext.imageSmoothingEnabled = true this.canvasContext.imageSmoothingQuality = 'medium' }) .onAreaChange((_oldVal: Area, newVal: Area) => { this.canvasWidth = newVal.width as number this.canvasHeight = newVal.height as number }) // Row() { // SymbolGlyph($r('sys.symbol.music')) // .fontSize(14) // .fontColor([$r('app.color.brand')]) // Text(this.getCurrentEffectName()) // .fontSize(12) // .fontColor($r('app.color.brand')) // .margin({ left: 4 }) // } // .padding({ top: 5, bottom: 5, left: 10, right: 10 }) // .borderRadius(16) // .backgroundColor(Color.Transparent) // .shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.45)', offsetX: 0, offsetY: 0 }) // .margin({ top: 8, right: 8 }) // .onClick(() => { // this.nextEffect() // }) } .width('100%') .height('100%') .onVisibleAreaChange([0.0, 1.0], (_isVisible: boolean, currentRatio: number) => { if (currentRatio <= 0.5) { this.stopRendering() return } this.updateRenderingState() }) } }