| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- 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<string, SpectrumRenderer> = new Map<string, SpectrumRenderer>()
- 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<number>('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()
- })
- }
- }
|