왜 만들었나
JJY 전파시계는 일본 밖에서는 전파를 받지 못하고, 받더라도 무조건 일본 시각(JST)으로 해석합니다. 휴대폰 소리로 신호를 만들고, 고른 지역의 현지 시각을 JST 자리에 넣어 보내 이 문제를 풉니다.
주요 기능
- 지도를 눌러 가장 가까운 도시의 시간대 선택 (기본값은 기기 시간대)
- 40kHz / 60kHz 국 선택
- 60초 타임코드 막대와 현재 초 표시
- 송신 중 화면 꺼짐 방지(Wake Lock)
- 한국어·영어·일본어 페이지
숫자로 보면
1분 프레임 = 기호 60개
지도에 미리 넣은 도시 62곳
언어별 페이지 3개(en·ko·ja)
어떻게 동작하나
시간대 → 시각 필드
fields()가 Intl.DateTimeFormat으로 선택한 IANA 시간대의 연·연중 일수·시·분·요일을 구해, 서머타임까지 자동 반영합니다.
jjy.js1분 프레임 생성
frame()이 분·시·연중 일수·연도·요일을 BCD로 바꾸고 마커와 패리티 비트를 넣어 0/1/M 기호 60개를 만듭니다.
jjy.js기호 → 펄스 길이
HIGH 표가 기호마다 매초 앞부분에서 신호를 켜 둘 시간(0은 0.8초, 1은 0.5초, M은 0.2초)을 정합니다.
jjy.js오디오 예약
schedule()이 50ms마다 돌며 앞으로 3초 분량의 펄스를 GainNode 값 변경으로 오디오 시계에 미리 예약하고, getOutputTimestamp로 출력 지연을 보정합니다.
app.js배음 송신
13.333kHz(또는 20kHz) 사인파를 켜고 끄면 이어폰 케이블에서 새는 3배음 40kHz(60kHz)를 시계 안테나가 받습니다.
app.js화면 표시
render()가 현지 시각, 60칸 비트 막대, 현재 초 램프를 매초 다시 그립니다.
app.js
핵심 코드
export function frame({ y, doy, h, m, wd }) {
const bcd = (v, ...w) => w.map(x => (v >= x ? (v -= x, 1) : 0));
const par = a => a.reduce((s, x) => s ^ x, 0);
const M = 'M';
const mi = bcd(m, 40, 20, 10, 8, 4, 2, 1);
const hr = bcd(h, 20, 10, 8, 4, 2, 1);
const d = bcd(doy, 200, 100, 80, 40, 20, 10, 8, 4, 2, 1);
return [
M, ...mi.slice(0, 3), 0, ...mi.slice(3), M, // 0-9 minute
0, 0, ...hr.slice(0, 2), 0, ...hr.slice(2), M, // 10-19 hour
0, 0, ...d.slice(0, 2), 0, ...d.slice(2, 6), M, // 20-29 day of year
...d.slice(6), 0, 0, par(hr), par(mi), 0, M, // 30-39 doy, PA1, PA2, SU1
0, ...bcd(y % 100, 80, 40, 20, 10, 8, 4, 2, 1), M, // 40-49 SU2, year
...bcd(wd, 4, 2, 1), 0, 0, 0, 0, 0, 0, M, // 50-59 weekday, LS1/2
];
}시각 필드를 JJY 규격의 60개 기호(0·1·마커)로 바꾸는 부분으로, BCD 변환과 패리티 계산이 들어 있습니다.
어려웠던 점
setTimeout은 수십 ms씩 밀려서 펄스 경계가 흔들리기 때문에, 벽시계 시각을 오디오 컨텍스트 시각으로 바꿔 3초 앞까지 setValueAtTime으로 미리 예약합니다. 또 휴대폰은 40kHz를 낼 수 없어서, 그 1/3 주파수를 내보내고 3배음을 이용합니다.
