|
@@ -1,45 +1,43 @@
|
|
|
import { IjkMediaPlayer } from '@ohos/ijkplayer'
|
|
import { IjkMediaPlayer } from '@ohos/ijkplayer'
|
|
|
|
|
+import { PreferencesUtil } from '@pura/harmony-utils'
|
|
|
import { FFTAnalyzer } from '../../common/util/FFTAnalyzer'
|
|
import { FFTAnalyzer } from '../../common/util/FFTAnalyzer'
|
|
|
import { SpectrumRenderer } from './SpectrumRenderer'
|
|
import { SpectrumRenderer } from './SpectrumRenderer'
|
|
|
-import { BarSpectrumRenderer } from './BarSpectrumRenderer'
|
|
|
|
|
-import { WaveSpectrumRenderer } from './WaveSpectrumRenderer'
|
|
|
|
|
-import { CircleSpectrumRenderer } from './CircleSpectrumRenderer'
|
|
|
|
|
-import { ParticleSpectrumRenderer } from './ParticleSpectrumRenderer'
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ SpectrumEffectDefinition,
|
|
|
|
|
+ findSpectrumEffectById,
|
|
|
|
|
+ fromLegacySpectrumMode,
|
|
|
|
|
+ getDefaultSpectrumEffectId,
|
|
|
|
|
+ getSpectrumEffects,
|
|
|
|
|
+ normalizeSpectrumEffectId
|
|
|
|
|
+} from './SpectrumEffectRegistry'
|
|
|
import { normalizeHexColor } from './SpectrumRenderer'
|
|
import { normalizeHexColor } from './SpectrumRenderer'
|
|
|
import Logger from '../../common/util/Logger'
|
|
import Logger from '../../common/util/Logger'
|
|
|
|
|
|
|
|
const TAG: string = 'heanup-SpectrumView'
|
|
const TAG: string = 'heanup-SpectrumView'
|
|
|
const BAND_COUNT: number = 32
|
|
const BAND_COUNT: number = 32
|
|
|
-const FRAME_INTERVAL: number = 33 // ~30fps
|
|
|
|
|
-
|
|
|
|
|
-export enum SpectrumMode {
|
|
|
|
|
- BAR = 0,
|
|
|
|
|
- WAVE = 1,
|
|
|
|
|
- CIRCLE = 2,
|
|
|
|
|
- PARTICLE = 3
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const MODE_NAMES: string[] = ['柱状', '波形', '圆形', '粒子']
|
|
|
|
|
|
|
+const PREFERENCE_SELECTED_EFFECT: string = 'selectedSpectrumEffectId'
|
|
|
|
|
|
|
|
@Component
|
|
@Component
|
|
|
export struct SpectrumView {
|
|
export struct SpectrumView {
|
|
|
- @StorageLink('spectrumMode') currentMode: number = 0
|
|
|
|
|
@Prop @Watch('onPlayingChange') isPlaying: boolean = false
|
|
@Prop @Watch('onPlayingChange') isPlaying: boolean = false
|
|
|
@StorageProp('themeColor') themeColor: string = '#FF0050'
|
|
@StorageProp('themeColor') themeColor: string = '#FF0050'
|
|
|
@Prop @Watch('onVisibilityChange') isVisible: boolean = false
|
|
@Prop @Watch('onVisibilityChange') isVisible: boolean = false
|
|
|
- private modeCount: number = MODE_NAMES.length
|
|
|
|
|
- @State canvasWidth: number = 0
|
|
|
|
|
- @State canvasHeight: number = 0
|
|
|
|
|
- private renderSettings: RenderingContextSettings = new RenderingContextSettings(true)
|
|
|
|
|
- private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderSettings)
|
|
|
|
|
- private fftAnalyzer: FFTAnalyzer = new FFTAnalyzer()
|
|
|
|
|
- private renderers: SpectrumRenderer[] = [
|
|
|
|
|
- new BarSpectrumRenderer(),
|
|
|
|
|
- new WaveSpectrumRenderer(),
|
|
|
|
|
- new CircleSpectrumRenderer(),
|
|
|
|
|
- new ParticleSpectrumRenderer()
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+
|
|
|
|
|
+ @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 timerId: number = -1
|
|
|
|
|
+ private currentFps: number = 30
|
|
|
|
|
+ private complexityScale: number = 1.0
|
|
|
|
|
+ private highLoadStreak: number = 0
|
|
|
|
|
+ private lowLoadStreak: number = 0
|
|
|
|
|
|
|
|
onVisibilityChange(): void {
|
|
onVisibilityChange(): void {
|
|
|
this.updateRenderingState()
|
|
this.updateRenderingState()
|
|
@@ -50,25 +48,61 @@ export struct SpectrumView {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
aboutToAppear(): void {
|
|
aboutToAppear(): void {
|
|
|
- // 读取持久化的特效偏好
|
|
|
|
|
- const savedMode: number | undefined = AppStorage.get<number>('spectrumMode')
|
|
|
|
|
- if (savedMode !== undefined && savedMode >= 0 && savedMode <= 3) {
|
|
|
|
|
- this.currentMode = savedMode
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ this.ensureRenderersReady()
|
|
|
|
|
+ this.restoreSelectedEffect()
|
|
|
|
|
+ this.resetPerformanceGuard()
|
|
|
this.updateRenderingState()
|
|
this.updateRenderingState()
|
|
|
- Logger.info(TAG, 'SpectrumView aboutToAppear, mode=' + this.currentMode)
|
|
|
|
|
|
|
+ Logger.info(TAG, 'SpectrumView aboutToAppear, effect=' + this.selectedEffectId)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
aboutToDisappear(): void {
|
|
aboutToDisappear(): void {
|
|
|
this.stopRendering()
|
|
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 {
|
|
|
|
|
+ const storedEffectId: string = PreferencesUtil.getStringSync(PREFERENCE_SELECTED_EFFECT, '')
|
|
|
|
|
+ if (storedEffectId) {
|
|
|
|
|
+ this.selectedEffectId = normalizeSpectrumEffectId(storedEffectId)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const legacyMode: number | undefined = AppStorage.get<number>('spectrumMode')
|
|
|
|
|
+ this.selectedEffectId = fromLegacySpectrumMode(legacyMode)
|
|
|
|
|
+ PreferencesUtil.putSync(PREFERENCE_SELECTED_EFFECT, this.selectedEffectId)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private startRendering(): void {
|
|
private startRendering(): void {
|
|
|
- if (this.timerId !== -1) return
|
|
|
|
|
- Logger.info(TAG, 'startRendering')
|
|
|
|
|
|
|
+ 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.timerId = setInterval(() => {
|
|
|
this.renderFrame()
|
|
this.renderFrame()
|
|
|
- }, FRAME_INTERVAL)
|
|
|
|
|
|
|
+ }, 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 {
|
|
private stopRendering(): void {
|
|
@@ -79,21 +113,88 @@ export struct SpectrumView {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ 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 {
|
|
private renderFrame(): void {
|
|
|
|
|
+ if (this.canvasWidth <= 0 || this.canvasHeight <= 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const player: IjkMediaPlayer = IjkMediaPlayer.getInstance()
|
|
const player: IjkMediaPlayer = IjkMediaPlayer.getInstance()
|
|
|
const pcmData: number[] = player.getSpectrumData()
|
|
const pcmData: number[] = player.getSpectrumData()
|
|
|
|
|
+ if (pcmData.length === 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (pcmData.length === 0) return
|
|
|
|
|
|
|
+ const effect: SpectrumEffectDefinition = this.getCurrentEffect()
|
|
|
|
|
+ const beginMs: number = Date.now()
|
|
|
|
|
|
|
|
const spectrumData = this.fftAnalyzer.analyze(pcmData, 44100)
|
|
const spectrumData = this.fftAnalyzer.analyze(pcmData, 44100)
|
|
|
- const rawBands: number[] = spectrumData.getBands(BAND_COUNT)
|
|
|
|
|
|
|
+ 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 bands: number[] = this.fftAnalyzer.smooth(rawBands)
|
|
|
const energy: number = spectrumData.getEnergy()
|
|
const energy: number = spectrumData.getEnergy()
|
|
|
|
|
|
|
|
- const ctx: CanvasRenderingContext2D = this.canvasContext
|
|
|
|
|
- const renderer: SpectrumRenderer = this.renderers[this.currentMode]
|
|
|
|
|
|
|
+ const renderer: SpectrumRenderer = this.getCurrentRenderer()
|
|
|
const brandColor: string = normalizeHexColor(this.themeColor, '#FF0050')
|
|
const brandColor: string = normalizeHexColor(this.themeColor, '#FF0050')
|
|
|
- renderer.render(ctx, bands, energy, this.canvasWidth, this.canvasHeight, brandColor)
|
|
|
|
|
|
|
+ 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 {
|
|
private updateRenderingState(): void {
|
|
@@ -104,19 +205,62 @@ export struct SpectrumView {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private switchMode(mode: number): void {
|
|
|
|
|
- if (mode === this.currentMode) return
|
|
|
|
|
- this.renderers[this.currentMode].reset()
|
|
|
|
|
- this.currentMode = mode
|
|
|
|
|
- AppStorage.setOrCreate<number>('spectrumMode', mode)
|
|
|
|
|
|
|
+ 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.setOrCreate<number>('spectrumMode', 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.fftAnalyzer.reset()
|
|
|
|
|
+ this.resetPerformanceGuard()
|
|
|
this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
|
- Logger.info(TAG, 'switchMode to ' + MODE_NAMES[mode])
|
|
|
|
|
|
|
+
|
|
|
|
|
+ PreferencesUtil.putSync(PREFERENCE_SELECTED_EFFECT, this.selectedEffectId)
|
|
|
|
|
+ this.persistLegacyMode()
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, 'switchEffect to ' + this.selectedEffectId)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private getCurrentEffectName(): string {
|
|
|
|
|
+ return this.getCurrentEffect().displayName
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private nextMode(): void {
|
|
|
|
|
- const next: number = (this.currentMode + 1) % this.modeCount
|
|
|
|
|
- this.switchMode(next)
|
|
|
|
|
|
|
+ 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() {
|
|
build() {
|
|
@@ -134,12 +278,11 @@ export struct SpectrumView {
|
|
|
this.canvasHeight = newVal.height as number
|
|
this.canvasHeight = newVal.height as number
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- // 特效切换按钮(点击循环切换)
|
|
|
|
|
Row() {
|
|
Row() {
|
|
|
SymbolGlyph($r('sys.symbol.music'))
|
|
SymbolGlyph($r('sys.symbol.music'))
|
|
|
.fontSize(14)
|
|
.fontSize(14)
|
|
|
.fontColor([$r('app.color.brand')])
|
|
.fontColor([$r('app.color.brand')])
|
|
|
- Text(MODE_NAMES[this.currentMode])
|
|
|
|
|
|
|
+ Text(this.getCurrentEffectName())
|
|
|
.fontSize(12)
|
|
.fontSize(12)
|
|
|
.fontColor($r('app.color.brand'))
|
|
.fontColor($r('app.color.brand'))
|
|
|
.margin({ left: 4 })
|
|
.margin({ left: 4 })
|
|
@@ -150,7 +293,7 @@ export struct SpectrumView {
|
|
|
.shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.45)', offsetX: 0, offsetY: 0 })
|
|
.shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.45)', offsetX: 0, offsetY: 0 })
|
|
|
.margin({ top: 8, right: 8 })
|
|
.margin({ top: 8, right: 8 })
|
|
|
.onClick(() => {
|
|
.onClick(() => {
|
|
|
- this.nextMode()
|
|
|
|
|
|
|
+ this.nextEffect()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
.width('100%')
|
|
.width('100%')
|