diff --git a/frontend/packages/core/components/Audio.tsx b/frontend/packages/core/components/Audio.tsx index bfce2dd61411607f7e3b17ca93b363c362786ce5..a30095c99477c87c5bcdb9f4389909cd219570e6 100644 --- a/frontend/packages/core/components/Audio.tsx +++ b/frontend/packages/core/components/Audio.tsx @@ -210,7 +210,7 @@ const Audio = React.forwardRef(({src, cache, } }); const buffer = await data.data.arrayBuffer(); - await p.load(buffer); + await p.load(buffer, data.type ?? undefined); setDecoding(false); setDuration(formatDuration(p.duration)); onLoad?.({sampleRate: p.sampleRate, duration: p.duration}); diff --git a/frontend/packages/core/components/SamplePage/AudioChart.tsx b/frontend/packages/core/components/SamplePage/AudioChart.tsx index 140282653e47b0c0e10d0fa311a9aa7b731872e1..1ce6901a7d6fb4dc25120c5f32ed1c4a5e2f5557 100644 --- a/frontend/packages/core/components/SamplePage/AudioChart.tsx +++ b/frontend/packages/core/components/SamplePage/AudioChart.tsx @@ -1,9 +1,9 @@ import Audio, {AudioProps, AudioRef} from '~/components/Audio'; import React, {FunctionComponent, useCallback, useState} from 'react'; import SampleChart, {SampleChartBaseProps} from '~/components/SamplePage/SampleChart'; -import {rem, size, textLighterColor} from '~/utils/style'; import {format} from 'd3-format'; +import {size} from '~/utils/style'; import styled from 'styled-components'; import {useTranslation} from '~/utils/i18n'; @@ -14,11 +14,6 @@ const StyledAudio = styled(Audio)` flex-shrink: 1; `; -const AudioInfo = styled.span` - color: ${textLighterColor}; - font-size: ${rem(12)}; -`; - const cache = 5 * 60 * 1000; type AudioChartProps = SampleChartBaseProps; @@ -46,11 +41,11 @@ const AudioChart: FunctionComponent = ({...props}) => { type="audio" cache={cache} footer={ - + {t('sample:sample-rate')} {t('common:colon')} {sampleRate} - + } content={content} {...props} diff --git a/frontend/packages/core/components/SamplePage/SampleChart.tsx b/frontend/packages/core/components/SamplePage/SampleChart.tsx index 525dfb8589de20e1b2ca6af497df2b0e7a25eae2..fe5f72a6f5280c4634d25a039f6c88639deb4b20 100644 --- a/frontend/packages/core/components/SamplePage/SampleChart.tsx +++ b/frontend/packages/core/components/SamplePage/SampleChart.tsx @@ -1,5 +1,5 @@ import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ellipsis, em, primaryColor, rem, size, textLightColor} from '~/utils/style'; +import {ellipsis, em, primaryColor, rem, size, textLightColor, textLighterColor} from '~/utils/style'; import ChartToolbox from '~/components/ChartToolbox'; import GridLoader from 'react-spinners/GridLoader'; @@ -78,6 +78,16 @@ const Footer = styled.div` justify-content: space-between; `; +const FooterInfo = styled.div` + color: ${textLighterColor}; + font-size: ${rem(12)}; + + > * { + display: inline-block; + margin-left: ${rem(10)}; + } +`; + type SampleData = { step: number; wallTime: number; @@ -221,7 +231,14 @@ const SampleChart: FunctionComponent = ({run, tag, running, ty } ]} /> - {footer} + + + {t('sample:sample')} + {t('common:colon')} + {data?.length ? `${step + 1}/${data.length}` : '--/--'} + + {footer} + ); diff --git a/frontend/packages/core/public/locales/en/sample.json b/frontend/packages/core/public/locales/en/sample.json index 135329ea5570b697b1ca609764c7142ccf3fa712..78f7bc89921468fe0a3de09f1aaefca3194ba406 100644 --- a/frontend/packages/core/public/locales/en/sample.json +++ b/frontend/packages/core/public/locales/en/sample.json @@ -5,6 +5,7 @@ "download-audio": "Download $t(audio)", "download-image": "Download $t(image)", "image": "image", + "sample": "Sample", "sample-rate": "Sample Rate", "show-actual-size": "Show Actual Image Size", "step": "Step", diff --git a/frontend/packages/core/public/locales/zh/sample.json b/frontend/packages/core/public/locales/zh/sample.json index 1b5cc729b374302a305264160eb26f8206f66760..bab20f3b9dc8602a90394d5dacbd9dde82d3744b 100644 --- a/frontend/packages/core/public/locales/zh/sample.json +++ b/frontend/packages/core/public/locales/zh/sample.json @@ -5,6 +5,7 @@ "download-audio": "下载$t(audio)", "download-image": "下载$t(image)", "image": "图片", + "sample": "样本", "sample-rate": "采样率", "show-actual-size": "按真实大小展示", "step": "Step", diff --git a/frontend/packages/core/utils/audio.ts b/frontend/packages/core/utils/audio.ts index 420ffb737cbd67c8867439514d346f51cfa142bc..7d42ddd1b1ca9da4788e584e2a6d73330e91fa81 100644 --- a/frontend/packages/core/utils/audio.ts +++ b/frontend/packages/core/utils/audio.ts @@ -9,6 +9,7 @@ export class AudioPlayer { private gain: GainNode; private source: AudioBufferSourceNode | null = null; private buffer: AudioBuffer | null = null; + private decodedSampleRate: number = Number.NaN; private startAt = 0; private stopAt = 0; @@ -38,7 +39,7 @@ export class AudioPlayer { if (!this.buffer) { return Number.NaN; } - return this.buffer.sampleRate; + return Number.isNaN(this.decodedSampleRate) ? this.buffer.sampleRate : this.decodedSampleRate; } get volumn() { @@ -75,8 +76,25 @@ export class AudioPlayer { } } - load(buffer: ArrayBuffer) { + static getWavSampleRate(buffer: ArrayBuffer) { + const intArr = new Int8Array(buffer); + const sampleRateArr = intArr.slice(24, 28); + return ( + (sampleRateArr[0] & 0xff) | + ((sampleRateArr[1] & 0xff) << 8) | + ((sampleRateArr[2] & 0xff) << 16) | + ((sampleRateArr[3] & 0xff) << 24) + ); + } + + load(buffer: ArrayBuffer, type?: string) { this.reset(); + if (type === 'audio/wave') { + this.decodedSampleRate = AudioPlayer.getWavSampleRate(buffer); + } else { + this.decodedSampleRate = Number.NaN; + } + return this.context.decodeAudioData(buffer, audioBuffer => { this.buffer = audioBuffer; }); diff --git a/frontend/packages/core/utils/style.ts b/frontend/packages/core/utils/style.ts index ca7999ee673b063ac92919e8296516bdbbf036c2..fb2abe8621835dbf85e27a026125c1257c21a2cc 100644 --- a/frontend/packages/core/utils/style.ts +++ b/frontend/packages/core/utils/style.ts @@ -14,9 +14,13 @@ export {default as styled} from 'styled-components'; export * from 'styled-components'; export * from 'polished'; // rename conflict shorthands -export {borderRadius as borderRadiusShortHand, borderColor as borderColorShortHand} from 'polished'; +export { + borderRadius as borderRadiusShortHand, + borderColor as borderColorShortHand, + fontFace as fontFaceShortHand +} from 'polished'; -const {math, size, lighten, darken, normalize, fontFace, transitions, border, position} = polished; +const {math, size, lighten, darken, normalize, transitions, border, position} = polished; export const iconFontPath = `${PUBLIC_PATH}/style/fonts/vdl-icon`; @@ -97,6 +101,36 @@ export const transitionProps = (props: string | string[], args?: string | {durat } return transitions(props, args); }; +export const fontFace = ({queryString, ...args}: {queryString?: string} & Parameters[0]) => { + const formatMap: Record = { + eot: 'embedded-opentype', + woff2: 'woff2', + woff: 'woff', + ttf: 'truetype', + svg: 'svg' + }; + + const f = polished.fontFace(args); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const src: string = (f['@font-face'] as any).src; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (f['@font-face'] as any).src = src.replace(/url\(['"](.*?)\.(\w*?)['"]\)/g, (_, path: string, format: string) => { + let replace = `url("${path}.${format}`; + if (queryString) { + replace += `?${queryString}`; + } + if (format === 'svg') { + replace += `#${args.fontFamily}`; + } + replace += '")'; + if (formatMap[format]) { + replace += ` format("${formatMap[format]}")`; + } + return replace; + }); + return f; +}; export const link = css` a { color: ${primaryColor}; @@ -131,6 +165,7 @@ export const GlobalStyle = createGlobalStyle` ${normalize} ${fontFace({ + queryString: 'wxo6ka', fontFamily: 'vdl-icon', fontFilePath: iconFontPath, fileFormats: ['ttf', 'woff', 'svg'],