xref: /MusicFree/src/core/trackPlayer/index.ts (revision 095287552b9baf2f2ceeb9397c563c292a4f7934)
1import produce from 'immer';
2import ReactNativeTrackPlayer, {
3    Event,
4    State,
5    Track,
6    TrackMetadataBase,
7    usePlaybackState,
8    useProgress,
9} from 'react-native-track-player';
10import shuffle from 'lodash.shuffle';
11import Config from '../config';
12import {
13    EDeviceEvents,
14    internalFakeSoundKey,
15    sortIndexSymbol,
16    timeStampSymbol,
17} from '@/constants/commonConst';
18import {GlobalState} from '@/utils/stateMapper';
19import delay from '@/utils/delay';
20import {
21    isSameMediaItem,
22    mergeProps,
23    sortByTimestampAndIndex,
24} from '@/utils/mediaItem';
25import Network from '../network';
26import LocalMusicSheet from '../localMusicSheet';
27import {SoundAsset} from '@/constants/assetsConst';
28import {getQualityOrder} from '@/utils/qualities';
29import musicHistory from '../musicHistory';
30import getUrlExt from '@/utils/getUrlExt';
31import {DeviceEventEmitter} from 'react-native';
32import LyricManager from '../lyricManager';
33import {MusicRepeatMode} from './common';
34import {
35    getMusicIndex,
36    getPlayList,
37    getPlayListMusicAt,
38    isInPlayList,
39    isPlayListEmpty,
40    setPlayList,
41    usePlayList,
42} from './internal/playList';
43import {createMediaIndexMap} from '@/utils/mediaIndexMap';
44import PluginManager from '../pluginManager';
45import {musicIsPaused} from '@/utils/trackUtils';
46import Toast from '@/utils/toast';
47import {trace} from '@/utils/log';
48
49/** 当前播放 */
50const currentMusicStore = new GlobalState<IMusic.IMusicItem | null>(null);
51
52/** 播放模式 */
53const repeatModeStore = new GlobalState<MusicRepeatMode>(MusicRepeatMode.QUEUE);
54
55/** 音质 */
56const qualityStore = new GlobalState<IMusic.IQualityKey>('standard');
57
58let currentIndex = -1;
59
60// TODO: 下个版本最大限制调大一些
61const maxMusicQueueLength = 1500; // 当前播放最大限制
62const halfMaxMusicQueueLength = Math.floor(maxMusicQueueLength / 2);
63const shrinkPlayListToSize = (
64    queue: IMusic.IMusicItem[],
65    targetIndex = currentIndex,
66) => {
67    // 播放列表上限,太多无法缓存状态
68    if (queue.length > maxMusicQueueLength) {
69        if (targetIndex < halfMaxMusicQueueLength) {
70            queue = queue.slice(0, maxMusicQueueLength);
71        } else {
72            const right = Math.min(
73                queue.length,
74                targetIndex + halfMaxMusicQueueLength,
75            );
76            const left = Math.max(0, right - maxMusicQueueLength);
77            queue = queue.slice(left, right);
78        }
79    }
80    return queue;
81};
82
83let hasSetupListener = false;
84
85async function setupTrackPlayer() {
86    const config = Config.get('status.music') ?? {};
87    const {rate, repeatMode, musicQueue, progress, track} = config;
88
89    // 状态恢复
90    if (rate) {
91        await ReactNativeTrackPlayer.setRate(+rate / 100);
92    }
93
94    if (musicQueue && Array.isArray(musicQueue)) {
95        addAll(musicQueue, undefined, repeatMode === MusicRepeatMode.SHUFFLE);
96    }
97
98    const currentQuality =
99        Config.get('setting.basic.defaultPlayQuality') ?? 'standard';
100
101    if (track && isInPlayList(track)) {
102        const newSource = await PluginManager.getByMedia(
103            track,
104        )?.methods.getMediaSource(track, currentQuality, 0);
105        // 重新初始化 获取最新的链接
106        track.url = newSource?.url || track.url;
107        track.headers = newSource?.headers || track.headers;
108
109        await setTrackSource(track as Track, false);
110        setCurrentMusic(track);
111
112        if (config?.progress) {
113            await ReactNativeTrackPlayer.seekTo(progress!);
114        }
115    }
116
117    if (!hasSetupListener) {
118        ReactNativeTrackPlayer.addEventListener(
119            Event.PlaybackActiveTrackChanged,
120            async evt => {
121                if (
122                    evt.index === 1 &&
123                    evt.lastIndex === 0 &&
124                    evt.track?.$ === internalFakeSoundKey
125                ) {
126                    trace('队列末尾,播放下一首');
127                    if (repeatModeStore.getValue() === MusicRepeatMode.SINGLE) {
128                        await play(null, true);
129                    } else {
130                        // 当前生效的歌曲是下一曲的标记
131                        await skipToNext();
132                    }
133                }
134            },
135        );
136
137        ReactNativeTrackPlayer.addEventListener(
138            Event.PlaybackError,
139            async e => {
140                // WARNING: 不稳定,报错的时候有可能track已经变到下一首歌去了
141                if (
142                    (await ReactNativeTrackPlayer.getActiveTrackIndex()) ===
143                        0 &&
144                    e.message &&
145                    e.message !== 'android-io-file-not-found'
146                ) {
147                    trace('播放出错', {
148                        message: e.message,
149                        code: e.code,
150                    });
151                    failToPlay();
152                }
153            },
154        );
155
156        hasSetupListener = true;
157    }
158}
159
160/**
161 * 获取自动播放的下一个track
162 */
163const getFakeNextTrack = () => {
164    let track: Track | undefined;
165    const repeatMode = repeatModeStore.getValue();
166    if (repeatMode === MusicRepeatMode.SINGLE) {
167        // 单曲循环
168        track = getPlayListMusicAt(currentIndex) as Track;
169    } else {
170        // 下一曲
171        track = getPlayListMusicAt(currentIndex + 1) as Track;
172    }
173
174    if (track) {
175        return produce(track, _ => {
176            _.url = SoundAsset.fakeAudio;
177            _.$ = internalFakeSoundKey;
178        });
179    } else {
180        // 只有列表长度为0时才会出现的特殊情况
181        return {url: SoundAsset.fakeAudio, $: internalFakeSoundKey} as Track;
182    }
183};
184
185/** 播放失败时的情况 */
186async function failToPlay() {
187    // 如果自动跳转下一曲, 500s后自动跳转
188    if (!Config.get('setting.basic.autoStopWhenError')) {
189        await ReactNativeTrackPlayer.reset();
190        await delay(500);
191        await skipToNext();
192    }
193}
194
195// 播放模式相关
196const _toggleRepeatMapping = {
197    [MusicRepeatMode.SHUFFLE]: MusicRepeatMode.SINGLE,
198    [MusicRepeatMode.SINGLE]: MusicRepeatMode.QUEUE,
199    [MusicRepeatMode.QUEUE]: MusicRepeatMode.SHUFFLE,
200};
201/** 切换下一个模式 */
202const toggleRepeatMode = () => {
203    setRepeatMode(_toggleRepeatMapping[repeatModeStore.getValue()]);
204};
205
206/** 设置音源 */
207const setTrackSource = async (track: Track, autoPlay = true) => {
208    await ReactNativeTrackPlayer.setQueue([track, getFakeNextTrack()]);
209    if (autoPlay) {
210        await ReactNativeTrackPlayer.play();
211    }
212    // 写缓存 TODO: MMKV
213    Config.set('status.music.track', track as IMusic.IMusicItem, false);
214    Config.set('status.music.progress', 0, false);
215};
216
217/**
218 * 添加到播放列表
219 * @param musicItems 目标歌曲
220 * @param beforeIndex 在第x首歌曲前添加
221 * @param shouldShuffle 随机排序
222 */
223const addAll = (
224    musicItems: Array<IMusic.IMusicItem> = [],
225    beforeIndex?: number,
226    shouldShuffle?: boolean,
227) => {
228    const now = Date.now();
229    let newPlayList: IMusic.IMusicItem[] = [];
230    let currentPlayList = getPlayList();
231    const _musicItems = musicItems.map((item, index) =>
232        produce(item, draft => {
233            draft[timeStampSymbol] = now;
234            draft[sortIndexSymbol] = index;
235        }),
236    );
237    if (beforeIndex === undefined || beforeIndex < 0) {
238        // 1.1. 添加到歌单末尾,并过滤掉已有的歌曲
239        newPlayList = currentPlayList.concat(
240            _musicItems.filter(item => !isInPlayList(item)),
241        );
242    } else {
243        // 1.2. 新的播放列表,插入
244        const indexMap = createMediaIndexMap(_musicItems);
245        const beforeDraft = currentPlayList
246            .slice(0, beforeIndex)
247            .filter(item => !indexMap.has(item));
248        const afterDraft = currentPlayList
249            .slice(beforeIndex)
250            .filter(item => !indexMap.has(item));
251
252        newPlayList = [...beforeDraft, ..._musicItems, ...afterDraft];
253    }
254
255    // 如果太长了
256    if (newPlayList.length > maxMusicQueueLength) {
257        newPlayList = shrinkPlayListToSize(
258            newPlayList,
259            beforeIndex ?? newPlayList.length - 1,
260        );
261    }
262
263    // 2. 如果需要随机
264    if (shouldShuffle) {
265        newPlayList = shuffle(newPlayList);
266    }
267    // 3. 设置播放列表
268    setPlayList(newPlayList);
269    const currentMusicItem = currentMusicStore.getValue();
270
271    // 4. 重置下标
272    if (currentMusicItem) {
273        currentIndex = getMusicIndex(currentMusicItem);
274    }
275
276    // TODO: 更新播放队列信息
277    // 5. 存储更新的播放列表信息
278};
279
280/** 追加到队尾 */
281const add = (
282    musicItem: IMusic.IMusicItem | IMusic.IMusicItem[],
283    beforeIndex?: number,
284) => {
285    addAll(Array.isArray(musicItem) ? musicItem : [musicItem], beforeIndex);
286};
287
288/**
289 * 下一首播放
290 * @param musicItem
291 */
292const addNext = (musicItem: IMusic.IMusicItem | IMusic.IMusicItem[]) => {
293    const shouldPlay = isPlayListEmpty();
294    add(musicItem, currentIndex + 1);
295    if (shouldPlay) {
296        play(Array.isArray(musicItem) ? musicItem[0] : musicItem);
297    }
298};
299
300const isCurrentMusic = (musicItem: IMusic.IMusicItem) => {
301    return isSameMediaItem(musicItem, currentMusicStore.getValue()) ?? false;
302};
303
304const remove = async (musicItem: IMusic.IMusicItem) => {
305    const playList = getPlayList();
306    let newPlayList: IMusic.IMusicItem[] = [];
307    let currentMusic: IMusic.IMusicItem | null = currentMusicStore.getValue();
308    const targetIndex = getMusicIndex(musicItem);
309    let shouldPlayCurrent: boolean | null = null;
310    if (targetIndex === -1) {
311        // 1. 这种情况应该是出错了
312        return;
313    }
314    // 2. 移除的是当前项
315    if (currentIndex === targetIndex) {
316        // 2.1 停止播放,移除当前项
317        newPlayList = produce(playList, draft => {
318            draft.splice(targetIndex, 1);
319        });
320        // 2.2 设置新的播放列表,并更新当前音乐
321        if (newPlayList.length === 0) {
322            currentMusic = null;
323            shouldPlayCurrent = false;
324        } else {
325            currentMusic = newPlayList[currentIndex % newPlayList.length];
326            try {
327                const state = (await ReactNativeTrackPlayer.getPlaybackState())
328                    .state;
329                if (musicIsPaused(state)) {
330                    shouldPlayCurrent = false;
331                } else {
332                    shouldPlayCurrent = true;
333                }
334            } catch {
335                shouldPlayCurrent = false;
336            }
337        }
338    } else {
339        // 3. 删除
340        newPlayList = produce(playList, draft => {
341            draft.splice(targetIndex, 1);
342        });
343    }
344
345    setPlayList(newPlayList);
346    setCurrentMusic(currentMusic);
347    Config.set('status.music.musicQueue', playList, false);
348    if (shouldPlayCurrent === true) {
349        await play(currentMusic, true);
350    } else if (shouldPlayCurrent === false) {
351        await ReactNativeTrackPlayer.reset();
352    }
353};
354
355/**
356 * 设置播放模式
357 * @param mode 播放模式
358 */
359const setRepeatMode = (mode: MusicRepeatMode) => {
360    const playList = getPlayList();
361    let newPlayList;
362    if (mode === MusicRepeatMode.SHUFFLE) {
363        newPlayList = shuffle(playList);
364    } else {
365        newPlayList = produce(playList, draft => {
366            return sortByTimestampAndIndex(draft);
367        });
368    }
369
370    setPlayList(newPlayList);
371    const currentMusicItem = currentMusicStore.getValue();
372    currentIndex = getMusicIndex(currentMusicItem);
373    repeatModeStore.setValue(mode);
374    // 更新下一首歌的信息
375    ReactNativeTrackPlayer.updateMetadataForTrack(1, getFakeNextTrack());
376    // 记录
377    Config.set('status.music.repeatMode', mode, false);
378};
379
380/** 清空播放列表 */
381const clear = async () => {
382    setPlayList([]);
383    setCurrentMusic(null);
384
385    await ReactNativeTrackPlayer.reset();
386    Config.set('status.music', {
387        repeatMode: repeatModeStore.getValue(),
388    });
389};
390
391/** 暂停 */
392const pause = async () => {
393    await ReactNativeTrackPlayer.pause();
394};
395
396const setCurrentMusic = (musicItem?: IMusic.IMusicItem | null) => {
397    if (!musicItem) {
398        currentIndex = -1;
399        currentMusicStore.setValue(null);
400    }
401    currentIndex = getMusicIndex(musicItem);
402    currentMusicStore.setValue(musicItem!);
403};
404
405/**
406 * 播放
407 *
408 * 当musicItem 为空时,代表暂停/播放
409 *
410 * @param musicItem
411 * @param forcePlay
412 * @returns
413 */
414const play = async (
415    musicItem?: IMusic.IMusicItem | null,
416    forcePlay?: boolean,
417) => {
418    try {
419        if (!musicItem) {
420            musicItem = currentMusicStore.getValue();
421        }
422        if (!musicItem) {
423            throw new Error(PlayFailReason.PLAY_LIST_IS_EMPTY);
424        }
425        // 1. 移动网络禁止播放
426        if (
427            Network.isCellular() &&
428            !Config.get('setting.basic.useCelluarNetworkPlay') &&
429            !LocalMusicSheet.isLocalMusic(musicItem)
430        ) {
431            await ReactNativeTrackPlayer.reset();
432            throw new Error(PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY);
433        }
434
435        // 2. 如果是当前正在播放的音频
436        if (isCurrentMusic(musicItem)) {
437            const currentTrack = await ReactNativeTrackPlayer.getTrack(0);
438            // 2.1 如果当前有源
439            if (
440                currentTrack?.url &&
441                isSameMediaItem(musicItem, currentTrack as IMusic.IMusicItem)
442            ) {
443                const currentActiveIndex =
444                    await ReactNativeTrackPlayer.getActiveTrackIndex();
445                if (currentActiveIndex !== 0) {
446                    await ReactNativeTrackPlayer.skip(0);
447                }
448                if (forcePlay) {
449                    // 2.1.1 强制重新开始
450                    await ReactNativeTrackPlayer.seekTo(0);
451                }
452                if (
453                    (await ReactNativeTrackPlayer.getPlaybackState()).state !==
454                    State.Playing
455                ) {
456                    // 2.1.2 恢复播放
457                    await ReactNativeTrackPlayer.play();
458                }
459                // 这种情况下,播放队列和当前歌曲都不需要变化
460                return;
461            }
462            // 2.2 其他情况:重新获取源
463        }
464
465        // 3. 如果没有在播放列表中,添加到队尾;同时更新列表状态
466        const inPlayList = isInPlayList(musicItem);
467        if (!inPlayList) {
468            add(musicItem);
469        }
470
471        // 4. 更新列表状态和当前音乐
472        setCurrentMusic(musicItem);
473
474        // 5. 获取音源
475        let track: IMusic.IMusicItem;
476
477        // 5.1 通过插件获取音源
478        const plugin = PluginManager.getByName(musicItem.platform);
479        // 5.2 获取音质排序
480        const qualityOrder = getQualityOrder(
481            Config.get('setting.basic.defaultPlayQuality') ?? 'standard',
482            Config.get('setting.basic.playQualityOrder') ?? 'asc',
483        );
484        // 5.3 插件返回音源
485        let source: IPlugin.IMediaSourceResult | null = null;
486        for (let quality of qualityOrder) {
487            if (isCurrentMusic(musicItem)) {
488                source =
489                    (await plugin?.methods?.getMediaSource(
490                        musicItem,
491                        quality,
492                    )) ?? null;
493                // 5.3.1 获取到真实源
494                if (source) {
495                    qualityStore.setValue(quality);
496                    break;
497                }
498            } else {
499                // 5.3.2 已经切换到其他歌曲了,
500                return;
501            }
502        }
503
504        if (!isCurrentMusic(musicItem)) {
505            return;
506        }
507
508        if (!source) {
509            // 如果有source
510            if (musicItem.source) {
511                for (let quality of qualityOrder) {
512                    if (musicItem.source[quality]?.url) {
513                        source = musicItem.source[quality]!;
514                        qualityStore.setValue(quality);
515                        break;
516                    }
517                }
518            }
519
520            // 5.4 没有返回源
521            if (!source && !musicItem.url) {
522                throw new Error(PlayFailReason.INVALID_SOURCE);
523            } else {
524                source = {
525                    url: musicItem.url,
526                };
527                qualityStore.setValue('standard');
528            }
529        }
530
531        // 6. 特殊类型源
532        if (getUrlExt(source.url) === '.m3u8') {
533            // @ts-ignore
534            source.type = 'hls';
535        }
536        // 7. 合并结果
537        track = mergeProps(musicItem, source) as IMusic.IMusicItem;
538
539        // 8. 新增历史记录
540        musicHistory.addMusic(musicItem);
541
542        trace('获取音源成功', track);
543
544        // 9. 设置音源
545        await setTrackSource(track as Track);
546
547        // 10. 获取补充信息
548        let info: Partial<IMusic.IMusicItem> | null = null;
549        try {
550            info = (await plugin?.methods?.getMusicInfo?.(musicItem)) ?? null;
551        } catch {}
552
553        // 11. 设置补充信息
554        if (info && isCurrentMusic(musicItem)) {
555            const mergedTrack = mergeProps(track, info);
556            currentMusicStore.setValue(mergedTrack as IMusic.IMusicItem);
557            await ReactNativeTrackPlayer.updateMetadataForTrack(
558                0,
559                mergedTrack as TrackMetadataBase,
560            );
561        }
562
563        // 12. 刷新歌词信息
564        if (
565            !isSameMediaItem(
566                LyricManager.getLyricState()?.lyricParser?.getCurrentMusicItem?.(),
567                musicItem,
568            )
569        ) {
570            DeviceEventEmitter.emit(EDeviceEvents.REFRESH_LYRIC, true);
571        }
572    } catch (e: any) {
573        const message = e?.message;
574        if (
575            message === 'The player is not initialized. Call setupPlayer first.'
576        ) {
577            await ReactNativeTrackPlayer.setupPlayer();
578            play(musicItem, forcePlay);
579        } else if (message === PlayFailReason.FORBID_CELLUAR_NETWORK_PLAY) {
580            Toast.warn(
581                '当前禁止移动网络播放音乐,如需播放请去侧边栏-基本设置中修改',
582            );
583        } else if (message === PlayFailReason.INVALID_SOURCE) {
584            trace('音源为空,播放失败');
585            await failToPlay();
586        } else if (message === PlayFailReason.PLAY_LIST_IS_EMPTY) {
587            // 队列是空的,不应该出现这种情况
588        }
589    }
590};
591
592/**
593 * 播放音乐,同时替换播放队列
594 * @param musicItem 音乐
595 * @param newPlayList 替代列表
596 */
597const playWithReplacePlayList = async (
598    musicItem: IMusic.IMusicItem,
599    newPlayList: IMusic.IMusicItem[],
600) => {
601    if (newPlayList.length !== 0) {
602        const now = Date.now();
603        if (newPlayList.length > maxMusicQueueLength) {
604            newPlayList = shrinkPlayListToSize(
605                newPlayList,
606                newPlayList.findIndex(it => isSameMediaItem(it, musicItem)),
607            );
608        }
609        const playListItems = newPlayList.map((item, index) =>
610            produce(item, draft => {
611                draft[timeStampSymbol] = now;
612                draft[sortIndexSymbol] = index;
613            }),
614        );
615        setPlayList(
616            repeatModeStore.getValue() === MusicRepeatMode.SHUFFLE
617                ? shuffle(playListItems)
618                : playListItems,
619        );
620        await play(musicItem, true);
621    }
622};
623
624const skipToNext = async () => {
625    if (isPlayListEmpty()) {
626        setCurrentMusic(null);
627        return;
628    }
629
630    await play(getPlayListMusicAt(currentIndex + 1), true);
631};
632
633const skipToPrevious = async () => {
634    if (isPlayListEmpty()) {
635        setCurrentMusic(null);
636        return;
637    }
638
639    await play(
640        getPlayListMusicAt(currentIndex === -1 ? 0 : currentIndex - 1),
641        true,
642    );
643};
644
645/** 修改当前播放的音质 */
646const changeQuality = async (newQuality: IMusic.IQualityKey) => {
647    // 获取当前的音乐和进度
648    if (newQuality === qualityStore.getValue()) {
649        return true;
650    }
651
652    // 获取当前歌曲
653    const musicItem = currentMusicStore.getValue();
654    if (!musicItem) {
655        return false;
656    }
657    try {
658        const progress = await ReactNativeTrackPlayer.getProgress();
659        const plugin = PluginManager.getByMedia(musicItem);
660        const newSource = await plugin?.methods?.getMediaSource(
661            musicItem,
662            newQuality,
663        );
664        if (!newSource?.url) {
665            throw new Error(PlayFailReason.INVALID_SOURCE);
666        }
667        if (isCurrentMusic(musicItem)) {
668            const playingState = (
669                await ReactNativeTrackPlayer.getPlaybackState()
670            ).state;
671            await setTrackSource(
672                mergeProps(musicItem, newSource) as unknown as Track,
673                !musicIsPaused(playingState),
674            );
675
676            await ReactNativeTrackPlayer.seekTo(progress.position ?? 0);
677            qualityStore.setValue(newQuality);
678        }
679        return true;
680    } catch {
681        // 修改失败
682        return false;
683    }
684};
685
686enum PlayFailReason {
687    /** 禁止移动网络播放 */
688    FORBID_CELLUAR_NETWORK_PLAY = 'FORBID_CELLUAR_NETWORK_PLAY',
689    /** 播放列表为空 */
690    PLAY_LIST_IS_EMPTY = 'PLAY_LIST_IS_EMPTY',
691    /** 无效源 */
692    INVALID_SOURCE = 'INVALID_SOURCE',
693    /** 非当前音乐 */
694}
695
696function useMusicState() {
697    const playbackState = usePlaybackState();
698
699    return playbackState.state;
700}
701
702function getPreviousMusic() {
703    const currentMusicItem = currentMusicStore.getValue();
704    if (!currentMusicItem) {
705        return null;
706    }
707
708    return getPlayListMusicAt(currentIndex - 1);
709}
710
711function getNextMusic() {
712    const currentMusicItem = currentMusicStore.getValue();
713    if (!currentMusicItem) {
714        return null;
715    }
716
717    return getPlayListMusicAt(currentIndex + 1);
718}
719
720const TrackPlayer = {
721    setupTrackPlayer,
722    usePlayList,
723    getPlayList,
724    addAll,
725    add,
726    addNext,
727    skipToNext,
728    skipToPrevious,
729    play,
730    playWithReplacePlayList,
731    pause,
732    remove,
733    clear,
734    useCurrentMusic: currentMusicStore.useValue,
735    getCurrentMusic: currentMusicStore.getValue,
736    useRepeatMode: repeatModeStore.useValue,
737    getRepeatMode: repeatModeStore.getValue,
738    toggleRepeatMode,
739    usePlaybackState,
740    getProgress: ReactNativeTrackPlayer.getProgress,
741    useProgress: useProgress,
742    seekTo: ReactNativeTrackPlayer.seekTo,
743    changeQuality,
744    useCurrentQuality: qualityStore.useValue,
745    getCurrentQuality: qualityStore.getValue,
746    getRate: ReactNativeTrackPlayer.getRate,
747    setRate: ReactNativeTrackPlayer.setRate,
748    useMusicState,
749    reset: ReactNativeTrackPlayer.reset,
750    getPreviousMusic,
751    getNextMusic,
752};
753
754export default TrackPlayer;
755export {MusicRepeatMode, State as MusicState};
756