|
@@ -0,0 +1,176 @@
|
|
|
|
|
+import { IjkMediaPlayer } from '@ohos/ijkplayer'
|
|
|
|
|
+import { FFTAnalyzer } from '../../common/util/FFTAnalyzer'
|
|
|
|
|
+import { SpectrumRenderer } from './SpectrumRenderer'
|
|
|
|
|
+import { BarSpectrumRenderer } from './BarSpectrumRenderer'
|
|
|
|
|
+import { WaveSpectrumRenderer } from './WaveSpectrumRenderer'
|
|
|
|
|
+import { CircleSpectrumRenderer } from './CircleSpectrumRenderer'
|
|
|
|
|
+import { ParticleSpectrumRenderer } from './ParticleSpectrumRenderer'
|
|
|
|
|
+import { normalizeHexColor } from './SpectrumRenderer'
|
|
|
|
|
+import Logger from '../../common/util/Logger'
|
|
|
|
|
+
|
|
|
|
|
+const TAG: string = 'heanup-SpectrumView'
|
|
|
|
|
+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[] = ['柱状', '波形', '圆形', '粒子']
|
|
|
|
|
+
|
|
|
|
|
+@Component
|
|
|
|
|
+export struct SpectrumView {
|
|
|
|
|
+ @StorageLink('spectrumMode') currentMode: number = 0
|
|
|
|
|
+ @StorageProp('isPlaying') @Watch('onPlayingChange') isPlaying: boolean = false
|
|
|
|
|
+ @StorageProp('themeColor') themeColor: string = '#FF0050'
|
|
|
|
|
+ @Prop @Watch('onVisibilityChange') isVisible: boolean = false
|
|
|
|
|
+ @State showModePanel: boolean = false
|
|
|
|
|
+ @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()
|
|
|
|
|
+ ]
|
|
|
|
|
+ private timerId: number = -1
|
|
|
|
|
+
|
|
|
|
|
+ onVisibilityChange(): void {
|
|
|
|
|
+ this.updateRenderingState()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onPlayingChange(): void {
|
|
|
|
|
+ this.updateRenderingState()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ aboutToAppear(): void {
|
|
|
|
|
+ // 读取持久化的特效偏好
|
|
|
|
|
+ const savedMode: number | undefined = AppStorage.get<number>('spectrumMode')
|
|
|
|
|
+ if (savedMode !== undefined && savedMode >= 0 && savedMode <= 3) {
|
|
|
|
|
+ this.currentMode = savedMode
|
|
|
|
|
+ }
|
|
|
|
|
+ this.updateRenderingState()
|
|
|
|
|
+ Logger.info(TAG, 'SpectrumView aboutToAppear, mode=' + this.currentMode)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ aboutToDisappear(): void {
|
|
|
|
|
+ this.stopRendering()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private startRendering(): void {
|
|
|
|
|
+ if (this.timerId !== -1) return
|
|
|
|
|
+ Logger.info(TAG, 'startRendering')
|
|
|
|
|
+ this.timerId = setInterval(() => {
|
|
|
|
|
+ this.renderFrame()
|
|
|
|
|
+ }, FRAME_INTERVAL)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private stopRendering(): void {
|
|
|
|
|
+ if (this.timerId !== -1) {
|
|
|
|
|
+ clearInterval(this.timerId)
|
|
|
|
|
+ this.timerId = -1
|
|
|
|
|
+ Logger.info(TAG, 'stopRendering')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private renderFrame(): void {
|
|
|
|
|
+ const player: IjkMediaPlayer = IjkMediaPlayer.getInstance()
|
|
|
|
|
+ const pcmData: number[] = player.getSpectrumData()
|
|
|
|
|
+
|
|
|
|
|
+ if (pcmData.length === 0) return
|
|
|
|
|
+
|
|
|
|
|
+ const spectrumData = this.fftAnalyzer.analyze(pcmData, 44100)
|
|
|
|
|
+ const rawBands: number[] = spectrumData.getBands(BAND_COUNT)
|
|
|
|
|
+ const bands: number[] = this.fftAnalyzer.smooth(rawBands)
|
|
|
|
|
+ const energy: number = spectrumData.getEnergy()
|
|
|
|
|
+
|
|
|
|
|
+ const ctx: CanvasRenderingContext2D = this.canvasContext
|
|
|
|
|
+ const renderer: SpectrumRenderer = this.renderers[this.currentMode]
|
|
|
|
|
+ const brandColor: string = normalizeHexColor(this.themeColor, '#FF0050')
|
|
|
|
|
+ renderer.render(ctx, bands, energy, this.canvasWidth, this.canvasHeight, brandColor)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private updateRenderingState(): void {
|
|
|
|
|
+ if (this.isVisible && this.isPlaying) {
|
|
|
|
|
+ this.startRendering()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.stopRendering()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private switchMode(mode: number): void {
|
|
|
|
|
+ if (mode === this.currentMode) return
|
|
|
|
|
+ this.renderers[this.currentMode].reset()
|
|
|
|
|
+ this.currentMode = mode
|
|
|
|
|
+ AppStorage.setOrCreate<number>('spectrumMode', mode)
|
|
|
|
|
+ this.fftAnalyzer.reset()
|
|
|
|
|
+ // 清空 Canvas
|
|
|
|
|
+ this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
|
|
|
+ Logger.info(TAG, 'switchMode to ' + MODE_NAMES[mode])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 特效切换按钮
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Text('🎵')
|
|
|
|
|
+ .fontSize(22)
|
|
|
|
|
+ .fontColor('#FFFFFF')
|
|
|
|
|
+ .opacity(0.7)
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.showModePanel = !this.showModePanel
|
|
|
|
|
+ })
|
|
|
|
|
+ .padding(8)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.showModePanel) {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ ForEach(MODE_NAMES, (name: string, index: number) => {
|
|
|
|
|
+ Text(name)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(index === this.currentMode ? $r('app.color.brand') : '#FFFFFF')
|
|
|
|
|
+ .fontWeight(index === this.currentMode ? FontWeight.Bold : FontWeight.Normal)
|
|
|
|
|
+ .opacity(index === this.currentMode ? 1.0 : 0.7)
|
|
|
|
|
+ .padding({ top: 8, bottom: 8, left: 16, right: 16 })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.switchMode(index)
|
|
|
|
|
+ this.showModePanel = false
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ .backgroundColor('rgba(0,0,0,0.6)')
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .padding(4)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .margin({ top: 16, right: 16 })
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .onVisibleAreaChange([0.0, 1.0], (_isVisible: boolean, currentRatio: number) => {
|
|
|
|
|
+ if (currentRatio <= 0.5) {
|
|
|
|
|
+ this.stopRendering()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.updateRenderingState()
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|