未验证 提交 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 { ...@@ -7,6 +7,7 @@ import {
primaryColor, primaryColor,
primaryFocusedColor, primaryFocusedColor,
rem, rem,
size,
textLightColor, textLightColor,
textLighterColor textLighterColor
} from '~/utils/style'; } from '~/utils/style';
...@@ -15,6 +16,7 @@ import {AudioPlayer} from '~/utils/audio'; ...@@ -15,6 +16,7 @@ import {AudioPlayer} from '~/utils/audio';
import Icon from '~/components/Icon'; import Icon from '~/components/Icon';
import PuffLoader from 'react-spinners/PuffLoader'; import PuffLoader from 'react-spinners/PuffLoader';
import RangeSlider from '~/components/RangeSlider'; import RangeSlider from '~/components/RangeSlider';
import Slider from 'react-rangeslider';
import SyncLoader from 'react-spinners/SyncLoader'; import SyncLoader from 'react-spinners/SyncLoader';
import Tippy from '@tippyjs/react'; import Tippy from '@tippyjs/react';
import mime from 'mime-types'; import mime from 'mime-types';
...@@ -69,19 +71,55 @@ const Container = styled.div` ...@@ -69,19 +71,55 @@ const Container = styled.div`
} }
`; `;
const VolumnSlider = styled.input.attrs(() => ({ const VolumnSlider = styled(Slider)`
type: 'range',
orient: 'vertical',
min: 0,
max: 100,
step: 1
}))`
writing-mode: bt-lr;
-webkit-appearance: slider-vertical;
margin: ${rem(15)} ${rem(18)}; margin: ${rem(15)} ${rem(18)};
width: ${rem(4)}; width: ${rem(4)};
height: ${rem(100)}; height: ${rem(100)};
cursor: pointer; 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; const SLIDER_MAX = 100;
...@@ -98,186 +136,199 @@ export type AudioRef = { ...@@ -98,186 +136,199 @@ export type AudioRef = {
}; };
export type AudioProps = { export type AudioProps = {
audioContext?: AudioContext;
src?: string; src?: string;
cache?: number; cache?: number;
onLoading?: () => unknown; onLoading?: () => unknown;
onLoad?: (audio: {sampleRate: number; duration: number}) => 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>(
const {t} = useTranslation('common'); ({audioContext, src, cache, onLoading, onLoad, className}, ref) => {
const {t} = useTranslation('common');
const {data, error, loading} = useRequest<BlobResponse>(src ?? null, blobFetcher, { const {data, error, loading} = useRequest<BlobResponse>(src ?? null, blobFetcher, {
dedupingInterval: cache ?? 2000 dedupingInterval: cache ?? 2000
}); });
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
save: (filename: string) => { save: (filename: string) => {
if (data) { if (data) {
const ext = data.type ? mime.extension(data.type) : null; const ext = data.type ? mime.extension(data.type) : null;
saveAs(data.data, filename.replace(/[/\\?%*:|"<>]/g, '_') + (ext ? `.${ext}` : '')); saveAs(data.data, filename.replace(/[/\\?%*:|"<>]/g, '_') + (ext ? `.${ext}` : ''));
}
} }
} }));
}));
const timer = useRef<number | null>(null);
const player = useRef<AudioPlayer | null>(null);
const [sliderValue, setSliderValue] = useState(0);
const [offset, setOffset] = useState(0);
const [duration, setDuration] = useState('00:00');
const [decoding, setDecoding] = useState(false);
const [playing, setPlaying] = useState(false);
const [volumn, setVolumn] = useState(100);
const [playAfterSeek, setPlayAfterSeek] = useState(false);
const play = useCallback(() => player.current?.play(), []);
const pause = useCallback(() => player.current?.pause(), []);
const toggle = useCallback(() => player.current?.toggle(), []);
const change = useCallback((value: number) => {
if (!player.current) {
return;
}
setOffset((value / SLIDER_MAX) * player.current.duration);
setSliderValue(value);
}, []);
const startSeek = useCallback(() => {
setPlayAfterSeek(playing);
pause();
}, [playing, pause]);
const stopSeek = useCallback(() => {
if (!player.current) {
return;
}
player.current.seek(offset);
if (playAfterSeek && offset < player.current.duration) {
play();
}
}, [play, offset, playAfterSeek]);
const toggleMute = useCallback(() => {
if (player.current) {
player.current.toggleMute();
setVolumn(player.current.volumn);
}
}, []);
const tick = useCallback(() => { const timer = useRef<number | null>(null);
if (player.current) { const player = useRef<AudioPlayer | null>(null);
const current = player.current.current; const [sliderValue, setSliderValue] = useState(0);
setOffset(current); const [offset, setOffset] = useState(0);
setSliderValue(Math.floor((current / player.current.duration) * SLIDER_MAX)); const [duration, setDuration] = useState('00:00');
} const [decoding, setDecoding] = useState(false);
}, []); const [playing, setPlaying] = useState(false);
const startTimer = useCallback(() => { const [volumn, setVolumn] = useState(100);
tick(); const [playAfterSeek, setPlayAfterSeek] = useState(false);
timer.current = (globalThis.setInterval(tick, 250) as unknown) as number;
}, [tick]); const play = useCallback(() => player.current?.play(), []);
const stopTimer = useCallback(() => { const pause = useCallback(() => player.current?.pause(), []);
if (player.current) { const toggle = useCallback(() => player.current?.toggle(), []);
if (player.current.current >= player.current.duration) { const change = useCallback((value: number) => {
tick(); if (!player.current) {
return;
} }
} setOffset((value / SLIDER_MAX) * player.current.duration);
if (timer.current) { setSliderValue(value);
globalThis.clearInterval(timer.current); }, []);
timer.current = null; const startSeek = useCallback(() => {
} setPlayAfterSeek(playing);
}, [tick]); pause();
}, [playing, pause]);
const stopSeek = useCallback(() => {
if (!player.current) {
return;
}
player.current.seek(offset);
if (playAfterSeek && offset < player.current.duration) {
play();
}
}, [play, offset, playAfterSeek]);
const toggleMute = useCallback(() => {
if (player.current) {
player.current.toggleMute();
setVolumn(player.current.volumn);
}
}, []);
useEffect(() => { const tick = useCallback(() => {
if (player.current) { if (player.current) {
player.current.volumn = volumn; const current = player.current.current;
} setOffset(current);
}, [volumn]); setSliderValue(Math.floor((current / player.current.duration) * SLIDER_MAX));
useEffect(() => {
if (process.browser) {
let p: AudioPlayer | null = null;
if (data) {
(async () => {
setDecoding(true);
onLoading?.();
setOffset(0);
setSliderValue(0);
setDuration('00:00');
p = new AudioPlayer({
onplay: () => {
setPlaying(true);
startTimer();
},
onstop: () => {
setPlaying(false);
stopTimer();
}
});
const buffer = await data.data.arrayBuffer();
await p.load(buffer, data.type ?? undefined);
setDecoding(false);
setDuration(formatDuration(p.duration));
onLoad?.({sampleRate: p.sampleRate, duration: p.duration});
player.current = p;
})();
} }
return () => { }, []);
if (p) { const startTimer = useCallback(() => {
setPlaying(false); tick();
p.dispose(); timer.current = (globalThis.setInterval(tick, 250) as unknown) as number;
player.current = null; }, [tick]);
const stopTimer = useCallback(() => {
if (player.current) {
if (player.current.current >= player.current.duration) {
tick();
} }
}; }
} if (timer.current) {
}, [data, startTimer, stopTimer, onLoading, onLoad]); globalThis.clearInterval(timer.current);
timer.current = null;
}
}, [tick]);
const volumnIcon = useMemo(() => { useEffect(() => {
if (volumn === 0) { if (player.current) {
return 'mute'; player.current.volumn = volumn;
} }
if (volumn <= 50) { }, [volumn]);
return 'volumn-low';
}
return 'volumn';
}, [volumn]);
if (loading) { useEffect(() => {
return <SyncLoader color={primaryColor} size="15px" />; if (process.browser) {
} let p: AudioPlayer | null = null;
if (data) {
(async () => {
setDecoding(true);
onLoading?.();
setOffset(0);
setSliderValue(0);
setDuration('00:00');
p = new AudioPlayer({
context: audioContext,
onplay: () => {
setPlaying(true);
startTimer();
},
onstop: () => {
setPlaying(false);
stopTimer();
}
});
const buffer = await data.data.arrayBuffer();
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});
player.current = p;
})();
}
return () => {
if (p) {
setPlaying(false);
p.dispose();
player.current = null;
}
};
}
}, [data, startTimer, stopTimer, onLoading, onLoad, audioContext]);
if (error) { const volumnIcon = useMemo(() => {
return <div>{t('common:error')}</div>; if (volumn === 0) {
} return 'mute';
}
if (volumn <= 50) {
return 'volumn-low';
}
return 'volumn';
}, [volumn]);
return ( if (loading) {
<Container className={className}> return <SyncLoader color={primaryColor} size="15px" />;
<a className={`control ${decoding ? 'disabled' : ''}`} onClick={toggle}> }
{decoding ? <PuffLoader size="16px" /> : <Icon type={playing ? 'pause' : 'play'} />}
</a> if (error) {
<div className="slider"> return <div>{t('common:error')}</div>;
<RangeSlider }
min={0}
max={SLIDER_MAX} return (
step={1} <Container className={className}>
value={sliderValue} <a className={`control ${decoding ? 'disabled' : ''}`} onClick={toggle}>
disabled={decoding} {decoding ? <PuffLoader size="16px" /> : <Icon type={playing ? 'pause' : 'play'} />}
onChange={change}
onChangeStart={startSeek}
onChangeComplete={stopSeek}
/>
</div>
<span className="time">
{formatDuration(offset)}/{duration}
</span>
<Tippy
placement="top"
animation="shift-away-subtle"
interactive
hideOnClick={false}
content={<VolumnSlider value={volumn} onChange={e => setVolumn(+e.target.value)} />}
>
<a className="control volumn" onClick={toggleMute}>
<Icon type={volumnIcon} />
</a> </a>
</Tippy> <div className="slider">
</Container> <RangeSlider
); min={0}
}); max={SLIDER_MAX}
step={1}
value={sliderValue}
disabled={decoding}
onChange={change}
onChangeStart={startSeek}
onChangeComplete={stopSeek}
/>
</div>
<span className="time">
{formatDuration(offset)}/{duration}
</span>
<Tippy
placement="top"
animation="shift-away-subtle"
interactive
hideOnClick={false}
content={
<VolumnSlider
value={volumn}
min={0}
max={100}
step={1}
onChange={setVolumn}
orientation="vertical"
/>
}
>
<a className="control volumn" onClick={toggleMute}>
<Icon type={volumnIcon} />
</a>
</Tippy>
</Container>
);
}
);
export default Audio; export default Audio;
...@@ -56,7 +56,12 @@ const Chart: FunctionComponent<ChartProps & WithStyled> = ({cid, width, height, ...@@ -56,7 +56,12 @@ const Chart: FunctionComponent<ChartProps & WithStyled> = ({cid, width, height,
}, [toggleMaximze]); }, [toggleMaximze]);
return ( return (
<Div maximized={maximized} width={width} height={height} className={className}> <Div
maximized={maximized}
width={width}
height={height}
className={`${maximized ? 'maximized' : ''} ${className ?? ''}`}
>
{children} {children}
</Div> </Div>
); );
......
...@@ -29,6 +29,10 @@ const Wrapper = styled.div` ...@@ -29,6 +29,10 @@ const Wrapper = styled.div`
margin: 0 ${rem(20)} ${rem(20)} 0; margin: 0 ${rem(20)} ${rem(20)} 0;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
&.maximized {
margin-right: 0;
}
} }
`; `;
......
...@@ -45,7 +45,6 @@ const Wrapper = styled.div<{disabled?: boolean}>` ...@@ -45,7 +45,6 @@ const Wrapper = styled.div<{disabled?: boolean}>`
&__track { &__track {
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
${transitionProps('width', {duration: '30ms'})}
&--background { &--background {
${size(railHeight, '100%')} ${size(railHeight, '100%')}
...@@ -67,7 +66,6 @@ const Wrapper = styled.div<{disabled?: boolean}>` ...@@ -67,7 +66,6 @@ const Wrapper = styled.div<{disabled?: boolean}>`
&__slider-container { &__slider-container {
top: -${half(`${thumbSize} - ${railHeight}`)}; top: -${half(`${thumbSize} - ${railHeight}`)};
margin-left: -${half(thumbSize)}; margin-left: -${half(thumbSize)};
${transitionProps('left', {duration: '30ms'})}
} }
&__slider { &__slider {
......
...@@ -3,22 +3,24 @@ import React, {FunctionComponent, useCallback, useState} from 'react'; ...@@ -3,22 +3,24 @@ import React, {FunctionComponent, useCallback, useState} from 'react';
import SampleChart, {SampleChartBaseProps} from '~/components/SamplePage/SampleChart'; import SampleChart, {SampleChartBaseProps} from '~/components/SamplePage/SampleChart';
import {format} from 'd3-format'; import {format} from 'd3-format';
import {size} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
const formatter = format('.5~s'); const formatter = format('.5~s');
const StyledAudio = styled(Audio)` const StyledAudio = styled(Audio)`
${size('100%')} width: 100%;
flex-shrink: 1; flex-shrink: 1;
align-self: stretch;
`; `;
const cache = 5 * 60 * 1000; 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 {t} = useTranslation(['sample', 'common']);
const [sampleRate, setSampleRate] = useState<string>('--Hz'); const [sampleRate, setSampleRate] = useState<string>('--Hz');
...@@ -31,9 +33,16 @@ const AudioChart: FunctionComponent<AudioChartProps> = ({...props}) => { ...@@ -31,9 +33,16 @@ const AudioChart: FunctionComponent<AudioChartProps> = ({...props}) => {
const content = useCallback( const content = useCallback(
(ref: React.RefObject<AudioRef>, src: string) => ( (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 ( return (
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-input-range": "1.3.0", "react-input-range": "1.3.0",
"react-is": "16.13.1", "react-is": "16.13.1",
"react-rangeslider": "2.2.0",
"react-spinners": "0.9.0", "react-spinners": "0.9.0",
"react-toastify": "6.0.8", "react-toastify": "6.0.8",
"save-svg-as-png": "1.4.17", "save-svg-as-png": "1.4.17",
...@@ -77,6 +78,7 @@ ...@@ -77,6 +78,7 @@
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/react": "16.9.43", "@types/react": "16.9.43",
"@types/react-dom": "16.9.8", "@types/react-dom": "16.9.8",
"@types/react-rangeslider": "2.2.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.1",
"@visualdl/mock": "2.0.0-beta.49", "@visualdl/mock": "2.0.0-beta.49",
"babel-plugin-emotion": "10.0.33", "babel-plugin-emotion": "10.0.33",
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import ChartPage, {WithChart} from '~/components/ChartPage'; import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n'; 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 useTagFilter, {ungroup} from '~/hooks/useTagFilter';
import AudioChart from '~/components/SamplePage/AudioChart'; import AudioChart from '~/components/SamplePage/AudioChart';
...@@ -20,6 +20,19 @@ const chartSize = { ...@@ -20,6 +20,19 @@ const chartSize = {
const Audio: NextI18NextPage = () => { const Audio: NextI18NextPage = () => {
const {t} = useTranslation(['sample', 'common']); 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 [running, setRunning] = useState(true);
const {runs, tags, selectedRuns, onChangeRuns, loadingRuns, loadingTags} = useTagFilter('audio', running); const {runs, tags, selectedRuns, onChangeRuns, loadingRuns, loadingTags} = useTagFilter('audio', running);
...@@ -41,7 +54,7 @@ const Audio: NextI18NextPage = () => { ...@@ -41,7 +54,7 @@ const Audio: NextI18NextPage = () => {
); );
const withChart = useCallback<WithChart<typeof ungroupedSelectedTags[number]>>( 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] [running]
); );
......
{ {
"audio": "语音", "audio": "音频",
"cancel": "取消", "cancel": "取消",
"close": "关闭", "close": "关闭",
"colon": ":", "colon": ":",
......
...@@ -4,8 +4,10 @@ declare global { ...@@ -4,8 +4,10 @@ declare global {
} }
namespace globalThis { namespace globalThis {
// eslint-disable-next-line no-var /* eslint-disable no-var */
var __visualdl_instance_id__: string | string[] | undefined; var __visualdl_instance_id__: string | string[] | undefined;
var webkitAudioContext: AudioContext | undefined;
/* eslint-enable no-var */
} }
} }
......
interface AudioPlayerOptions { interface AudioPlayerOptions {
volumn: number; context?: AudioContext;
volumn?: number;
onplay?: () => void; onplay?: () => void;
onstop?: () => void; onstop?: () => void;
} }
...@@ -10,6 +11,7 @@ export class AudioPlayer { ...@@ -10,6 +11,7 @@ export class AudioPlayer {
private source: AudioBufferSourceNode | null = null; private source: AudioBufferSourceNode | null = null;
private buffer: AudioBuffer | null = null; private buffer: AudioBuffer | null = null;
private decodedSampleRate: number = Number.NaN; private decodedSampleRate: number = Number.NaN;
private contextFromOptions: boolean;
private startAt = 0; private startAt = 0;
private stopAt = 0; private stopAt = 0;
...@@ -19,7 +21,7 @@ export class AudioPlayer { ...@@ -19,7 +21,7 @@ export class AudioPlayer {
public playing = false; public playing = false;
public readonly options: AudioPlayerOptions; public readonly options: Required<AudioPlayerOptions>;
get current() { get current() {
if (this.playing) { if (this.playing) {
...@@ -39,7 +41,7 @@ export class AudioPlayer { ...@@ -39,7 +41,7 @@ export class AudioPlayer {
if (!this.buffer) { if (!this.buffer) {
return Number.NaN; return Number.NaN;
} }
return Number.isNaN(this.decodedSampleRate) ? this.buffer.sampleRate : this.decodedSampleRate; return this.decodedSampleRate;
} }
get volumn() { get volumn() {
...@@ -54,12 +56,16 @@ export class AudioPlayer { ...@@ -54,12 +56,16 @@ export class AudioPlayer {
this.gain.gain.value = value / 100; this.gain.gain.value = value / 100;
} }
constructor(options?: Partial<AudioPlayerOptions>) { constructor(options?: AudioPlayerOptions) {
this.options = { this.options = {
context: options?.context ?? new AudioContext(),
volumn: 100, volumn: 100,
onplay: () => void 0,
onstop: () => void 0,
...options ...options
}; };
this.context = new AudioContext(); this.contextFromOptions = !!options?.context;
this.context = this.options.context;
this.gain = this.context.createGain(); this.gain = this.context.createGain();
this.volumn = this.options.volumn; this.volumn = this.options.volumn;
} }
...@@ -87,16 +93,69 @@ export class AudioPlayer { ...@@ -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) { load(buffer: ArrayBuffer, type?: string) {
this.reset(); this.reset();
if (type === 'audio/wave') { if (type === 'wav') {
this.decodedSampleRate = AudioPlayer.getWavSampleRate(buffer); this.decodedSampleRate = AudioPlayer.getWavSampleRate(buffer);
} else if (type === 'mpga' || type === 'mp3') {
this.decodedSampleRate = AudioPlayer.getMp3SampleRate(buffer);
} else { } else {
this.decodedSampleRate = Number.NaN; this.decodedSampleRate = Number.NaN;
} }
// safari doesn't return promise here
return this.context.decodeAudioData(buffer, audioBuffer => { return new Promise<void>((resolve, reject) => {
this.buffer = audioBuffer; this.context.decodeAudioData(
buffer,
audioBuffer => {
this.buffer = audioBuffer;
resolve();
},
reject
);
}); });
} }
...@@ -111,7 +170,7 @@ export class AudioPlayer { ...@@ -111,7 +170,7 @@ export class AudioPlayer {
this.stopAt = this.context.currentTime; this.stopAt = this.context.currentTime;
this.offset += this.stopAt - this.startAt; this.offset += this.stopAt - this.startAt;
this.playing = false; this.playing = false;
this.options.onstop?.(); this.options.onstop();
this.source = null; this.source = null;
}); });
if (this.offset >= this.duration) { if (this.offset >= this.duration) {
...@@ -120,7 +179,7 @@ export class AudioPlayer { ...@@ -120,7 +179,7 @@ export class AudioPlayer {
this.source.start(0, this.offset); this.source.start(0, this.offset);
this.startAt = this.context.currentTime; this.startAt = this.context.currentTime;
this.playing = true; this.playing = true;
this.options.onplay?.(); this.options.onplay();
} }
pause() { pause() {
...@@ -159,7 +218,9 @@ export class AudioPlayer { ...@@ -159,7 +218,9 @@ export class AudioPlayer {
dispose() { dispose() {
this.reset(); this.reset();
this.context.close(); if (!this.contextFromOptions) {
this.context.close();
}
this.source = null; this.source = null;
this.buffer = null; this.buffer = null;
} }
......
...@@ -49,6 +49,17 @@ export const blobFetcher = async (url: string, options?: RequestInit): Promise<B ...@@ -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 res = await fetch(process.env.API_URL + url, addApiToken(options));
const data = await res.blob(); const data = await res.blob();
const disposition = res.headers.get('Content-Disposition'); 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; let filename: string | null = null;
if (disposition && disposition.indexOf('attachment') !== -1) { if (disposition && disposition.indexOf('attachment') !== -1) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition); const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
......
...@@ -279,6 +279,8 @@ export const GlobalStyle = createGlobalStyle` ...@@ -279,6 +279,8 @@ export const GlobalStyle = createGlobalStyle`
> .tippy-content { > .tippy-content {
padding: 0; padding: 0;
/* trigger bfc */
display: flow-root;
} }
&[data-placement^='top'] > .tippy-arrow::before { &[data-placement^='top'] > .tippy-arrow::before {
......
...@@ -2676,6 +2676,13 @@ ...@@ -2676,6 +2676,13 @@
dependencies: dependencies:
"@types/react" "*" "@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": "@types/react@*", "@types/react@16.9.43":
version "16.9.43" version "16.9.43"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.43.tgz#c287f23f6189666ee3bebc2eb8d0f84bcb6cdb6b" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.43.tgz#c287f23f6189666ee3bebc2eb8d0f84bcb6cdb6b"
...@@ -4381,7 +4388,7 @@ class-utils@^0.3.5: ...@@ -4381,7 +4388,7 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" 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" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== 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- ...@@ -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" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== 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: react-refresh@0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
...@@ -12359,6 +12374,11 @@ requires-port@^1.0.0: ...@@ -12359,6 +12374,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= 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: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
......
...@@ -13,4 +13,4 @@ ...@@ -13,4 +13,4 @@
# limitations under the License. # 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.
先完成此消息的编辑!
想要评论请 注册