Design system.
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 

259 рядки
11 KiB

  1. import * as React from 'react';
  2. import {WaveSurferOptions} from 'wavesurfer.js';
  3. import clsx from 'clsx';
  4. import {getFormValues} from '@theoryofnekomata/formxtra';
  5. export type SpectrogramCanvasDerivedElement = HTMLDivElement;
  6. export interface SpectrogramCanvasProps
  7. extends React.HTMLProps<SpectrogramCanvasDerivedElement>,
  8. Omit<WaveSurferOptions, 'waveColor' | 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> {
  9. waveColor?: string;
  10. audioRef?: React.Ref<HTMLAudioElement>;
  11. }
  12. export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedElement, SpectrogramCanvasProps>(({
  13. className,
  14. children,
  15. controls,
  16. // TODO organize props for color
  17. waveColor,
  18. progressColor,
  19. cursorColor,
  20. cursorWidth,
  21. barWidth,
  22. barGap,
  23. barRadius,
  24. barHeight,
  25. barAlign,
  26. minPxPerSec,
  27. peaks,
  28. duration,
  29. autoPlay,
  30. interact,
  31. hideScrollbar,
  32. audioRate,
  33. autoScroll,
  34. autoCenter,
  35. sampleRate,
  36. splitChannels,
  37. normalize,
  38. audioRef,
  39. ...etcProps
  40. }, forwardedRef) => {
  41. const [isPlaying, setIsPlaying] = React.useState(false);
  42. const defaultRef = React.useRef<SpectrogramCanvasDerivedElement>(null);
  43. const containerRef = forwardedRef ?? defaultRef;
  44. const waveSurferRef = React.useRef<any>(null);
  45. const cursorRef = React.useRef<HTMLDivElement>(null);
  46. const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => {
  47. e.preventDefault();
  48. const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement };
  49. const formData = getFormValues(
  50. e.currentTarget,
  51. {
  52. submitter: nativeEvent.submitter,
  53. }
  54. );
  55. const actionName = formData['action'] as string;
  56. switch (actionName) {
  57. case 'togglePlayback':
  58. setIsPlaying((prev) => !prev);
  59. break;
  60. default:
  61. break;
  62. }
  63. };
  64. React.useEffect(() => {
  65. if (!(typeof audioRef === 'object' && audioRef)) {
  66. return;
  67. }
  68. const { current: media } = audioRef;
  69. if (!media) {
  70. return;
  71. }
  72. if (!(typeof containerRef === 'object' && containerRef)) {
  73. return;
  74. }
  75. const { current: container } = containerRef;
  76. if (!container) {
  77. return;
  78. }
  79. if (!(typeof cursorRef === 'object' && cursorRef)) {
  80. return;
  81. }
  82. const { current: cursor } = cursorRef;
  83. if (!cursor) {
  84. return;
  85. }
  86. const handleTimeUpdate = (e: Event) => {
  87. const thisMedia = e.currentTarget as HTMLAudioElement;
  88. cursor.style.width = `${(thisMedia?.currentTime ?? 0) / (thisMedia?.duration ?? 1) * 100}%`;
  89. };
  90. const load = async (media: HTMLAudioElement, container: HTMLElement) => {
  91. const { default: WaveSurfer } = await import('wavesurfer.js');
  92. const { default: Spectrogram, } = await import('wavesurfer.js/dist/plugins/spectrogram');
  93. const dummyContainer = window.document.createElement('div');
  94. window.document.body.appendChild(dummyContainer);
  95. const waveSurferInstance = WaveSurfer.create({
  96. container: dummyContainer,
  97. height: 100,
  98. fillParent: true,
  99. autoplay: autoPlay,
  100. waveColor,
  101. progressColor,
  102. cursorColor,
  103. barWidth,
  104. barGap,
  105. barRadius,
  106. barHeight,
  107. barAlign,
  108. minPxPerSec,
  109. peaks,
  110. duration,
  111. interact,
  112. hideScrollbar,
  113. audioRate,
  114. autoScroll,
  115. autoCenter,
  116. sampleRate,
  117. splitChannels,
  118. normalize,
  119. plugins: [],
  120. cursorWidth,
  121. media,
  122. });
  123. let colorMap: Array<[number, number, number, number]> = [];
  124. if (waveColor?.toLowerCase().startsWith('rgb(')) {
  125. const waveColorParse = waveColor.match(/rgb\((\d+)[, ]\s*(\d+)[, ]\s*(\d+)\)/);
  126. const waveColorR = parseInt(waveColorParse?.[1] ?? '0', 10);
  127. const waveColorG = parseInt(waveColorParse?.[2] ?? '0', 10);
  128. const waveColorB = parseInt(waveColorParse?.[3] ?? '0', 10);
  129. for (let i = 0; i < 256; i += 1) {
  130. colorMap.push([waveColorR / 256, waveColorG / 256, waveColorB / 256, i / 256]);
  131. }
  132. }
  133. waveSurferInstance.registerPlugin(
  134. Spectrogram.create({
  135. container,
  136. labels: true,
  137. labelsColor: 'rgb(0 0 0/0)',
  138. height: container.clientHeight,
  139. colorMap,
  140. }),
  141. )
  142. waveSurferInstance.on('ready', () => {
  143. if (!container) {
  144. return;
  145. }
  146. while (container.children.length > 1) {
  147. container.removeChild(container.children[0]);
  148. }
  149. dummyContainer.remove();
  150. });
  151. await waveSurferInstance.load(media.currentSrc);
  152. waveSurferInstance.setTime(media.currentTime);
  153. media.addEventListener('timeupdate', handleTimeUpdate);
  154. return waveSurferInstance;
  155. };
  156. const { current: waveSurferCurrent } = waveSurferRef;
  157. void load(media, container).then((i) => {
  158. waveSurferRef.current = i;
  159. });
  160. return () => {
  161. if (waveSurferCurrent) {
  162. (waveSurferCurrent as unknown as Record<string, Function>).destroy();
  163. }
  164. if (container) {
  165. container.innerHTML = '';
  166. }
  167. if (media) {
  168. media.removeEventListener('timeupdate', handleTimeUpdate);
  169. }
  170. };
  171. }, [
  172. audioRef,
  173. autoPlay,
  174. waveColor,
  175. progressColor,
  176. cursorColor,
  177. barWidth,
  178. barGap,
  179. barRadius,
  180. barHeight,
  181. barAlign,
  182. minPxPerSec,
  183. peaks,
  184. duration,
  185. interact,
  186. hideScrollbar,
  187. audioRate,
  188. autoScroll,
  189. autoCenter,
  190. sampleRate,
  191. splitChannels,
  192. normalize,
  193. cursorWidth,
  194. containerRef,
  195. ]);
  196. return (
  197. <div
  198. className={clsx(
  199. 'flex flex-col',
  200. className,
  201. )}
  202. >
  203. <div
  204. className="flex-auto relative aspect-video sm:aspect-auto"
  205. style={{
  206. maskImage: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACEAYAAAAiJtFnAAAFDmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMjdUMTk6MTE6MTQrMDgwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyMy0wNi0yN1QxOToxMToxNCswODAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSIyIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSIxIgogICB0aWZmOkltYWdlV2lkdGg9IjIiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjIiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjcyLzEiCiAgIHRpZmY6WVJlc29sdXRpb249IjcyLzEiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJwcm9kdWNlZCIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWZmaW5pdHkgUGhvdG8gMiAyLjAuNCIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMy0wNi0yN1QxOToxMjoyMyswODowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+HFYtUQAAAYFpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAACiRdZHLS0JBFIc/tRdZFNQiqIWEtaowA6lNkBEWSIgZ9Nro1auB2uVeI6Jt0DYoiNr0WtRfUNugdRAURRBta13UpuR2rgpK5BnOnG9+M+cwcwbskbSSMWo8kMnm9HDA75qbX3DVv1JHJ05gIKoY2lgoFKSqfT1gs+Jdv1Wr+rl/zRlPGArYGoRHFU3PCU8KB9dymsW7wu1KKhoXPhfu0+WCwveWHivym8XJIv9YrEfC42BvFXYlKzhWwUpKzwjLy3Fn0qtK6T7WS5oS2dkZid3iXRiECeDHxRQTjONjkBGZffTjZUBWVMn3FPKnWZFcRWaNdXSWSZIiR5+oq1I9IVEVPSEjzbrV/799NdQhb7F6kx9qX0zzowfqdyC/bZrfx6aZPwHHM1xly/krRzD8Kfp2WXMfQssmXFyXtdgeXG5Bx5MW1aMFySFuV1V4P4PmeWi7hcbFYs9K+5w+QmRDvuoG9g+gV863LP0CQBln1EZARokAAAAJcEhZcwAACxMAAAsTAQCanBgAAAASSURBVAiZY2BAASzdqHyG//8BDTECjpzQZHQAAAAASUVORK5CYII=)',
  207. WebkitMaskImage: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACEAYAAAAiJtFnAAAFDmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMjdUMTk6MTE6MTQrMDgwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyMy0wNi0yN1QxOToxMToxNCswODAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSIyIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSIxIgogICB0aWZmOkltYWdlV2lkdGg9IjIiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjIiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjcyLzEiCiAgIHRpZmY6WVJlc29sdXRpb249IjcyLzEiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJwcm9kdWNlZCIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWZmaW5pdHkgUGhvdG8gMiAyLjAuNCIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMy0wNi0yN1QxOToxMjoyMyswODowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+HFYtUQAAAYFpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAACiRdZHLS0JBFIc/tRdZFNQiqIWEtaowA6lNkBEWSIgZ9Nro1auB2uVeI6Jt0DYoiNr0WtRfUNugdRAURRBta13UpuR2rgpK5BnOnG9+M+cwcwbskbSSMWo8kMnm9HDA75qbX3DVv1JHJ05gIKoY2lgoFKSqfT1gs+Jdv1Wr+rl/zRlPGArYGoRHFU3PCU8KB9dymsW7wu1KKhoXPhfu0+WCwveWHivym8XJIv9YrEfC42BvFXYlKzhWwUpKzwjLy3Fn0qtK6T7WS5oS2dkZid3iXRiECeDHxRQTjONjkBGZffTjZUBWVMn3FPKnWZFcRWaNdXSWSZIiR5+oq1I9IVEVPSEjzbrV/799NdQhb7F6kx9qX0zzowfqdyC/bZrfx6aZPwHHM1xly/krRzD8Kfp2WXMfQssmXFyXtdgeXG5Bx5MW1aMFySFuV1V4P4PmeWi7hcbFYs9K+5w+QmRDvuoG9g+gV863LP0CQBln1EZARokAAAAJcEhZcwAACxMAAAsTAQCanBgAAAASSURBVAiZY2BAASzdqHyG//8BDTECjpzQZHQAAAAASUVORK5CYII=)',
  208. }}
  209. >
  210. <div
  211. ref={cursorRef}
  212. style={{
  213. position: 'absolute',
  214. top: 0,
  215. left: 0,
  216. height: '100%',
  217. mixBlendMode: 'plus-lighter',
  218. backgroundColor: 'rgb(var(--color-primary))',
  219. zIndex: 5,
  220. }}
  221. />
  222. <div className="absolute top-0 left-0 w-full h-full"
  223. ref={containerRef}
  224. />
  225. </div>
  226. {controls && (
  227. <form
  228. onSubmit={handleAction}
  229. >
  230. <button
  231. type="submit"
  232. name="action"
  233. value="togglePlayback"
  234. >
  235. {isPlaying ? '⏸' : '▶'}
  236. </button>
  237. </form>
  238. )}
  239. </div>
  240. );
  241. });
  242. SpectrogramCanvas.displayName = 'WavesurferCanvas';