@ihabdevteam/core 0.284.0 → 0.288.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/components.cjs.js +174 -174
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +176 -176
- package/dist/ihabdevteam-core.d.ts +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +33 -44
- package/dist/packages/core/src/components/Training/useNoiseFade.d.ts +4 -5
- package/dist/packages/core/src/components/Training/useNoiseFade.js +11 -6
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +17 -0
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +28 -2
- package/dist/packages/core/src/components/Training/useProgressTicker.d.ts +7 -5
- package/dist/packages/core/src/components/Training/useProgressTicker.js +4 -4
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -1651,7 +1651,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1651
1651
|
*
|
|
1652
1652
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1653
1653
|
*/
|
|
1654
|
-
export declare const CORE_VERSION = "0.
|
|
1654
|
+
export declare const CORE_VERSION = "0.288.0";
|
|
1655
1655
|
|
|
1656
1656
|
/** @public */
|
|
1657
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -50,10 +50,6 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
50
50
|
const objectUrls = useObjectUrls();
|
|
51
51
|
const sourceButtonsRef = useRef(null);
|
|
52
52
|
const fetchingRef = useRef(false);
|
|
53
|
-
// handlePrev/handleNext/handleJumpTo 세대 토큰 — 빠른 연타로 여러 호출이 겹치면
|
|
54
|
-
// src 교체/load()가 서로 인터리빙되며 "선택된" UI가 실제 재생 중인 트랙과 어긋날 수 있다.
|
|
55
|
-
// 각 호출 시작 시 증가시키고, await 이후 최신 호출인지 확인해 아니면 중단한다.
|
|
56
|
-
const jumpTokenRef = useRef(0);
|
|
57
53
|
const triPathRef = useRef(null);
|
|
58
54
|
/* 듣기 단추 설명 툴팁이 붙을 자리 — 가리키기와 초점 둘 다 받고, 다른 곳을 누르면 닫는다. */
|
|
59
55
|
/* 구조 분해해서 받는다 — `listenBtnRect` 처럼 점을 찍어 쓰면 lint 규칙이 그 객체를
|
|
@@ -139,24 +135,19 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
139
135
|
팝업을 닫아도 흐름이 계속 돌아, **떼어진 <audio> 에서 play()** 가 불려 이전 문장이 조작 단추 없이 끝까지
|
|
140
136
|
울렸다. 재생 직전에 본다. */
|
|
141
137
|
const mountedRef = useRef(true);
|
|
142
|
-
/* **시간표의 자리·의도·뜸
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
/* **시간표의 자리·의도·뜸 타이머·멈춘 자리는 진행기가 쥔다**((85) 2a~2c, 2026-10-06).
|
|
139
|
+
묻는 쪽(소음 · 틱 · 토글 · 자물쇠)은 **자리를 받아 제 손으로 셈하지 않고** 진행기에게
|
|
140
|
+
묻는다 — `isRunning()` 하나다. 그 셈이 세 벌로 흩어져 있던 것이 오늘 결함 여섯의 뿌리였다. */
|
|
145
141
|
const 진행기 = usePlaybackSequencer({ mountedRef });
|
|
146
|
-
|
|
142
|
+
/* `onEnded` 가 적어 두는 "여기서부터 이어 간다" — 토글이 멈출 때 진행기에 넘긴다 */
|
|
143
|
+
const 이어가기RefBox = useRef(null);
|
|
147
144
|
/* 이벤트 손잡이는 한 번만 붙으므로 prop 을 ref 로 들고 본다 — 안 그러면 낡은 값을 쓴다
|
|
148
145
|
(이 파일이 `disabledRef`·`noiseSettingsRef` 에 쓰는 것과 같은 결). */
|
|
149
146
|
const leadInMsRef = useRef(leadInMs);
|
|
150
147
|
const tailMsRef = useRef(tailMs);
|
|
151
148
|
useEffect(() => { leadInMsRef.current = Math.max(0, leadInMs || 0); }, [leadInMs]);
|
|
152
149
|
useEffect(() => { tailMsRef.current = Math.max(0, tailMs || 0); }, [tailMs]);
|
|
153
|
-
|
|
154
|
-
아래 `handleToggle` 이 쓴다(말귀 요청 (85) 0단계에서 찾은 결함, 2026-10-06). */
|
|
155
|
-
const 뒤뜸에서멈춤Ref = useRef(false);
|
|
156
|
-
const 이어가기Ref = useRef(null);
|
|
157
|
-
/** 뜸만큼 기다린다 — 그 사이 **의도가 바뀌면 거짓**을 돌려 부르는 쪽이 그만둔다 */
|
|
158
|
-
const 뜸기다리기 = 진행기.waitPad;
|
|
159
|
-
const { start: startTicker, stop: stopTicker, settleTo: settleTicker, } = useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, padRef: 뜸Ref, leadInMs, tailMs });
|
|
150
|
+
const { start: startTicker, stop: stopTicker, settleTo: settleTicker, } = useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, sequencer: 진행기, leadInMs, tailMs });
|
|
160
151
|
/* 파형이 자리를 잡을 때까지 기다리는 규칙은 useWaveReady 가 들고 있다 —
|
|
161
152
|
ref 둘·함수 둘에 "준비 안 됨으로 되돌리기"가 일곱 군데 흩어져 있던 것을 모았다. */
|
|
162
153
|
const { onReadyChange: handleWaveReadyChange, waitForReady: waitForWaveReady, markNotReady: markWaveNotReady, } = useWaveReady({ showSoundWaveForm, showNoiseWaveForm });
|
|
@@ -183,7 +174,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
183
174
|
noisePlayer,
|
|
184
175
|
audioRef,
|
|
185
176
|
playbackStateRef,
|
|
186
|
-
|
|
177
|
+
sequencer: 진행기,
|
|
187
178
|
});
|
|
188
179
|
const { startTTS: 합성시작, stopTTS } = useTTSPlayback({
|
|
189
180
|
ttsText,
|
|
@@ -237,7 +228,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
237
228
|
*
|
|
238
229
|
* 이제 세대 하나다. `멈춤의도()` 는 세대를 올리고 자리를 비운다.
|
|
239
230
|
*/
|
|
240
|
-
const 멈춤의도 = () => { 진행기.abort();
|
|
231
|
+
const 멈춤의도 = () => { 진행기.abort(); };
|
|
241
232
|
/* 스피커는 하나(currentSound.ts) — 한 화면의 다른 재생기(문장 듣기·문제 듣기)·낱말 카드·미리 듣기가 울리면 이 재생기는
|
|
242
233
|
멈춘다. 전에는 낱말 카드만 자물쇠를 잡아, 듣기 단계가 둘인 화면·듣기+카드 단계에서 두 소리가 함께 울렸다.
|
|
243
234
|
멈춤 단추와 같게 멈춘다(이어 듣기 가능). 함수는 한 벌이어야 자물쇠가 '내 것' 을 알아본다. */
|
|
@@ -351,7 +342,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
351
342
|
}
|
|
352
343
|
catch (e) { }
|
|
353
344
|
}
|
|
354
|
-
if (!(await
|
|
345
|
+
if (!(await 진행기.waitPad(leadInMsRef.current, 'lead')))
|
|
355
346
|
return false;
|
|
356
347
|
if (!mountedRef.current || !진행기.isCurrent(내세대))
|
|
357
348
|
return false;
|
|
@@ -392,7 +383,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
392
383
|
}
|
|
393
384
|
catch (_) { }
|
|
394
385
|
return false;
|
|
395
|
-
}, [t, startTicker,
|
|
386
|
+
}, [t, startTicker, 진행기, fadeInNoise, noiseSettingsRef]);
|
|
396
387
|
const attachAudioListeners = useCallback(() => {
|
|
397
388
|
const a = audioRef.current;
|
|
398
389
|
if (!a)
|
|
@@ -436,7 +427,6 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
436
427
|
다음 소리가 있으면 그 소리의 **앞 뜸**이 이어 붙어 사이가 둘을 더한 만큼이 된다 —
|
|
437
428
|
누를 때의 뜸과 사이의 뜸을 가르는 자리가 여기다. */
|
|
438
429
|
const onEnded = () => {
|
|
439
|
-
뒤뜸에서멈춤Ref.current = false;
|
|
440
430
|
const 이어가기 = () => {
|
|
441
431
|
try {
|
|
442
432
|
const sref = sourcesRef.current;
|
|
@@ -476,13 +466,13 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
476
466
|
}
|
|
477
467
|
catch (e) { }
|
|
478
468
|
};
|
|
479
|
-
/*
|
|
480
|
-
이어가기
|
|
469
|
+
/* 멈췄다 다시 누를 때 **여기서부터** 잇는다 — 진행기가 그 자리를 안다 */
|
|
470
|
+
이어가기RefBox.current = 이어가기;
|
|
481
471
|
if (tailMsRef.current > 0) {
|
|
482
472
|
/* 기다리는 동안 진행바를 돌린다 — `pause` 가 이미 세웠을 수 있어 다시 켠다.
|
|
483
|
-
그 사이 멈추거나 떠나면
|
|
473
|
+
그 사이 멈추거나 떠나면 `waitPad` 가 거짓을 주고 아무 일도 안 한다. */
|
|
484
474
|
startTicker();
|
|
485
|
-
|
|
475
|
+
진행기.waitPad(tailMsRef.current, 'tail').then((이어도되나) => {
|
|
486
476
|
/* 뒤뜸이 끝났다 — 자리는 비우되 **세대는 안 올린다**(`abort` 가 아니다). 올리면
|
|
487
477
|
바로 아래에서 이어 갈 사슬까지 제가 버린 것이 된다. */
|
|
488
478
|
진행기.leave();
|
|
@@ -505,7 +495,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
505
495
|
}
|
|
506
496
|
catch (e) { }
|
|
507
497
|
};
|
|
508
|
-
}, [fadeInNoise, fadeOutNoise, tryPlayWithRetry, noiseSettingsRef, startTicker, stopTicker,
|
|
498
|
+
}, [fadeInNoise, fadeOutNoise, tryPlayWithRetry, noiseSettingsRef, startTicker, stopTicker, 진행기]);
|
|
509
499
|
useEffect(() => {
|
|
510
500
|
const cleanup = attachAudioListeners();
|
|
511
501
|
return cleanup;
|
|
@@ -889,8 +879,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
889
879
|
+1000ms 에 한 번, 원래 사슬이 닿는 +1500ms 에 또 한 번).
|
|
890
880
|
감시 타이머가 0.268.0 에서 배운 것과 같은 것을 이 단추가 못 배우고 있었다 —
|
|
891
881
|
뜸은 소리가 안 나는 구간이지 **멈춘 구간이 아니다.** */
|
|
892
|
-
const
|
|
893
|
-
const 뜸도는중 = 뜸.phase !== 'sound' && 뜸.at > 0;
|
|
882
|
+
const 뜸도는중 = 진행기.isRunning();
|
|
894
883
|
if (a.paused && !뜸도는중) {
|
|
895
884
|
if (completedByEndRef.current) {
|
|
896
885
|
await handleReplay();
|
|
@@ -899,17 +888,19 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
899
888
|
/* 뒤뜸에서 멈췄던 것이면 그 소리를 다시 틀지 않고 **그다음으로** 잇는다 — 끝난 요소에
|
|
900
889
|
`play()` 를 걸면 방금 들은 소리가 처음부터 또 난다. 뜸은 이미 한 번 기다렸으니
|
|
901
890
|
다시 세지 않는다. */
|
|
902
|
-
|
|
903
|
-
|
|
891
|
+
/* **멈춘 자리에서 이어 간다** — 어디서 멈췄는지는 진행기가 안다.
|
|
892
|
+
`'restart'`(앞뜸이었다)면 아래로 흘러 처음부터 다시 돈다. */
|
|
893
|
+
const 이어갈것 = 진행기.resume();
|
|
894
|
+
if (typeof 이어갈것 === 'function') {
|
|
904
895
|
setPlaybackState("playing");
|
|
905
|
-
|
|
896
|
+
이어갈것();
|
|
906
897
|
return;
|
|
907
898
|
}
|
|
908
899
|
await tryPlayWithRetry(a);
|
|
909
900
|
setPlaybackState("playing");
|
|
910
901
|
}
|
|
911
902
|
else {
|
|
912
|
-
|
|
903
|
+
진행기.pause(이어가기RefBox.current);
|
|
913
904
|
멈춤의도();
|
|
914
905
|
a.pause();
|
|
915
906
|
setPlaybackState("paused");
|
|
@@ -1130,18 +1121,16 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
1130
1121
|
* 이동 토큰은 연타 방지다 — 파형을 기다리는 사이 더 최신 이동이 오면 이 호출은 물러선다.
|
|
1131
1122
|
*/
|
|
1132
1123
|
const goToTrack = async (index, { sameSegment = false } = {}) => {
|
|
1133
|
-
/* **먼저 돌고 있는 시간표를 걷는다**(2026-10-06).
|
|
1134
|
-
|
|
1135
|
-
사슬이 둘이 된다 — 가짜 시계 실측(소리 둘 · 뜸 1000 · 사이
|
|
1136
|
-
`b.mp3` 가 3100ms 와 3600ms 에 **두 번** 났다. 게다가
|
|
1137
|
-
났다** — 이쪽이 뜸을 기다리는 동안 살아 있던 사슬이
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1124
|
+
/* **먼저 돌고 있는 시간표를 걷는다**(2026-10-06). 전에는 트랙 이동이 **제 토큰**
|
|
1125
|
+
(`jumpTokenRef`)으로 이동끼리만 가렸고 **뜸 사슬은 그대로 살아 있었다.** 그래서 사이
|
|
1126
|
+
뜸에 번호 단추를 누르면 사슬이 둘이 된다 — 가짜 시계 실측(소리 둘 · 뜸 1000 · 사이
|
|
1127
|
+
뜸 2100ms 에 [소스 1]): `b.mp3` 가 3100ms 와 3600ms 에 **두 번** 났다. 게다가
|
|
1128
|
+
**누른 것은 1번인데 2번이 났다** — 이쪽이 뜸을 기다리는 동안 살아 있던 사슬이
|
|
1129
|
+
`a.src` 를 다음 소리로 바꿔 놓아서다.
|
|
1130
|
+
그 토큰은 **세대와 같은 일**이었다((85) 2d) — `멈춤의도()` 가 둘 다 올리고 여기서
|
|
1131
|
+
둘 다 집었다. 세대 하나로 합친다: `abort()` 로 걷고, **그 뒤에** 제 세대를 집는다. */
|
|
1141
1132
|
멈춤의도();
|
|
1142
|
-
|
|
1143
|
-
이어가기Ref.current = null;
|
|
1144
|
-
const myToken = ++jumpTokenRef.current;
|
|
1133
|
+
const 내세대 = 진행기.generation();
|
|
1145
1134
|
const a = audioRef.current;
|
|
1146
1135
|
const sref = sourcesRef.current;
|
|
1147
1136
|
const src = sref?.[index]?.src;
|
|
@@ -1158,10 +1147,10 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
1158
1147
|
소리싣기(a, src);
|
|
1159
1148
|
if (!sameSegment && (await waitForWaveReady()) === false)
|
|
1160
1149
|
return;
|
|
1161
|
-
if (
|
|
1150
|
+
if (!진행기.isCurrent(내세대))
|
|
1162
1151
|
return;
|
|
1163
1152
|
await tryPlayWithRetry(a);
|
|
1164
|
-
if (
|
|
1153
|
+
if (!진행기.isCurrent(내세대))
|
|
1165
1154
|
return;
|
|
1166
1155
|
setPlaybackState('playing');
|
|
1167
1156
|
setStateLocal('playMode');
|
|
@@ -5,7 +5,7 @@ type PlaybackState = 'stopped' | 'playing' | 'paused';
|
|
|
5
5
|
* TrainingStepAudioPlayer에서 분리 — 소음(noise) 오디오 엘리먼트 생성/페이드인/페이드아웃/워치독을
|
|
6
6
|
* 전담한다. audioRef(메인 문장 오디오)·playbackStateRef는 메인 컴포넌트 소유라 그대로 전달받는다.
|
|
7
7
|
*/
|
|
8
|
-
export declare function useNoiseFade({ noisePlayer, audioRef, playbackStateRef,
|
|
8
|
+
export declare function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequencer, }: {
|
|
9
9
|
noisePlayer: NoisePlayerContextValue | null;
|
|
10
10
|
audioRef: RefObject<HTMLAudioElement | null>;
|
|
11
11
|
playbackStateRef: RefObject<PlaybackState>;
|
|
@@ -17,10 +17,9 @@ export declare function useNoiseFade({ noisePlayer, audioRef, playbackStateRef,
|
|
|
17
17
|
* 소음을 걷는다. 특히 아래 감시 타이머는 0.8초 뒤에 내리므로, 1000ms 뜸은 **800ms 에서
|
|
18
18
|
* 소음이 꺼진다.** 뜸은 소리가 안 나는 구간이지 **재생이 끝난 구간이 아니다.**
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}>;
|
|
20
|
+
sequencer?: {
|
|
21
|
+
isRunning: () => boolean;
|
|
22
|
+
};
|
|
24
23
|
}): {
|
|
25
24
|
noiseAudioRef: RefObject<HTMLAudioElement | null>;
|
|
26
25
|
activeNoiseUrl: string | null;
|
|
@@ -17,7 +17,7 @@ function generateNoiseUrl(type) {
|
|
|
17
17
|
* TrainingStepAudioPlayer에서 분리 — 소음(noise) 오디오 엘리먼트 생성/페이드인/페이드아웃/워치독을
|
|
18
18
|
* 전담한다. audioRef(메인 문장 오디오)·playbackStateRef는 메인 컴포넌트 소유라 그대로 전달받는다.
|
|
19
19
|
*/
|
|
20
|
-
export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef,
|
|
20
|
+
export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequencer, }) {
|
|
21
21
|
const registerAnalyserRef = useRef(noisePlayer?.registerAnalyser);
|
|
22
22
|
registerAnalyserRef.current = noisePlayer?.registerAnalyser;
|
|
23
23
|
const noiseAudioRef = useRef(null);
|
|
@@ -35,11 +35,10 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, padRef,
|
|
|
35
35
|
// 판별해, 이미 다른 의도(fadeIn 등)로 대체된 정지를 실행하지 않게 한다.
|
|
36
36
|
const noiseFadeTokenRef = useRef(0);
|
|
37
37
|
const noiseHardStopTimerRef = useRef(null);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}, [padRef]);
|
|
38
|
+
/* **"지금 시간표가 도는가" 는 진행기에게 묻는다**((85) 2c, 2026-10-06). 전에는 자리를
|
|
39
|
+
받아 **제 손으로 셈했다**(`p.phase !== 'sound' && p.at > 0`) — 같은 셈이 틱·토글·여기
|
|
40
|
+
세 벌이었다. 셈이 한 자리에 있어야 "뜸이란 무엇인가" 가 갈라지지 않는다. */
|
|
41
|
+
const 뜸중 = useCallback(() => !!sequencer?.isRunning(), [sequencer]);
|
|
43
42
|
const cancelNoiseFade = useCallback(() => {
|
|
44
43
|
if (noiseFadeRafRef.current !== null) {
|
|
45
44
|
cancelAnimationFrame(noiseFadeRafRef.current);
|
|
@@ -207,6 +206,11 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, padRef,
|
|
|
207
206
|
const mainPlaying = !!a && !a.paused && !a.ended;
|
|
208
207
|
// speechSynthesis TTS 경로는 main <audio> 없이 재생되므로 playbackState도 함께 본다
|
|
209
208
|
// 뜸 구간은 메인이 멈춰 있어도 **재생 중**이다 — 안 보면 1000ms 뜸의 800ms 에 소음이 꺼진다
|
|
209
|
+
/* `뜸중()` 은 지금 **겹옷이다**(0.288.0 에서 재 봤다). 뜸이 시작될 때 재생기가
|
|
210
|
+
playbackState 를 'playing' 으로 올리므로 오른쪽이 이미 참이다 — 이 셋 가운데
|
|
211
|
+
`뜸중()` 만 지워도 시험 108 개가 다 통과한다. 둘이 같은 뜻이 아니라 **오늘 같이
|
|
212
|
+
갈 뿐**이라 남겨 둔다. 지우려면 "뜸이면 playbackState 가 'playing' 이다" 를 먼저
|
|
213
|
+
문으로 세워라 — 안 세우고 지우면 다음에 그 하나가 안 따라간다. */
|
|
210
214
|
if (mainPlaying || 뜸중() || playbackStateRef.current === 'playing') {
|
|
211
215
|
misses = 0;
|
|
212
216
|
return;
|
|
@@ -315,6 +319,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, padRef,
|
|
|
315
319
|
// 나오는 버그가 있었다. 메인이 재생 중이 아니면 무조건 정지시킨다.
|
|
316
320
|
const mainAudio = audioRef.current;
|
|
317
321
|
// speechSynthesis TTS 경로는 메인 <audio> 없이 재생된다 — 감시 타이머처럼 playbackState 도 본다(안 보면 비율만 바꿔도 소음이 멈췄다)
|
|
322
|
+
// `뜸중()` 은 여기서도 겹옷이다 — 위 감시 타이머 옆 주석 참고.
|
|
318
323
|
const mainPlaying = (!!mainAudio && !mainAudio.paused && !mainAudio.ended) || 뜸중() || playbackStateRef.current === 'playing';
|
|
319
324
|
if (mainPlaying && pendingNoiseUrlRef.current) {
|
|
320
325
|
/* 종류를 바꾼 뒤 150ms 페이드아웃 안에 다른 설정을 바꾸면, 아래 볼륨만 옮기는 갈래가 파일을 바꿀 페이드를 지워 화면의 종류와
|
|
@@ -53,6 +53,23 @@ export interface PlaybackSequencer {
|
|
|
53
53
|
* 자리다). 그 길을 **진짜로** 여는 문을 둔다.
|
|
54
54
|
*/
|
|
55
55
|
__bumpGenerationForTest: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* **멈춘 자리를 적어 둔다 — 다시 누르면 어디서부터인가.**
|
|
58
|
+
*
|
|
59
|
+
* 멈출 때 `pause(이어가기)` 로 적고, 다시 누를 때 `resume()` 으로 묻는다. 답은 둘이다:
|
|
60
|
+
*
|
|
61
|
+
* `'restart'` 앞뜸에서 멈췄다 → **앞뜸부터 다시.** 앞뜸은 귀가 소음에 자리를 잡는
|
|
62
|
+
* 시간인데(0.268.0) 멈추면 소음도 멎으니, 남은 뜸만 채우면 그 시간이 잘린다.
|
|
63
|
+
* `(함수)` 사이·뒤뜸에서 멈췄다 → **그 자리에서 이어 간다**(다음 소리로, 마지막이면
|
|
64
|
+
* 완료로). 끝난 요소에 `play()` 를 걸면 방금 들은 소리가 처음부터 또 난다.
|
|
65
|
+
*
|
|
66
|
+
* 전에는 재생기가 ref 둘(`뒤뜸에서멈춤Ref` · `이어가기Ref`)로 들고 **멈추는 자리마다** 그
|
|
67
|
+
* 둘을 맞춰 적어야 했다 — 네 자리 중 한 곳만 빠뜨려도 틀린 데서 이어 간다.
|
|
68
|
+
* 어느 구간에서 멈췄는지는 **자리를 쥔 쪽**이 안다.
|
|
69
|
+
*/
|
|
70
|
+
pause: (resume: (() => void) | null) => void;
|
|
71
|
+
/** 멈춘 자리에서 이어 갈 방법. 멈춘 적이 없으면 `null` */
|
|
72
|
+
resume: () => 'restart' | (() => void) | null;
|
|
56
73
|
/**
|
|
57
74
|
* 뜸만큼 기다린다. 그 사이 세대가 바뀌거나 화면을 떠나면 **거짓**을 준다 —
|
|
58
75
|
* 부르는 쪽은 거짓이면 아무 일도 하지 않고 돌아간다.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
export function usePlaybackSequencer({ mountedRef }) {
|
|
4
4
|
const positionRef = useRef({ phase: 'sound', at: 0 });
|
|
5
5
|
const 세대Ref = useRef(0);
|
|
@@ -18,6 +18,22 @@ export function usePlaybackSequencer({ mountedRef }) {
|
|
|
18
18
|
* (처음에는 걷는 쪽으로 썼다가, 그 때문에 **세대 비교가 죽은 코드**가 된 것을 뒤늦게 봤다 —
|
|
19
19
|
* 걷어 버리면 세대를 볼 일이 없다. 둘 중 하나를 골라야 하는 자리였다.)
|
|
20
20
|
*/
|
|
21
|
+
/** 멈출 때의 자리 — `'restart'`(앞뜸이었다) 또는 이어 갈 함수, 안 멈췄으면 `null` */
|
|
22
|
+
const 멈춘자리Ref = useRef(null);
|
|
23
|
+
/* **`abort()` 보다 먼저 부른다** — `abort()` 는 자리를 비우므로 뒤에 부르면 "어느 구간에서
|
|
24
|
+
멈췄나" 를 이미 잃는다. `pause()` 가 그 자리를 보고 적고, 그다음에 `abort()` 가 걷는다.
|
|
25
|
+
(`abort()` 가 이 적바림까지 지우게 했다가 못이 울렸다 — 멈춘 자리를 바로 지워 버려
|
|
26
|
+
이을 때 앞 소리가 또 났다. `멈춤의도()` 안에서 둘이 잇따라 불린다.) */
|
|
27
|
+
const pause = useCallback((resume) => {
|
|
28
|
+
const p = positionRef.current;
|
|
29
|
+
const 뜸중 = p.phase !== 'sound' && p.at > 0;
|
|
30
|
+
멈춘자리Ref.current = !뜸중 ? null : p.phase === 'lead' ? 'restart' : resume;
|
|
31
|
+
}, []);
|
|
32
|
+
const resume = useCallback(() => {
|
|
33
|
+
const v = 멈춘자리Ref.current;
|
|
34
|
+
멈춘자리Ref.current = null;
|
|
35
|
+
return v;
|
|
36
|
+
}, []);
|
|
21
37
|
const abort = useCallback(() => {
|
|
22
38
|
세대Ref.current += 1;
|
|
23
39
|
positionRef.current = { phase: 'sound', at: 0 };
|
|
@@ -56,5 +72,15 @@ export function usePlaybackSequencer({ mountedRef }) {
|
|
|
56
72
|
뜸타이머Ref.current = null;
|
|
57
73
|
}
|
|
58
74
|
}, []);
|
|
59
|
-
|
|
75
|
+
/**
|
|
76
|
+
* **판마다 새 객체면 안 된다**((85) 2c 에서 겪었다, 2026-10-06).
|
|
77
|
+
*
|
|
78
|
+
* 묻는 쪽이 이것을 **효과의 입력으로** 받는다. 판마다 새로 지으면 그 효과가 매번 다시
|
|
79
|
+
* 돌고, 손잡이 효과의 치우기가 **재생 도중 진행 사슬을 끊는다** — 그러면 뜸 동안 재생바가
|
|
80
|
+
* 멈춰 보인다. 0.271.0 에 세워 둔 문(`audioProgressChainRerender`)이 그 자리를 바로 잡았다.
|
|
81
|
+
* 안 세웠으면 "앞뜸에 재생바가 안 움직인다" 를 또 사용자가 짚을 뻔했다.
|
|
82
|
+
*
|
|
83
|
+
* 안쪽 것들은 전부 `useRef`·`useCallback` 이라 이미 고정이다 — 겉만 고정하면 된다.
|
|
84
|
+
*/
|
|
85
|
+
return useMemo(() => ({ positionRef, isRunning, generation, isCurrent, abort, waitPad, enterSound, leave, pause, resume, __bumpGenerationForTest }), [isRunning, generation, isCurrent, abort, waitPad, enterSound, leave, pause, resume, __bumpGenerationForTest]);
|
|
60
86
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
2
|
import type { Source } from './trainingStepAudioPlayerTypes';
|
|
3
|
-
export declare function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef,
|
|
3
|
+
export declare function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, sequencer, leadInMs, tailMs, }: {
|
|
4
4
|
audioRef: RefObject<HTMLAudioElement | null>;
|
|
5
5
|
/** 진행 마커 — `left` 를 퍼센트로 민다 */
|
|
6
6
|
fillRef: RefObject<HTMLDivElement | null>;
|
|
@@ -14,10 +14,12 @@ export declare function useProgressTicker({ audioRef, fillRef, waveProgressRef,
|
|
|
14
14
|
* 뜸을 주면 **소리가 안 나는 동안에도 재생바가 움직여야 한다** — 전에는 `currentTime` 을
|
|
15
15
|
* 그대로 따라 멈춰 있었다(말귀 실측: 누른 뒤 2029ms 에 `play`, 그때까지 채움 0%).
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
sequencer?: {
|
|
18
|
+
positionRef: RefObject<{
|
|
19
|
+
phase: 'lead' | 'sound' | 'tail';
|
|
20
|
+
at: number;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
21
23
|
leadInMs?: number;
|
|
22
24
|
tailMs?: number;
|
|
23
25
|
}): {
|
|
@@ -61,7 +61,7 @@ function audioDurationOf(a) {
|
|
|
61
61
|
* 아무 일도 안 한다.
|
|
62
62
|
*/
|
|
63
63
|
const 느린틀 = 400;
|
|
64
|
-
export function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef,
|
|
64
|
+
export function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, sequencer, leadInMs = 0, tailMs = 0, }) {
|
|
65
65
|
const rafRef = useRef(null);
|
|
66
66
|
const stop = useCallback(() => {
|
|
67
67
|
if (rafRef.current === null)
|
|
@@ -111,8 +111,8 @@ export function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesR
|
|
|
111
111
|
const 뜸있다 = (leadInMs || 0) > 0 || (tailMs || 0) > 0;
|
|
112
112
|
const pos = 뜸있다
|
|
113
113
|
? progressWithPad({
|
|
114
|
-
phase:
|
|
115
|
-
phaseElapsedMs:
|
|
114
|
+
phase: sequencer?.positionRef.current?.phase ?? 'sound',
|
|
115
|
+
phaseElapsedMs: sequencer?.positionRef.current ? Math.max(0, Date.now() - sequencer.positionRef.current.at) : 0,
|
|
116
116
|
currentTime: a && typeof a.currentTime === 'number' ? a.currentTime : 0,
|
|
117
117
|
trackDurationSec: 트랙길이알아 ? 트랙.duration : (총길이 || 0),
|
|
118
118
|
leadInMs: leadInMs || 0,
|
|
@@ -142,7 +142,7 @@ export function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesR
|
|
|
142
142
|
rafRef.current = requestAnimationFrame(step);
|
|
143
143
|
};
|
|
144
144
|
rafRef.current = requestAnimationFrame(step);
|
|
145
|
-
}, [audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef,
|
|
145
|
+
}, [audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, sequencer, leadInMs, tailMs]);
|
|
146
146
|
/** 다 들었을 때처럼 끝에 붙여 놓아야 하는 자리 */
|
|
147
147
|
const settleTo = useCallback((pos) => {
|
|
148
148
|
stop();
|