SpectrumView.ets 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import { IjkMediaPlayer } from '@ohos/ijkplayer'
  2. import { PreferencesUtil } from '@pura/harmony-utils'
  3. import { FFTAnalyzer } from '../../common/util/FFTAnalyzer'
  4. import { SpectrumRenderer } from './SpectrumRenderer'
  5. import {
  6. SpectrumEffectDefinition,
  7. findSpectrumEffectById,
  8. fromLegacySpectrumMode,
  9. getDefaultSpectrumEffectId,
  10. getSpectrumEffects,
  11. normalizeSpectrumEffectId
  12. } from './SpectrumEffectRegistry'
  13. import { normalizeHexColor } from './SpectrumRenderer'
  14. import Logger from '../../common/util/Logger'
  15. const TAG: string = 'heanup-SpectrumView'
  16. const BAND_COUNT: number = 48 // 频段数量
  17. const PREFERENCE_SELECTED_EFFECT: string = 'selectedSpectrumEffectId'
  18. @Component
  19. export struct SpectrumView {
  20. @Prop @Watch('onPlayingChange') isPlaying: boolean = false
  21. @StorageProp('themeColor') themeColor: string = '#FF0050'
  22. @Prop @Watch('onVisibilityChange') isVisible: boolean = false
  23. @StorageProp('spectrumMode') @Watch('onSpectrumModeChange') spectrumMode: number = 0
  24. @State private selectedEffectId: string = getDefaultSpectrumEffectId()
  25. @State private canvasWidth: number = 0
  26. @State private canvasHeight: number = 0
  27. private readonly effects: SpectrumEffectDefinition[] = getSpectrumEffects()
  28. private readonly renderSettings: RenderingContextSettings = new RenderingContextSettings(true)
  29. private readonly canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderSettings)
  30. private readonly fftAnalyzer: FFTAnalyzer = new FFTAnalyzer()
  31. private readonly renderers: Map<string, SpectrumRenderer> = new Map<string, SpectrumRenderer>()
  32. private timerId: number = -1
  33. private currentFps: number = 30
  34. private complexityScale: number = 1.0
  35. private highLoadStreak: number = 0
  36. private lowLoadStreak: number = 0
  37. onVisibilityChange(): void {
  38. this.updateRenderingState()
  39. }
  40. onPlayingChange(): void {
  41. this.updateRenderingState()
  42. }
  43. onSpectrumModeChange(): void {
  44. const effects = getSpectrumEffects();
  45. if (this.spectrumMode >= 0 && this.spectrumMode < effects.length) {
  46. const targetEffectId = effects[this.spectrumMode].effectId;
  47. if (targetEffectId !== this.selectedEffectId) {
  48. this.switchEffect(targetEffectId);
  49. }
  50. }
  51. }
  52. aboutToAppear(): void {
  53. this.ensureRenderersReady()
  54. this.restoreSelectedEffect()
  55. this.resetPerformanceGuard()
  56. this.updateRenderingState()
  57. Logger.info(TAG, 'SpectrumView aboutToAppear, effect=' + this.selectedEffectId)
  58. }
  59. aboutToDisappear(): void {
  60. this.stopRendering()
  61. }
  62. private ensureRenderersReady(): void {
  63. if (this.renderers.size > 0) {
  64. return
  65. }
  66. for (let i = 0; i < this.effects.length; i++) {
  67. const effect: SpectrumEffectDefinition = this.effects[i]
  68. this.renderers.set(effect.effectId, effect.createRenderer())
  69. }
  70. }
  71. private restoreSelectedEffect(): void {
  72. // 首先尝试从 Preferences 读取保存的索引
  73. const storedIndex: number = PreferencesUtil.getNumberSync('spectrumModeIndex', -1)
  74. if (storedIndex >= 0 && storedIndex < this.effects.length) {
  75. this.selectedEffectId = this.effects[storedIndex].effectId
  76. Logger.info(TAG, 'Restored spectrum mode from preferences: ' + storedIndex + ' (' + this.selectedEffectId + ')')
  77. return
  78. }
  79. // 其次尝试从 AppStorage 读取 spectrumMode
  80. if (this.spectrumMode >= 0 && this.spectrumMode < this.effects.length) {
  81. this.selectedEffectId = this.effects[this.spectrumMode].effectId
  82. Logger.info(TAG, 'Restored spectrum mode from AppStorage: ' + this.spectrumMode + ' (' + this.selectedEffectId + ')')
  83. return
  84. }
  85. // 再次尝试从 Preferences 读取 effectId
  86. const storedEffectId: string = PreferencesUtil.getStringSync(PREFERENCE_SELECTED_EFFECT, '')
  87. if (storedEffectId) {
  88. this.selectedEffectId = normalizeSpectrumEffectId(storedEffectId)
  89. Logger.info(TAG, 'Restored spectrum effectId from preferences: ' + this.selectedEffectId)
  90. return
  91. }
  92. // 使用默认值
  93. this.selectedEffectId = getDefaultSpectrumEffectId()
  94. Logger.info(TAG, 'Using default spectrum effect: ' + this.selectedEffectId)
  95. }
  96. private startRendering(): void {
  97. if (this.timerId !== -1) {
  98. return
  99. }
  100. const intervalMs: number = Math.max(16, Math.floor(1000 / this.currentFps))
  101. Logger.info(TAG, 'startRendering fps=' + this.currentFps + ', interval=' + intervalMs)
  102. this.timerId = setInterval(() => {
  103. this.renderFrame()
  104. }, intervalMs)
  105. }
  106. private restartRenderingIfNeeded(nextFps: number): void {
  107. if (nextFps === this.currentFps) {
  108. return
  109. }
  110. this.currentFps = nextFps
  111. if (this.timerId === -1) {
  112. return
  113. }
  114. clearInterval(this.timerId)
  115. this.timerId = -1
  116. this.startRendering()
  117. }
  118. private stopRendering(): void {
  119. if (this.timerId !== -1) {
  120. clearInterval(this.timerId)
  121. this.timerId = -1
  122. Logger.info(TAG, 'stopRendering')
  123. }
  124. }
  125. private getCurrentEffect(): SpectrumEffectDefinition {
  126. const effect: SpectrumEffectDefinition | undefined = findSpectrumEffectById(this.selectedEffectId)
  127. if (effect) {
  128. return effect
  129. }
  130. return this.effects[0]
  131. }
  132. private getCurrentRenderer(): SpectrumRenderer {
  133. const effectId: string = this.getCurrentEffect().effectId
  134. const renderer: SpectrumRenderer | undefined = this.renderers.get(effectId)
  135. if (renderer) {
  136. return renderer
  137. }
  138. return this.renderers.get(getDefaultSpectrumEffectId()) as SpectrumRenderer
  139. }
  140. private renderFrame(): void {
  141. if (this.canvasWidth <= 0 || this.canvasHeight <= 0) {
  142. return
  143. }
  144. const player: IjkMediaPlayer = IjkMediaPlayer.getInstance()
  145. const pcmData: number[] = player.getSpectrumData()
  146. if (pcmData.length === 0) {
  147. return
  148. }
  149. const effect: SpectrumEffectDefinition = this.getCurrentEffect()
  150. const beginMs: number = Date.now()
  151. const spectrumData = this.fftAnalyzer.analyze(pcmData, 44100)
  152. const scaledBandCount: number = Math.max(8, Math.floor(effect.performance.maxBandCount * this.complexityScale))
  153. const rawBands: number[] = spectrumData.getBands(Math.min(BAND_COUNT, scaledBandCount))
  154. const bands: number[] = this.fftAnalyzer.smooth(rawBands)
  155. const energy: number = spectrumData.getEnergy()
  156. const renderer: SpectrumRenderer = this.getCurrentRenderer()
  157. const brandColor: string = normalizeHexColor(this.themeColor, '#FF0050')
  158. renderer.render(
  159. this.canvasContext,
  160. bands,
  161. energy,
  162. this.canvasWidth,
  163. this.canvasHeight,
  164. brandColor,
  165. { complexityScale: this.complexityScale }
  166. )
  167. const renderCostMs: number = Date.now() - beginMs
  168. this.updatePerformanceGuard(effect, renderCostMs)
  169. }
  170. private updatePerformanceGuard(effect: SpectrumEffectDefinition, renderCostMs: number): void {
  171. const frameBudget: number = 1000 / this.currentFps
  172. if (renderCostMs > frameBudget * 1.15) {
  173. this.highLoadStreak += 1
  174. this.lowLoadStreak = 0
  175. if (this.highLoadStreak >= 6) {
  176. this.highLoadStreak = 0
  177. this.complexityScale = Math.max(effect.performance.complexityFloor, this.complexityScale - 0.1)
  178. const nextFps: number = Math.max(effect.performance.minFps, this.currentFps - 2)
  179. this.restartRenderingIfNeeded(nextFps)
  180. }
  181. return
  182. }
  183. if (renderCostMs < frameBudget * 0.65) {
  184. this.lowLoadStreak += 1
  185. this.highLoadStreak = 0
  186. if (this.lowLoadStreak >= 14) {
  187. this.lowLoadStreak = 0
  188. this.complexityScale = Math.min(1.0, this.complexityScale + 0.05)
  189. const nextFps: number = Math.min(effect.performance.targetFps, this.currentFps + 1)
  190. this.restartRenderingIfNeeded(nextFps)
  191. }
  192. return
  193. }
  194. this.highLoadStreak = 0
  195. this.lowLoadStreak = 0
  196. }
  197. private updateRenderingState(): void {
  198. if (this.isVisible && this.isPlaying) {
  199. this.startRendering()
  200. } else {
  201. this.stopRendering()
  202. }
  203. }
  204. private resetPerformanceGuard(): void {
  205. const effect: SpectrumEffectDefinition = this.getCurrentEffect()
  206. this.currentFps = effect.performance.targetFps
  207. this.complexityScale = 1.0
  208. this.highLoadStreak = 0
  209. this.lowLoadStreak = 0
  210. }
  211. private persistLegacyMode(): void {
  212. let modeIndex: number = 0
  213. for (let i = 0; i < this.effects.length; i++) {
  214. if (this.effects[i].effectId === this.selectedEffectId) {
  215. modeIndex = i
  216. break
  217. }
  218. }
  219. // 同时保存到 AppStorage 和 Preferences
  220. AppStorage.setOrCreate<number>('spectrumMode', modeIndex)
  221. PreferencesUtil.put('spectrumModeIndex', modeIndex)
  222. Logger.info(TAG, 'Persisted spectrum mode index: ' + modeIndex)
  223. }
  224. private switchEffect(effectId: string): void {
  225. const normalizedId: string = normalizeSpectrumEffectId(effectId)
  226. if (normalizedId === this.selectedEffectId) {
  227. return
  228. }
  229. const previousRenderer: SpectrumRenderer = this.getCurrentRenderer()
  230. previousRenderer.reset()
  231. this.selectedEffectId = normalizedId
  232. this.fftAnalyzer.reset()
  233. this.resetPerformanceGuard()
  234. this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  235. PreferencesUtil.putSync(PREFERENCE_SELECTED_EFFECT, this.selectedEffectId)
  236. this.persistLegacyMode()
  237. Logger.info(TAG, 'switchEffect to ' + this.selectedEffectId)
  238. }
  239. private getCurrentEffectName(): string {
  240. return this.getCurrentEffect().displayName
  241. }
  242. private nextEffect(): void {
  243. if (this.effects.length <= 1) {
  244. return
  245. }
  246. let currentIndex: number = 0
  247. for (let i = 0; i < this.effects.length; i++) {
  248. if (this.effects[i].effectId === this.selectedEffectId) {
  249. currentIndex = i
  250. break
  251. }
  252. }
  253. const nextIndex: number = (currentIndex + 1) % this.effects.length
  254. this.switchEffect(this.effects[nextIndex].effectId)
  255. }
  256. build() {
  257. Stack({ alignContent: Alignment.TopEnd }) {
  258. Canvas(this.canvasContext)
  259. .width('100%')
  260. .height('100%')
  261. .backgroundColor(Color.Transparent)
  262. .onReady(() => {
  263. this.canvasContext.imageSmoothingEnabled = true
  264. this.canvasContext.imageSmoothingQuality = 'medium'
  265. })
  266. .onAreaChange((_oldVal: Area, newVal: Area) => {
  267. this.canvasWidth = newVal.width as number
  268. this.canvasHeight = newVal.height as number
  269. })
  270. // Row() {
  271. // SymbolGlyph($r('sys.symbol.music'))
  272. // .fontSize(14)
  273. // .fontColor([$r('app.color.brand')])
  274. // Text(this.getCurrentEffectName())
  275. // .fontSize(12)
  276. // .fontColor($r('app.color.brand'))
  277. // .margin({ left: 4 })
  278. // }
  279. // .padding({ top: 5, bottom: 5, left: 10, right: 10 })
  280. // .borderRadius(16)
  281. // .backgroundColor(Color.Transparent)
  282. // .shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.45)', offsetX: 0, offsetY: 0 })
  283. // .margin({ top: 8, right: 8 })
  284. // .onClick(() => {
  285. // this.nextEffect()
  286. // })
  287. }
  288. .width('100%')
  289. .height('100%')
  290. .onVisibleAreaChange([0.0, 1.0], (_isVisible: boolean, currentRatio: number) => {
  291. if (currentRatio <= 0.5) {
  292. this.stopRendering()
  293. return
  294. }
  295. this.updateRenderingState()
  296. })
  297. }
  298. }