未验证 提交 a31d235d 编写于 作者: P Peter Pan 提交者: GitHub

minor bug fix in sample pages (#721)

* fix: sample rate display error

* bump 2.0.0-beta.8

* feat: decode mp3 audio sample rate

* fix: compatibility of safari

* chore: use shared audio context to decrease memory usage

* fix: chinese translation

* fix: formalize volumn slider in different browser

* fix: display error when chart maximized
上级 19311009
......@@ -7,6 +7,7 @@ import {
primaryColor,
primaryFocusedColor,
rem,
size,
textLightColor,
textLighterColor
} from '~/utils/style';
......@@ -15,6 +16,7 @@ import {AudioPlayer} from '~/utils/audio';
import Icon from '~/components/Icon';
import PuffLoader from 'react-spinners/PuffLoader';
import RangeSlider from '~/components/RangeSlider';
import Slider from 'react-rangeslider';
import SyncLoader from 'react-spinners/SyncLoader';
import Tippy from '@tippyjs/react';
import mime from 'mime-types';
......@@ -69,19 +71,55 @@ const Container = styled.div`
}
`;
const VolumnSlider = styled.input.attrs(() => ({
type: 'range',
orient: 'vertical',
min: 0,
max: 100,
step: 1
}))`
writing-mode: bt-lr;
-webkit-appearance: slider-vertical;
const VolumnSlider = styled(Slider)`
margin: ${rem(15)} ${rem(18)};
width: ${rem(4)};
height: ${rem(100)};
cursor: pointer;
position: relative;
background-color: #dbdeeb;
outline: none;
border-radius: ${rem(2)};
user-select: none;
--color: ${primaryColor};
&:hover {
--color: ${primaryFocusedColor};
}
&:active {
--color: ${primaryActiveColor};
}
.rangeslider__fill {
background-color: var(--color);
position: absolute;
bottom: 0;
width: 100%;
border-bottom-left-radius: ${rem(2)};
border-bottom-right-radius: ${rem(2)};
border-top: ${rem(4)} solid var(--color);
box-sizing: content-box;
}
.rangeslider__handle {
background-color: var(--color);
${size(rem(8), rem(8))}
position: absolute;
left: -${rem(2)};
border-radius: 50%;
outline: none;
.rangeslider__handle-tooltip,
.rangeslider__handle-label {
display: none;
}
}
.rangeslider__labels {
display: none;
}
`;
const SLIDER_MAX = 100;
......@@ -98,13 +136,15 @@ export type AudioRef = {
};
export type AudioProps = {
audioContext?: AudioContext;
src?: string;
cache?: number;
onLoading?: () => unknown;
onLoad?: (audio: {sampleRate: number; duration: number}) => unknown;
};
const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(({src, cache, onLoading, onLoad, className}, ref) => {
const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(
({audioContext, src, cache, onLoading, onLoad, className}, ref) => {
const {t} = useTranslation('common');
const {data, error, loading} = useRequest<BlobResponse>(src ?? null, blobFetcher, {
......@@ -200,6 +240,7 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(({src, cache,
setSliderValue(0);
setDuration('00:00');
p = new AudioPlayer({
context: audioContext,
onplay: () => {
setPlaying(true);
startTimer();
......@@ -210,7 +251,7 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(({src, cache,
}
});
const buffer = await data.data.arrayBuffer();
await p.load(buffer, data.type ?? undefined);
await p.load(buffer, data.type != null ? mime.extension(data.type) || undefined : undefined);
setDecoding(false);
setDuration(formatDuration(p.duration));
onLoad?.({sampleRate: p.sampleRate, duration: p.duration});
......@@ -225,7 +266,7 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(({src, cache,
}
};
}
}, [data, startTimer, stopTimer, onLoading, onLoad]);
}, [data, startTimer, stopTimer, onLoading, onLoad, audioContext]);
const volumnIcon = useMemo(() => {
if (volumn === 0) {
......@@ -270,7 +311,16 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(({src, cache,
animation="shift-away-subtle"
interactive
hideOnClick={false}
content={<VolumnSlider value={volumn} onChange={e => setVolumn(+e.target.value)} />}
content={
<VolumnSlider
value={volumn}
min={0}
max={100}
step={1}
onChange={setVolumn}
orientation="vertical"
/>
}
>
<a className="control volumn" onClick={toggleMute}>
<Icon type={volumnIcon} />
......@@ -278,6 +328,7 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(({src, cache,
</Tippy>
</Container>
);
});
}
);
export default Audio;
......@@ -56,7 +56,12 @@ const Chart: FunctionComponent<ChartProps & WithStyled> = ({cid, width, height,
}, [toggleMaximze]);
return (
<Div maximized={maximized} width={width} height={height} className={className}>
<Div
maximized={maximized}
width={width}
height={height}
className={`${maximized ? 'maximized' : ''} ${className ?? ''}`}
>
{children}
</Div>
);
......
......@@ -29,6 +29,10 @@ const Wrapper = styled.div`
margin: 0 ${rem(20)} ${rem(20)} 0;
flex-shrink: 0;
flex-grow: 0;
&.maximized {
margin-right: 0;
}
}
`;
......
......@@ -45,7 +45,6 @@ const Wrapper = styled.div<{disabled?: boolean}>`
&__track {
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
${transitionProps('width', {duration: '30ms'})}
&--background {
${size(railHeight, '100%')}
......@@ -67,7 +66,6 @@ const Wrapper = styled.div<{disabled?: boolean}>`
&__slider-container {
top: -${half(`${thumbSize} - ${railHeight}`)};
margin-left: -${half(thumbSize)};
${transitionProps('left', {duration: '30ms'})}
}
&__slider {
......
......@@ -3,22 +3,24 @@ import React, {FunctionComponent, useCallback, useState} from 'react';
import SampleChart, {SampleChartBaseProps} from '~/components/SamplePage/SampleChart';
import {format} from 'd3-format';
import {size} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const formatter = format('.5~s');
const StyledAudio = styled(Audio)`
${size('100%')}
width: 100%;
flex-shrink: 1;
align-self: stretch;
`;
const cache = 5 * 60 * 1000;
type AudioChartProps = SampleChartBaseProps;
type AudioChartProps = {
audioContext?: AudioContext;
} & SampleChartBaseProps;
const AudioChart: FunctionComponent<AudioChartProps> = ({...props}) => {
const AudioChart: FunctionComponent<AudioChartProps> = ({audioContext, ...props}) => {
const {t} = useTranslation(['sample', 'common']);
const [sampleRate, setSampleRate] = useState<string>('--Hz');
......@@ -31,9 +33,16 @@ const AudioChart: FunctionComponent<AudioChartProps> = ({...props}) => {
const content = useCallback(
(ref: React.RefObject<AudioRef>, src: string) => (
<StyledAudio src={src} cache={cache} onLoading={onLoading} onLoad={onLoad} ref={ref} />
<StyledAudio
audioContext={audioContext}
src={src}
cache={cache}
onLoading={onLoading}
onLoad={onLoad}
ref={ref}
/>
),
[onLoading, onLoad]
[onLoading, onLoad, audioContext]
);
return (
......
......@@ -56,6 +56,7 @@
"react-dom": "16.13.1",
"react-input-range": "1.3.0",
"react-is": "16.13.1",
"react-rangeslider": "2.2.0",
"react-spinners": "0.9.0",
"react-toastify": "6.0.8",
"save-svg-as-png": "1.4.17",
......@@ -77,6 +78,7 @@
"@types/nprogress": "0.2.0",
"@types/react": "16.9.43",
"@types/react-dom": "16.9.8",
"@types/react-rangeslider": "2.2.3",
"@types/styled-components": "5.1.1",
"@visualdl/mock": "2.0.0-beta.49",
"babel-plugin-emotion": "10.0.33",
......
......@@ -2,7 +2,7 @@
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useMemo, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import useTagFilter, {ungroup} from '~/hooks/useTagFilter';
import AudioChart from '~/components/SamplePage/AudioChart';
......@@ -20,6 +20,19 @@ const chartSize = {
const Audio: NextI18NextPage = () => {
const {t} = useTranslation(['sample', 'common']);
const audioContext = useRef<AudioContext>();
useEffect(() => {
if (process.browser) {
// safari only has webkitAudioContext
const AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
audioContext.current = new AudioContext();
return () => {
audioContext.current?.close();
};
}
}, []);
const [running, setRunning] = useState(true);
const {runs, tags, selectedRuns, onChangeRuns, loadingRuns, loadingTags} = useTagFilter('audio', running);
......@@ -41,7 +54,7 @@ const Audio: NextI18NextPage = () => {
);
const withChart = useCallback<WithChart<typeof ungroupedSelectedTags[number]>>(
({run, label}) => <AudioChart run={run} tag={label} running={running} />,
({run, label}) => <AudioChart audioContext={audioContext.current} run={run} tag={label} running={running} />,
[running]
);
......
{
"audio": "语音",
"audio": "音频",
"cancel": "取消",
"close": "关闭",
"colon": ":",
......
......@@ -4,8 +4,10 @@ declare global {
}
namespace globalThis {
// eslint-disable-next-line no-var
/* eslint-disable no-var */
var __visualdl_instance_id__: string | string[] | undefined;
var webkitAudioContext: AudioContext | undefined;
/* eslint-enable no-var */
}
}
......
interface AudioPlayerOptions {
volumn: number;
context?: AudioContext;
volumn?: number;
onplay?: () => void;
onstop?: () => void;
}
......@@ -10,6 +11,7 @@ export class AudioPlayer {
private source: AudioBufferSourceNode | null = null;
private buffer: AudioBuffer | null = null;
private decodedSampleRate: number = Number.NaN;
private contextFromOptions: boolean;
private startAt = 0;
private stopAt = 0;
......@@ -19,7 +21,7 @@ export class AudioPlayer {
public playing = false;
public readonly options: AudioPlayerOptions;
public readonly options: Required<AudioPlayerOptions>;
get current() {
if (this.playing) {
......@@ -39,7 +41,7 @@ export class AudioPlayer {
if (!this.buffer) {
return Number.NaN;
}
return Number.isNaN(this.decodedSampleRate) ? this.buffer.sampleRate : this.decodedSampleRate;
return this.decodedSampleRate;
}
get volumn() {
......@@ -54,12 +56,16 @@ export class AudioPlayer {
this.gain.gain.value = value / 100;
}
constructor(options?: Partial<AudioPlayerOptions>) {
constructor(options?: AudioPlayerOptions) {
this.options = {
context: options?.context ?? new AudioContext(),
volumn: 100,
onplay: () => void 0,
onstop: () => void 0,
...options
};
this.context = new AudioContext();
this.contextFromOptions = !!options?.context;
this.context = this.options.context;
this.gain = this.context.createGain();
this.volumn = this.options.volumn;
}
......@@ -87,16 +93,69 @@ export class AudioPlayer {
);
}
static getMp3SampleRate(buffer: ArrayBuffer) {
let arr = new Uint8Array(buffer);
if (String.fromCharCode.apply(null, Array.from(arr.slice(0, 3))) === 'ID3') {
arr = arr.slice(10);
let i = 0;
while (arr[i] !== 0x00) {
const size = arr[i + 4] * 0x100000000 + arr[i + 5] * 0x10000 + arr[i + 6] * 0x100 + arr[i + 7];
i += 10 + size;
}
}
let j = 0;
while (arr[j++] !== 0xff) {}
j--;
const header = arr.slice(j, j + 4);
const version = (header[1] & 0b00011000) >> 3;
const sampleRate = (header[2] & 0b00001100) >> 2;
if (version === 0b11) {
if (sampleRate === 0b00) {
return 44100;
} else if (sampleRate === 0b01) {
return 48000;
} else if (sampleRate === 0b10) {
return 32000;
}
} else if (version === 0b10) {
if (sampleRate === 0b00) {
return 22050;
} else if (sampleRate === 0b01) {
return 24000;
} else if (sampleRate === 0b10) {
return 16000;
}
} else if (version === 0b00) {
if (sampleRate === 0b00) {
return 11025;
} else if (sampleRate === 0b01) {
return 12000;
} else if (sampleRate === 0b10) {
return 8000;
}
}
return Number.NaN;
}
load(buffer: ArrayBuffer, type?: string) {
this.reset();
if (type === 'audio/wave') {
if (type === 'wav') {
this.decodedSampleRate = AudioPlayer.getWavSampleRate(buffer);
} else if (type === 'mpga' || type === 'mp3') {
this.decodedSampleRate = AudioPlayer.getMp3SampleRate(buffer);
} else {
this.decodedSampleRate = Number.NaN;
}
return this.context.decodeAudioData(buffer, audioBuffer => {
// safari doesn't return promise here
return new Promise<void>((resolve, reject) => {
this.context.decodeAudioData(
buffer,
audioBuffer => {
this.buffer = audioBuffer;
resolve();
},
reject
);
});
}
......@@ -111,7 +170,7 @@ export class AudioPlayer {
this.stopAt = this.context.currentTime;
this.offset += this.stopAt - this.startAt;
this.playing = false;
this.options.onstop?.();
this.options.onstop();
this.source = null;
});
if (this.offset >= this.duration) {
......@@ -120,7 +179,7 @@ export class AudioPlayer {
this.source.start(0, this.offset);
this.startAt = this.context.currentTime;
this.playing = true;
this.options.onplay?.();
this.options.onplay();
}
pause() {
......@@ -159,7 +218,9 @@ export class AudioPlayer {
dispose() {
this.reset();
if (!this.contextFromOptions) {
this.context.close();
}
this.source = null;
this.buffer = null;
}
......
......@@ -49,6 +49,17 @@ export const blobFetcher = async (url: string, options?: RequestInit): Promise<B
const res = await fetch(process.env.API_URL + url, addApiToken(options));
const data = await res.blob();
const disposition = res.headers.get('Content-Disposition');
// support safari
if (!data.arrayBuffer) {
data.arrayBuffer = async () =>
new Promise<ArrayBuffer>((resolve, reject) => {
const fileReader = new FileReader();
fileReader.addEventListener('load', e =>
e.target ? resolve(e.target.result as ArrayBuffer) : reject()
);
fileReader.readAsArrayBuffer(data);
});
}
let filename: string | null = null;
if (disposition && disposition.indexOf('attachment') !== -1) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
......
......@@ -279,6 +279,8 @@ export const GlobalStyle = createGlobalStyle`
> .tippy-content {
padding: 0;
/* trigger bfc */
display: flow-root;
}
&[data-placement^='top'] > .tippy-arrow::before {
......
......@@ -2676,6 +2676,13 @@
dependencies:
"@types/react" "*"
"@types/react-rangeslider@2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@types/react-rangeslider/-/react-rangeslider-2.2.3.tgz#b116483679b720f19fadf793067f3dd41b4f15e6"
integrity sha512-2LNR2FIKqSks7bSx4RWN3nCPR39ABG5cZUGRY5R0wLuEiMB6s6TWVxhfQa+5rm+mMhsdMGDB6Bm5pi6zq8kf8A==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@16.9.43":
version "16.9.43"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.43.tgz#c287f23f6189666ee3bebc2eb8d0f84bcb6cdb6b"
......@@ -4381,7 +4388,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@2.2.6, classnames@^2.2.6:
classnames@2.2.6, classnames@^2.2.3, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
......@@ -11935,6 +11942,14 @@ react-is@16.13.1, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-rangeslider@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-rangeslider/-/react-rangeslider-2.2.0.tgz#4362b01f4f5a455f0815d371d496f69ca4c6b5aa"
integrity sha512-5K7Woa+cyqZ5wiW5+KhqGV+3+FiFxGKQ9rUxTMh52sObXVYEeBbfxFrp1eBvS8mRIxnUbHz9ppnFP0LhwOyNeg==
dependencies:
classnames "^2.2.3"
resize-observer-polyfill "^1.4.2"
react-refresh@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
......@@ -12359,6 +12374,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.4.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
......
......@@ -13,4 +13,4 @@
# limitations under the License.
# =======================================================================
vdl_version = '2.0.0-beta.7'
vdl_version = '2.0.0-beta.8'
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册