diff --git a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx index 282b7432..fecef837 100644 --- a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx +++ b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx @@ -3,6 +3,7 @@ import { ThreejsArtifactViewer, isThreejsArtifact, } from "./ThreejsArtifactViewer" +import { WaveSurferArtifactViewer } from "./WaveSurferArtifactViewer" import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile" import { CardMedia, Box } from "@mui/material" @@ -43,9 +44,12 @@ export const ArtifactCardMedia: FC<{ alignItems: "center", }} > - + ) } else if (artifact.mimetype.startsWith("image")) { diff --git a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx new file mode 100644 index 00000000..74177602 --- /dev/null +++ b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useEffect, useState, useRef } from "react" +import WaveSurfer from "wavesurfer.js" +import { Box } from "@mui/material" + +interface WaveSurferArtifactViewerProps { + height: number + waveColor: string + progressColor: string + url: string +} + +const useWavesurfer = ( + containerRef: React.MutableRefObject, + options: WaveSurferArtifactViewerProps +) => { + const [wavesurfer, setWavesurfer] = useState(null) + + useEffect(() => { + if (!containerRef.current) return + + const ws = WaveSurfer.create({ + ...options, + container: containerRef.current, + }) + + setWavesurfer(ws) + + return () => { + ws.destroy() + } + }, [containerRef]) + + return wavesurfer +} + +// Create a React component of wavesurfer. +export const WaveSurferArtifactViewer: React.FC< + WaveSurferArtifactViewerProps +> = (props) => { + const containerRef = useRef(null!) + const [isPlaying, setIsPlaying] = useState(false) + const wavesurfer = useWavesurfer(containerRef, props) + + const onPlayClick = useCallback(() => { + if (!wavesurfer) return + wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play() + }, [wavesurfer]) + + useEffect(() => { + if (!wavesurfer) return + + setIsPlaying(false) + + const subscriptions = [ + wavesurfer.on("play", () => setIsPlaying(true)), + wavesurfer.on("pause", () => setIsPlaying(false)), + ] + + return () => { + subscriptions.forEach((unsub) => unsub()) + } + }, [wavesurfer]) + + return ( + +
+ + + ) +} diff --git a/package-lock.json b/package-lock.json index 409d4225..05c79bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,8 @@ "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "three": "^0.155.0" + "three": "^0.155.0", + "wavesurfer.js": "^7.4.12" }, "devDependencies": { "@babel/core": "^7.14.3", @@ -14824,6 +14825,11 @@ "node": ">=10.13.0" } }, + "node_modules/wavesurfer.js": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz", + "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g==" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -25938,6 +25944,11 @@ "graceful-fs": "^4.1.2" } }, + "wavesurfer.js": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz", + "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g==" + }, "web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index 3e624c56..9e6e59e9 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "three": "^0.155.0" + "three": "^0.155.0", + "wavesurfer.js": "^7.4.12" }, "devDependencies": { "@babel/core": "^7.14.3",