메아리

Tour de IOCCC: 2001/coupard

#include<unistd.h> 
#include<time.h>

#define k ("C9B7351A@D-/E+F?')G>H%J#=I"[(d[(i/13)*2]*91+d[(i/13)*2+1]-3220)&(\
4096>>(i%13))?l+1:l]-59)
#define g(n)e(n<13?n:n<20?n+1:n>20?11+n/10:13,0);e(n>12&&n<20?26:n>20&&n%10?n%\
10:-1,2);
#define x(n)g(localtime(&a)->tm_##n)

unsigned char *d="KZs2ITTwhwZYvec@JbYxOjf9-TZRGDb/el7#q(`SZ#|_neTwq\\MqJ5cVgte\
K-ReK-(Mq8+D'6Ui0tG88vXJ-Tu{VI=d%cR]h7CumwBq\\-#{thj8fw$OEfEvLHP13_##w.OD[7Cw2\
]<T{|[F}L;L:*+A#PwLnp{9'M3Mr|w_|unm'}$*(5]_$?O9zO{{wz4p6vP8Ipu}$BQospf=-Isnyl'\
|g53o^c`ov-P`-x+|ZAd<e?<'b9P|LkZOf{B-8`K([srqv&gy1,:}$|s7D{yN6M#cQyKpC|*_|#xA#\
'YfQ}$k$kr7dM#dcnWDg|PHdA#^j&q}$x@(a;k2JB]50ZKp{xRwbkTm.v\\a3fJ@V[`J#fN`s|sZeH\
mmHX7`JKiry4sm,bUfz69{rt'k*pBq^l,ut6UVvb9N%r%l:Py3r[.Z/pBkaz2J4u{GTu)hyp#%gbS$\
r`mzi9G|3M,r}-Dt?w)_##%fQt$k&n]e$HzFXu(|Fvt`i#$A#|ykrz+nhs'P#|pm*9[_#,5P#}$}$x\
&s(i,HzO'&d&g?/_$P#kZ7Dn]e$J{zi8ypi|hq$_.|cLx`sy;f8GRMQM#7R5$hym*:^`sxrvtfQ|z_\
N|ye[7dH[}#Ts\\59V@|#z}$}$y;usL)vXFiA0AukZ8H|fyKurd]|s5<|xq@Cpm,}${,e$KufO?/2M\
t$zmM#vtfQ|zRKlEA#P&fPOwJ#pf_$?/2#@|$w}$?fLxA#^-}$cM^aA#&g#wIv?^|yY#s)Lwt'K#ON\
yoXKYkpCl0m}D[9OGU7n([ZrL)[8eRy7uR*PJ#^5xRG>zSf>p]ZrJ_[9q?qgYe^4$r3lY$4SdsSyNv\
J_l&w2I?q#fN*F1n|s9OGUv|w&l+pR-3Nudtyn@|Dfq#^o|s9Pt=oZCol/{VdgkLdwyn1yDed}?Dzi\
}#Kje#d}Y${8pSt<q?qgIv|`DH?9}$O{k(LgE;|KU-tJnWOw*F@YP#|yY$b4|.50[2d}D'kaqhp;JE\
$r&fH&'6nykYGCv>K$t?yLbwtUn[F{ncOx_.:K#nwF#f|HQs&<&bfQPE?:uJm*&\\S=*/@|We*w@$@\
f0,*H])WL<i@^xX@)(a.>MV*p{Zqf&zH%###################u2#A#_$A#_$A#_$A#_$@tA#QI#\
3#0&:p#?1[P*6WQ@i#C1{[;@|1}S%^r#g.d^z#M}$ziZe|kU6oZbq5$kF]4[E^nA#_.|}e$Ku^r&WA\
#_$?/*P_$@t&gA#[;_$A#^j2#A#_*d}@v^z#8?D?D9Q`s}$5$L|u<3JC/xVz;#s^{?9$M}$x_Y,7uP\
$",p[6789];

int o[]={145,1145,1745,2545,3045,4045,4345,5145,5745,6369},i=0,j=0,l=0;

void e(n,h){
 for(j=0;n>0;n-=!(p[j++]^9));
 for(;!n&&j[p]^9;j++)write(1,p+p[j][o],o[p[j]+1]-p[j][o]);
 for(i=0;i<h[o];i++)write(1,d+7,1);}

int main(){
 time_t a=time(&a);

 for(;i<8476;j[p]=k>=0?j<*o?k-2:(p[j+1]=k<<4):0,j+=k>=0?1+(j>=*o):0,
 l=-k*(k<0),i++);e(21,4);x(hour)e(22,1);x(min)e(23,1);e(25,0);x(sec)e(24,0);

 return;}

TODO: 적절한 머릿말 찾는 중

뭘 하는 프로그램인가?

믿거나 말거나, 이 프로그램은 현재 시각을 사운드로 들려 줍니다.

TODO: /dev/audio 포맷을 wav로 변환하는 방법에 대한 내용.

어떻게 동작하는가?

언제나 그렇듯이 코드 정리를 하면서 시작하겠습니다.

#include <unistd.h> 
#include <time.h>

#define k \
    ("C9B7351A@D-/E+F?')G>H%J#=I" \
        [(d[(i/13)*2] * 91 + d[(i/13)*2+1] - 3220) & (4096 >> (i % 13)) ? \
         l + 1 : l] - 59)
#define g(n) \
    e(n < 13 ? n : n < 20 ? n + 1 : n > 20 ? 11 + n/10 : 13, 0); \
    e(n > 12 && n < 20 ? 26 : n > 20 && n % 10 ? n % 10 : -1, 2);
#define x(n) g(localtime(&a)->tm_##n)

unsigned char *d =
    "KZs2ITTwhwZYvec@JbYxOjf9-TZRGDb/el7#q(`SZ#|_neTwq\\MqJ5cVgteK-ReK-(Mq8+"
    "D'6Ui0tG88vXJ-Tu{VI=d%cR]h7CumwBq\\-#{thj8fw$OEfEvLHP13_##w.OD[7Cw2]<T{"
    "|[F}L;L:*+A#PwLnp{9'M3Mr|w_|unm'}$*(5]_$?O9zO{{wz4p6vP8Ipu}$BQospf=-Is"
    "nyl'|g53o^c`ov-P`-x+|ZAd<e?<'b9P|LkZOf{B-8`K([srqv&gy1,:}$|s7D{yN6M#cQ"
    "yKpC|*_|#xA#'YfQ}$k$kr7dM#dcnWDg|PHdA#^j&q}$x@(a;k2JB]50ZKp{xRwbkTm.v\\"
    "a3fJ@V[`J#fN`s|sZeHmmHX7`JKiry4sm,bUfz69{rt'k*pBq^l,ut6UVvb9N%r%l:Py3r"
    "[.Z/pBkaz2J4u{GTu)hyp#%gbS$r`mzi9G|3M,r}-Dt?w)_##%fQt$k&n]e$HzFXu(|Fvt"
    "`i#$A#|ykrz+nhs'P#|pm*9[_#,5P#}$}$x&s(i,HzO'&d&g?/_$P#kZ7Dn]e$J{zi8ypi"
    "|hq$_.|cLx`sy;f8GRMQM#7R5$hym*:^`sxrvtfQ|z_N|ye[7dH[}#Ts\\59V@|#z}$}$y;"
    "usL)vXFiA0AukZ8H|fyKurd]|s5<|xq@Cpm,}${,e$KufO?/2Mt$zmM#vtfQ|zRKlEA#P&"
    "fPOwJ#pf_$?/2#@|$w}$?fLxA#^-}$cM^aA#&g#wIv?^|yY#s)Lwt'K#ONyoXKYkpCl0m}"
    "D[9OGU7n([ZrL)[8eRy7uR*PJ#^5xRG>zSf>p]ZrJ_[9q?qgYe^4$r3lY$4SdsSyNvJ_l&"
    "w2I?q#fN*F1n|s9OGUv|w&l+pR-3Nudtyn@|Dfq#^o|s9Pt=oZCol/{VdgkLdwyn1yDed}"
    "?Dzi}#Kje#d}Y${8pSt<q?qgIv|`DH?9}$O{k(LgE;|KU-tJnWOw*F@YP#|yY$b4|.50[2"
    "d}D'kaqhp;JE$r&fH&'6nykYGCv>K$t?yLbwtUn[F{ncOx_.:K#nwF#f|HQs&<&bfQPE?:"
    "uJm*&\\S=*/@|We*w@$@f0,*H])WL<i@^xX@)(a.>MV*p{Zqf&zH%##################"
    "#u2#A#_$A#_$A#_$A#_$@tA#QI#3#0&:p#?1[P*6WQ@i#C1{[;@|1}S%^r#g.d^z#M}$zi"
    "Ze|kU6oZbq5$kF]4[E^nA#_.|}e$Ku^r&WA#_$?/*P_$@t&gA#[;_$A#^j2#A#_*d}@v^z"
    "#8?D?D9Q`s}$5$L|u<3JC/xVz;#s^{?9$M}$x_Y,7uP$", p[6789];

int o[] = {145,1145,1745,2545,3045,4045,4345,5145,5745,6369}, i=0, j=0, l=0;

void e(n, h) {
    for (j = 0; n > 0; n -= !(p[j++] ^ 9));
    for (; !n && j[p] ^ 9; j++)
        write(1, p + p[j][o], o[p[j]+1] - p[j][o]);
    for (i = 0; i < h[o]; i++)
        write(1, d + 7, 1);
}

int main() {
    time_t a = time(&a);

    for (; i < 8476;
        j[p] = k >= 0 ? j < *o ? k - 2 : (p[j+1] = k << 4) : 0,
        j += k >= 0 ? 1 + (j >= *o) : 0,
        l = -k * (k<0),
        i++);

    e(21,4);
    x(hour) e(22,1);
    x(min) e(23,1); e(25,0);
    x(sec) e(24,0);

    return;
}

압축된 데이터 풀기

먼저 k, g, x 세 개의 매크로를 코드에 적절히 끼워 넣는 것부터 시작합시다. 일단 x는 큰 문제 없이 끼워 넣을 수 있습니다.

int main() {
    time_t a = time(&a);

    for (; i < 8476;
        j[p] = k >= 0 ? j < *o ? k - 2 : (p[j+1] = k << 4) : 0,
        j += k >= 0 ? 1 + (j >= *o) : 0,
        l = -k * (k<0),
        i++);

    e(21,4);
    g(localtime(&a)->tm_hour) e(22,1);
    g(localtime(&a)->tm_min) e(23,1); e(25,0);
    g(localtime(&a)->tm_sec) e(24,0);

    return;
}

g는 손쉽게 함수로 변환할 수 있고, k는 변수 il에 의존하지만 이들을 직접 설정하지는 않기 때문에 인자로 il을 받아 쉽게 함수로 변환할 수 있습니다. 따라서,

int k(i, l) {
    return ("C9B7351A@D-/E+F?')G>H%J#=I"
        [(d[(i/13)*2] * 91 + d[(i/13)*2+1] - 3220) & (4096 >> (i % 13)) ?
         l + 1 : l] - 59);
}

void g(n) {
    e(n < 13 ? n : n < 20 ? n + 1 : n > 20 ? 11 + n/10 : 13, 0);
    e(n > 12 && n < 20 ? 26 : n > 20 && n % 10 ? n % 10 : -1, 2);
}

int main() {
    time_t a = time(&a);

    for (; i < 8476;
        j[p] = k(i,l) >= 0 ? j < *o ? k(i,l) - 2 : (p[j+1] = k(i,l) << 4) : 0,
        j += k(i,l) >= 0 ? 1 + (j >= *o) : 0,
        l = -k(i,l) * (k(i,l)<0),
        i++);

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

이제 main 함수의 첫 for 문을 정리하겠습니다. 루프의 증감문에 무려 네 개나 되는 문장을 끼워 넣긴 했습니다만, 콤마로 잘 구분되어 있으니 몸체로 옮길 수 있겠습니다. (마지막 i++는 증감문에 있는 게 더 어울리겠죠.)

int k(i, l) {
    return ("C9B7351A@D-/E+F?')G>H%J#=I"
        [(d[(i/13)*2] * 91 + d[(i/13)*2+1] - 3220) & (4096 >> (i % 13)) ?
         l + 1 : l] - 59);
}

void g(n) {
    e(n < 13 ? n : n < 20 ? n + 1 : n > 20 ? 11 + n/10 : 13, 0);
    e(n > 12 && n < 20 ? 26 : n > 20 && n % 10 ? n % 10 : -1, 2);
}

int main() {
    time_t a = time(&a);

    for (; i < 8476; i++) {
        j[p] = k(i,l) >= 0 ? j < *o ? k(i,l) - 2 : (p[j+1] = k(i,l) << 4) : 0;
        j += k(i,l) >= 0 ? 1 + (j >= *o) : 0;
        l = -k(i,l) * (k(i,l)<0);
    }

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

이 루프 안에서는 k 함수를 호출하기 전에 il을 바꾸는 부분이 없으므로 함수 호출을 맨 앞에 한 번만 하게 할 수 있습니다. 또 이렇게 하면 k가 함수이어야 할 필요도 없겠죠. 따라서,

void g(n) {
    e(n < 13 ? n : n < 20 ? n + 1 : n > 20 ? 11 + n/10 : 13, 0);
    e(n > 12 && n < 20 ? 26 : n > 20 && n % 10 ? n % 10 : -1, 2);
}

int main() {
    time_t a = time(&a);

    for(; i < 8476; i++) {
        int k = ("C9B7351A@D-/E+F?')G>H%J#=I"
            [(d[(i/13)*2] * 91 + d[(i/13)*2+1] - 3220) & (4096 >> (i % 13)) ?
             l + 1 : l] - 59);
        j[p] = k >= 0 ? j < *o ? k - 2 : (p[j+1] = k << 4) : 0;
        j += k >= 0 ? 1 + (j >= *o) : 0;
        l = -k * (k<0);
    }

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

(앞으로는 g는 생략하겠습니다. 앞으로 설명할 코드와는 상관이 없는 부분이거든요.)

이제 k의 값을 잘 봅시다. 중간의 "C9B7351A@D-/E+F?')G>H%J#=I"라는 문자열은 보기에 테이블을 인코딩해 놓은 것으로 보입니다. k는 이 문자열에서 값을 취한 뒤 59를 빼서 결정됩니다. 만약 문자열이 아니라 숫자 배열을 바로 쓸 수 있다면, 테이블은 다음과 같을 것입니다.

static const int ktab[] = {
    'C'-59, '9'-59, 'B'-59, '7'-59, '3'-59, '5'-59, '1'-59, 'A'-59,
    '@'-59, 'D'-59, '-'-59, '/'-59, 'E'-59, '+'-59, 'F'-59, '?'-59,
    '\''-59, ')'-59, 'G'-59, '>'-59, 'H'-59, '%'-59, 'J'-59, '#'-59,
    '='-59, 'I'-59};

우리는 ASCII 문자셋을 쓰고 있으므로 정리하면:

static const int ktab[] = {8, -2, 7, -4, -8, -6, -10, 6, 5, 9, -14, -12,
    10, -16, 11, 4, -20, -18, 12, 3, 13, -22, 15, -24, 2, 14};

이제 이 테이블의 인덱스를 결정하는 수식을 살펴 봅시다.

(d[(i/13)*2] * 91 + d[(i/13)*2+1] - 3220) & (4096 >> (i % 13)) ? l + 1 : l

이 수식을 잘 살펴 보면 유용한 사실 몇 가지를 알 수 있습니다. 하나는 i가 13 주기로 반복된다는 점이고, d는 두 문자씩 같이 읽어서 해석해야 한다는 점이지요. (후자는 d의 인덱스가 (i/13)*2(i/13)*2+1임에서 알 수 있습니다.) 전자를 간단히 하기 위해서, i를 통째로 쓰는 대신에 i/13i%13을 각각 ib라는 이름의 변수로 바꿔서 사용하도록 하겠습니다. 그러면,

int main() {
    static const int ktab[] = {8, -2, 7, -4, -8, -6, -10, 6, 5, 9, -14, -12,
        10, -16, 11, 4, -20, -18, 12, 3, 13, -22, 15, -24, 2, 14};

    time_t a = time(&a);
    int b;

    for (i = 0; i < 8476 / 13; i++) {
        for (b = 0; b < 13; b++) {
            int k = ktab[(d[i*2] * 91 + d[i*2+1] - 3220) & (4096 >> b) ?
                 l + 1 : l];
            j[p] = k >= 0 ? j < *o ? k - 2 : (p[j+1] = k << 4) : 0;
            j += k >= 0 ? 1 + (j >= *o) : 0;
            l = -k * (k<0);
        }
    }

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

b에 의존하지 않는 부분만 별도의 변수로 떼어 내면 다음과 같겠군요.

int main() {
    static const int ktab[] = {8, -2, 7, -4, -8, -6, -10, 6, 5, 9, -14, -12,
        10, -16, 11, 4, -20, -18, 12, 3, 13, -22, 15, -24, 2, 14};

    time_t a = time(&a);
    int b;

    for (i = 0; i < 8476 / 13; i++) {
        int v = d[i*2] * 91 + d[i*2+1] - 3220;
        for (b = 0; b < 13; b++) {
            int k = ktab[v & (4096 >> b) ? l + 1 : l];
            j[p] = k >= 0 ? j < *o ? k - 2 : (p[j+1] = k << 4) : 0;
            j += k >= 0 ? 1 + (j >= *o) : 0;
            l = -k * (k<0);
        }
    }

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

실제로 문자열이 8476 ÷ 13개의 2바이트 묶음으로 구성되어 있는지 확인해 보시길 바랍니다. (문자열은 1304바이트입니다.) 이렇게 보니 v는 13비트이고 k의 값은 v의 각 비트를 하나씩 체크하면서 결정되는 걸 알 수 있습니다. 실제로 체크되는 순서는 다음과 같겠군요.

k는 크게 양수일 때와 음수일 때로 나눌 수 있습니다. 루프 몸체에서는 k >= 0k < 0 식으로 나누는 코드가 많거든요. 비슷하게 j < *oj >= *o라는 서로 반대되는 두 조건도 볼 수 있습니다. 따라서 다음과 같은 코드 대신에,

j[p] = k >= 0 ? j < *o ? k - 2 : (p[j+1] = k << 4) : 0;
j += k >= 0 ? 1 + (j >= *o) : 0;
l = -k * (k<0);

k >= 0일 때와 k < 0일 때를 명확히 나눠 다음과 같이 조건문으로 써 놓으면 더 보기 편하겠습니다.

if (k >= 0) {
    j[p] = j < *o ? k - 2 : (p[j+1] = k << 4);
    j += 1 + (j >= *o);
    l = -k * 0;
} else {
    j[p] = 0;
    j += 0;
    l = -k * 1;
}

비슷하게 k >= 0일 때를 j < *o일 때와 j >= *o일 때로 나누면 다음과 같이 됩니다.

if (k >= 0) {
    if (j < *o) {
        j[p] = k - 2;
        j += 1 + 0;
    } else {
        j[p] = (p[j+1] = k << 4);
        j += 1 + 1;
    }
    l = -k * 0;
} else {
    j[p] = 0;
    j += 0;
    l = -k * 1;
}

지금까지 나온 코드를 다시 원래 코드에 끼워 넣겠습니다. 아, j[p]p[j]와 같다는 건 너무 고전적이라서 설명할 필요도 없을 것 같군요. (자세한 내용은 1984/anonymous 등을 참고하세요.)

int main() {
    static const int ktab[] = {8, -2, 7, -4, -8, -6, -10, 6, 5, 9, -14, -12,
        10, -16, 11, 4, -20, -18, 12, 3, 13, -22, 15, -24, 2, 14};

    time_t a = time(&a);
    int b;

    for (i = 0; i < 8476 / 13; i++) {
        int v = d[i*2] * 91 + d[i*2+1] - 3220;
        for (b = 0; b < 13; b++) {
            int k = ktab[v & (4096 >> b) ? l + 1 : l];
            if (k >= 0) {
                if (j < *o) {
                    p[j] = k - 2;
                    j += 1;
                } else {
                    p[j] = p[j+1] = k << 4;
                    j += 2;
                }
                l = 0;
            } else {
                p[j] = 0;
                l = -k;
            }
        }
    }

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

이제 코드가 무슨 작동을 하는지 머릿속으로 생각해 봅시다. 각 비트에 대해서 루프 본체는 다음과 같은 일을 합니다.

일종의 상태 기계(state machine) 같죠? 저자가 힌트에서 밝힌 대로, 이 코드는 허프만 코드를 디코딩하는 부분입니다. 이제 허프만 코드라는 걸 알고 있으니까 위의 작동 과정을 다시 설명하겠습니다. (뒤에 굵게 표시한 부분이 추가된 부분입니다.)

*o, 즉 o[0]은 맨 처음의 코드에서 볼 수 있듯이 145로 설정되어 있습니다. 따라서 허프만 코드를 디코딩하는 과정은 다음과 같이 정리할 수 있습니다.

int main() {
    /* 허프만 트리를 나타내는 테이블.
     * - 음수 k는 다음 비트가 0이면 상태가 -k, 1이면 -k+1이 됨을 나타낸다.
     * - 양수 k는 트리의 잎 노드로 데이터를 나타낸다.
     * 첫 비트가 0일 때 상태는 0이며, 1일 때 상태는 1이다.
     */
    static const int ktab[] = {8, -2, 7, -4, -8, -6, -10, 6, 5, 9, -14, -12,
        10, -16, 11, 4, -20, -18, 12, 3, 13, -22, 15, -24, 2, 14};

    time_t a = time(&a);
    int b;
    int state = 0; /* 원래 l이었음 */

    for (i = 0; i < 8476 / 13; i++) {
        int v = d[i*2] * 91 + d[i*2+1] - 3220;

        /* (1<<12), (1<<11), ..., (1<<0)에 해당하는 비트를 처리한다. */
        for (b = 1 << 12; b > 0; b >>= 1) {
            int cur = ktab[v & b ? state + 1 : state]; /* 원래 k였음 */
            if (cur >= 0) {
                if (j < 145) {
                    /* 한 개의 바이트를 설정 */
                    p[j] = cur - 2;
                    j += 1;
                } else {
                    /* 두 개의 바이트를 설정 */
                    p[j] = p[j+1] = cur << 4;
                    j += 2;
                }
                state = 0;
            } else {
                state = -cur;
            }
        }
    }

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

이 규칙에 따라서 실제로 p 테이블을 채우면 다음과 같은 문자열을 얻을 수 있습니다.

unsigned char p[6789] =
    /* 첫 145바이트 */
    "\x06\x02\x05\x04\x09\x04\x00\x03\x09\x07\x04\x09\x07\x05\x02\x09\x06"
    "\x04\x05\x09\x06\x00\x08\x09\x06\x02\x06\x06\x09\x06\x01\x08\x01\x03"
    "\x09\x01\x02\x07\x09\x03\x00\x03\x09\x07\x01\x03\x09\x02\x05\x01\x08"
    "\x01\x03\x09\x07\x04\x01\x08\x08\x09\x07\x04\x01\x03\x07\x02\x09\x07"
    "\x01\x05\x07\x02\x09\x06\x04\x05\x07\x02\x09\x06\x01\x06\x07\x02\x09"
    "\x06\x02\x06\x06\x07\x02\x09\x06\x01\x08\x01\x03\x07\x02\x09\x01\x02"
    "\x07\x02\x09\x03\x00\x03\x07\x02\x09\x07\x01\x07\x00\x03\x02\x06\x09"
    "\x00\x01\x05\x05\x06\x09\x03\x02\x02\x07\x06\x09\x06\x01\x07\x01\x03"
    "\x03\x06\x09\x01\x03\x03\x09\x03\x09"

    /* 그 다음 6246바이트 */
    "\x60\x60\x80\x80\xa0\xa0\xc0\xc0\xa0\xa0\x60\x60\x50\x50\x80\x80\x90"
    "\x90\x80\x80\x60\x60\x40\x40\x60\x60\x80\x80\x90\x90\x80\x80\x70\x70"
    "\x70\x70\x80\x80\x90\x90\x80\x80\x70\x70\x70\x70\x70\x70\x80\x80\x80"
    "\x80\x80\x80\x70\x70\x70\x70\x80\x80\x80\x80\x80\x80\x70\x70\x70\x70"
    "\x70\x70\x70\x70\x70\x70\x70\x70\x70\x70\x70\x70\x80\x80\x80\x80\x80"
    /* ... 358줄 생략 ... */
    "\x80\x80\x80\x80\x80\x70\x70\x70\x70\x80\x80\x80\x80\x80\x80\x80\x80"
    "\x80\x80\x70\x70\x80\x80\x80\x80\x80\x80\x70\x70\x60\x60\x60\x60\x60"
    "\x60\x60\x60\x50\x50\x40\x40\x50\x50\x50\x50\xd0\xd0\xe0\xe0\x80\x80"
    "\x70\x70\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80"
    "\x80\x80\x80\x80\x80\x80\x80";

이제 허프만 트리 압축을 푸는 코드는 필요가 없으므로 지울 수 있습니다.

#include <unistd.h> 
#include <time.h>

unsigned char *d = "...";
unsigned char p[6789] = "...";
int o[] = {145,1145,1745,2545,3045,4045,4345,5145,5745,6369}, i=0, j=0;

void e(n, h) {
    for (j = 0; n > 0; n -= !(p[j++] ^ 9));
    for (; !n && j[p] ^ 9; j++)
        write(1, p + p[j][o], o[p[j]+1] - p[j][o]);
    for (i = 0; i < h[o]; i++)
        write(1, d + 7, 1);
}

void g(n) {
    e(n < 13 ? n : n < 20 ? n + 1 : n > 20 ? 11 + n/10 : 13, 0);
    e(n > 12 && n < 20 ? 26 : n > 20 && n % 10 ? n % 10 : -1, 2);
}

int main() {
    time_t a = time(&a);

    e(21,4);
    g(localtime(&a)->tm_hour); e(22,1);
    g(localtime(&a)->tm_min); e(23,1); e(25,0);
    g(localtime(&a)->tm_sec); e(24,0);

    return;
}

사운드 출력과 무음 처리

다음으로 분석할 함수는 e입니다. 이 함수는 o, p, d 세 개의 배열을 모두 사용하고 있습니다.

void e(n, h) {
    for (j = 0; n > 0; n -= !(p[j++] ^ 9));
    for (; !n && j[p] ^ 9; j++)
        write(1, p + p[j][o], o[p[j]+1] - p[j][o]);
    for (i = 0; i < h[o]; i++)
        write(1, d + 7, 1);
}

먼저 첫 for 문장부터 풀겠습니다. !(p[j++] ^ 9)는 어떤 결과를 낼까요? 일단 p[j++] ^ 9p[j++]가 9이면 0이 되고 아니면 0 아닌 숫자가 되는데 그렇다면 이 식은 !(p[j++] != 9)와 같다는 얘기가 되고, !을 걷어 내면 (p[j++] == 9)가 됩니다. j++를 빼고 나머지를 루프 몸체로 옮기면 다음과 같이 됩니다.

void e(n, h) {
    for (j = 0; n > 0; j++)
        n -= (p[j] == 9);
    for (; !n && j[p] ^ 9; j++)
        write(1, p + p[j][o], o[p[j]+1] - p[j][o]);
    for (i = 0; i < h[o]; i++)
        write(1, d + 7, 1);
}

(p[j] == 9)는 0 아니면 1이므로 해당 문장을 if 문으로 고쳐 써도 괜찮을 것입니다.

void e(n, h) {
    for (j = 0; n > 0; j++)
        if (p[j] == 9) --n;
    for (; !n && j[p] ^ 9; j++)
        write(1, p + p[j][o], o[p[j]+1] - p[j][o]);
    for (i = 0; i < h[o]; i++)
        write(1, d + 7, 1);
}

그 다음 for 문에도 비슷한 코드가 있습니다. j[p] ^ 9j[p] != 9일 때 참(0이 아님)이 되고 아니면 거짓이 되지요. 또한 !nn == 0은 같은 의미이므로,

void e(n, h) {
    for (j = 0; n > 0; j++)
        if (p[j] == 9) --n;
    for (; n == 0 && j[p] != 9; j++)
        write(1, p + p[j][o], o[p[j]+1] - p[j][o]);
    for (i = 0; i < h[o]; i++)
        write(1, d + 7, 1);
}

배열과 인덱스의 순서를 바꾸어 놓은 고전적인 코드도 정리하면 다음과 같이 됩니다.

void e(n, h) {
    for (j = 0; n > 0; j++)
        if (p[j] == 9) --n;
    for (; n == 0 && p[j] != 9; j++)
        write(1, p + o[p[j]], o[p[j]+1] - o[p[j]]);
    for (i = 0; i < o[h]; i++)
        write(1, d + 7, 1);
}

이제 첫번째 루프를 다시 살펴 봅시다. 이 루프는 p의 처음 부분을 계속 읽다가, 9가 n번 나타나면 j를 그 다음 위치를 가리키게 합니다. 즉 p의 처음 부분(아마도 첫 145바이트겠죠?)의 각 조각을 구분하는 구분자가 9인 것입니다. 원래 p의 내용을 여기에 따라 정리하면,

unsigned char p[6789] =
    "\x06\x02\x05\x04\x09"
    "\x04\x00\x03\x09"
    "\x07\x04\x09"
    "\x07\x05\x02\x09"
    "\x06\x04\x05\x09"
    "\x06\x00\x08\x09"
    "\x06\x02\x06\x06\x09"
    "\x06\x01\x08\x01\x03\x09"
    "\x01\x02\x07\x09"
    "\x03\x00\x03\x09"
    "\x07\x01\x03\x09"
    "\x02\x05\x01\x08\x01\x03\x09"
    "\x07\x04\x01\x08\x08\x09"
    "\x07\x04\x01\x03\x07\x02\x09"
    "\x07\x01\x05\x07\x02\x09"
    "\x06\x04\x05\x07\x02\x09"
    "\x06\x01\x06\x07\x02\x09"
    "\x06\x02\x06\x06\x07\x02\x09"
    "\x06\x01\x08\x01\x03\x07\x02\x09"
    "\x01\x02\x07\x02\x09"
    "\x03\x00\x03\x07\x02\x09"
    "\x07\x01\x07\x00\x03\x02\x06\x09"
    "\x00\x01\x05\x05\x06\x09"
    "\x03\x02\x02\x07\x06\x09"
    "\x06\x01\x07\x01\x03\x03\x06\x09"
    "\x01\x03\x03\x09"
    "\x03\x09"
    /* 이하 데이터 생략 */;

...로 27개의 조각이 나타납니다. 따라서 e에 전달되는 n의 최대값은 26이어야 겠지요. 이게 실제로 그런지는 나중에 살펴 보고, 일단은 나머지 코드를 해석해 보겠습니다.

두번째 루프는 p[j]가 가리키고 있는 데이터들을 그대로 표준 출력(파일 서술자 1)에 출력합니다. 루프에서 n은 바꾸지 않기 때문에 처음의 n == 0 조건은 바깥의 if 문으로 빼 낼 수 있겠고, 뭐 예상할 수 있듯이 두번째 조건은 현재 p[j]부터 다음으로 나오는 9까지 처리하는 역할을 합니다. 여기까지 일단 중간 정리하면,

void e(n, h) {
    /* p에서 처음 n개의 9를 건너 뛴다. */
    for (j = 0; n > 0; j++)
        if (p[j] == 9) --n;

    if (n == 0) {
        /* 그 다음 9까지 p[j]를 처리한다. */
        for (; p[j] != 9; j++)
            write(1, p + o[p[j]], o[p[j]+1] - o[p[j]]);
    }

    for (i = 0; i < o[h]; i++)
        write(1, d + 7, 1);
}

두번째 루프의 몸체는 p에서 o[p[j]]o[p[j]+1] 사이의 문자열을 출력하도록 되어 있습니다. 즉, o는 각 사운드 데이터가 어디부터 어디까지인지 나타내는 역할을 하는 것이지요. 코드에 따르면 첫번째(0번) 사운드는 p[145]에서 시작해서 p[1145] 앞에서 끝나고, ..., 아홉번째(8번) 사운드는 p[5745]에서 시작해서 p[6369] 앞에서 끝나게 됩니다. 열번째(9번) 사운드는 존재할 리가 없으니 이걸 대신에 구분자로 사용한 것이지요. 이걸 코드에 반영하면,

void e(n, h) {
    /* p에서 처음 n개의 9를 건너 뛴다. */
    for (j = 0; n > 0; j++)
        if (p[j] == 9) --n;

    if (n == 0) {
        /* 그 다음 9가 나올 때까지 p[j]번 사운드를 출력한다. */
        for (; p[j] != 9; j++) {
            int off = o[p[j]], limit = o[p[j]+1];
            write(1, p + off, limit - off);
        }
    }

    for (i = 0; i < o[h]; i++)
        write(1, d + 7, 1);
}

마지막 세번째 루프는 단순히 o[h] 길이만큼 d[7]이 가리키는 문자를 출력합니다. d[7]은 다름 아닌 'w'인데, 이게 무슨 의미일까요?

사용 예에서 봤듯 우리가 출력하는 사운드 데이터는 모노 채널에 8비트 부호 없는 정수로 표현된 PCM 데이터를 사용합니다. "모노 채널"이라는 건 예를 들어 왼쪽 오른쪽 스피커에서 같은 소리가 나온다는 소리고, PCM은 다음 그림을 사용해서 설명할 수 있겠습니다. TODO 아스키 아트 말고 제대로 된 그림으로...

          ,-.--._
        ,|  |  | `.
       / |  |  |  |\
     .|  |  |  |  | \
    .'|  |  |  |  |  .
   .' |  |  |  |  |  |\
   |  |  |  |  |  |  | \
  /|  |  |  |  |  |  |  .
 | |  |  |  |  |  |  |  |
 / |  |  |  |  |  |  |  |\
+--+--+--+--+--+--+--+--+--------------------------+
                          \|  |  |  |  |  |  |  | /
                           |  |  |  |  |  |  |  |/
                            \ |  |  |  |  |  |  +
                            `.|  |  |  |  |  | /
                             ||  |  |  |  |  |/
                              +  |  |  |  |  +
                               `.|  |  |  | /
                                 +  |  |  |'
                                  `.|  |,'
                                     `'

우리가 듣는 소리는 위와 같이 진동으로 나타낼 수 있습니다. 이 진동을 표현하는 방법은 여러 가지가 있을텐데, 예를 들어 진폭이 얼마이고 주기가 얼마인 파동이라고 설명한다면 이게 가장 간단하겠죠. 하지만 우리가 듣는 소리는 수많은 파동들이 서로 겹쳐서 생긴 것이라 실제로는 이렇게 표현하기 매우 힘듭니다. (물론 할 수만 있다면 매우 효율적이겠죠. MP3, Ogg Vorbis 같은 대부분의 "손실" 압축 포맷이 비슷한 방법을 사용합니다.) 따라서 위에 보이는 세로 선처럼 일정 주기로 진폭을 계산해서 저장하게 되는데, 이 일정 주기를 "샘플링 레이트"1라고 하고 이렇게 저장된 진폭을 "샘플"이라고 합니다.

"8비트 부호 없는 정수"로 저장한다는 얘기는 바로 이 샘플이 이렇게 저장된다는 뜻입니다. 다른 저장 방법으로는 16비트 부호 있는 정수라거나 32비트 부호 있는 정수, 32비트 부동 소숫점 실수 등이 있지요. 그런데 부호가 없다면 위의 그림에서 가로선에 해당하는, 진폭이 0인 점은 어떻게 저장할까요? 이 경우 진폭이 아래로 가장 클 때가 0이고 위로 가장 클 때가 최대값이 됩니다. 따라서 중간점은 그 중간, 8비트의 경우 127이나 128이 됩니다.

'w'는 ASCII 코드로 119로 중간값에 상당히 가까운 글자이고, 따라서 이 글자를 반복하면 아쉽게나마(?) 아무 소리도 안 나는 부분을 흉내낼 수 있습니다. 따라서 마지막 세 번째 루프는 지정된 길이만큼 무음(silence)을 첨가하는 역할을 하게 됩니다. 여기까지 코드를 정리하면 다음과 같이 됩니다.

#include <unistd.h> 
#include <time.h>

unsigned char p[6789] = "...";
int o[] = {145,1145,1745,2545,3045,4045,4345,5145,5745,6369}, i=0, j=0;

void say_word(int word /* n에서 바꿈 */, int silence /* h에서 바꿈 */) {
    /* word에 해당하는 데이터로 이동한다. */
    for (j = 0; word > 0; j++)
        if (p[j] == 9) --word;

    if (word == 0) {
        /* 다음 구분자(9)가 나올 때까지 해당 사운드 데이터를 출력한다. */
        for (; p[j] != 9; j++) {
            int off = o[p[j]], limit = o[p[j]+1];
            write(1, p + off, limit - off);
        }
    }

    /* o[silence] 길이만큼 무음을 첨가한다. */
    static const char silencedata[1] = {128};
    for (i = 0; i < o[silence]; i++)
        write(1, silencedata, 1);
}

void g(n) {
    say_word(n < 13 ? n : n < 20 ? n + 1 : n > 20 ? 11 + n/10 : 13, 0);
    say_word(n > 12 && n < 20 ? 26 : n > 20 && n % 10 ? n % 10 : -1, 2);
}

int main() {
    time_t a = time(&a);

    say_word(21,4);
    g(localtime(&a)->tm_hour); say_word(22,1);
    g(localtime(&a)->tm_min); say_word(23,1); say_word(25,0);
    g(localtime(&a)->tm_sec); say_word(24,0);

    return 0;
}

이제 d는 정말로 아무 곳에서도 쓰이지 않으므로 지워도 됩니다.

숫자 읽기

다음으로 확인할 함수는 g입니다. 이 함수는, 함수 호출에서 볼 수 있듯이 지정된 "숫자"에 해당하는 단어들을 말하는 역할을 합니다. 먼저 삼항 연산자 ?:를 if 문으로 바꾸도록 하겠습니다.

void g(n) {
    int first, second;

    if (n < 13) {
        first = n;
    } else if (n < 20) {
        first = n + 1;
    } else if (n == 20) {
        first = 13;
    } else { /* n > 20 */
        first = 11 + n/10;
    }

    if (n > 12 && n < 20) {
        second = 26;
    } else if (n > 20 && n % 10) {
        second = n % 10;
    } else {
        second = -1;
    }

    say_word(first, 0);
    say_word(second, 2);
}

흥미롭게도 say_word의 첫 인자에 -1이 들어 가는 경우가 있습니다! 그런데 어떻게 잘 작동했던 거죠? say_word의 실제 내용을 다시 한 번 확인해 봅시다.

void say_word(int word, int silence) {
    /* word에 해당하는 데이터로 이동한다. */
    for (j = 0; word > 0; j++)
        if (p[j] == 9) --word;

    if (word == 0) {
        /* 다음 구분자(9)가 나올 때까지 해당 사운드 데이터를 출력한다. */
        for (; p[j] != 9; j++) {
            int off = o[p[j]], limit = o[p[j]+1];
            write(1, p + off, limit - off);
        }
    }

    /* o[silence] 길이만큼 무음을 첨가한다. */
    static const char silencedata[1] = {128};
    for (i = 0; i < o[silence]; i++)
        write(1, silencedata, 1);
}

아하, 첫 루프에서 word가 양수이면 루프가 끝난 뒤에는 0으로 바뀌지만 음수이면 음수인 채로 유지되는군요. 따라서 두번째 루프는 word가 음수일 때는 아예 실행조차 되지 않고, 무음만 잘 첨가되던 것이었습니다. 반대로 say_word의 첫 인자가 가질 수 있는 최대값은 26이고 코드에 잘 나타나 있으니 크게 염려할 필요는 없겠습니다. 그래도 코드를 이해하기 편하게 하려면 word가 음수인 경우를 따로 처리해 주는 게 좋겠죠.

void say_word(int word, int silence) {
    if (word >= 0) {
        /* word에 해당하는 데이터로 이동한다. */
        for (j = 0; word > 0; j++)
            if (p[j] == 9) --word;

        /* 다음 구분자(9)가 나올 때까지 해당 사운드 데이터를 출력한다. */
        for (; p[j] != 9; j++) {
            int off = o[p[j]], limit = o[p[j]+1];
            write(1, p + off, limit - off);
        }
    }

    /* o[silence] 길이만큼 무음을 첨가한다. */
    static const char silencedata[1] = {128};
    for (i = 0; i < o[silence]; i++)
        write(1, silencedata, 1);
}

다시 g 함수로 돌아 옵시다. 이제 맨 아래에 있는 함수 호출을 각 경우에 따라 분리해서 나눠 봅시다. 제대로 정리가 되면 다음과 같이 될 것입니다.

void g(n) {
    if (n < 13) {
        say_word(n, 0);
        say_word(-1, 2);
    } else if (n < 20) {
        say_word(n + 1, 0);
        say_word(26, 2);
    } else if (n == 20) {
        say_word(13, 0);
        say_word(-1, 2);
    } else if (n % 10 == 0) { /* 30, 40, 50 */
        say_word(11 + n/10, 0);
        say_word(-1, 2);
    } else {
        say_word(11 + n/10, 0);
        say_word(n % 10, 2);
    }
}

함수 호출은 여전히 각 경우에 따라 두 번 필요합니다. 왜냐하면 say_word 함수의 두번째 인자는 o의 인덱스를 가리키거든요. 그냥 say_word(-1, 2) 한다면 o[2], 즉 1745 샘플만큼 무음이 첨가되겠지만, say_word(-1, 0) 하면 무음이 첨가되지 않는 게 아니라 o[0], 즉 145 샘플만큼 무음이 첨가되기 때문에 그냥 0인 경우를 날려 버릴 수가 없습니다. 이건 나중에 적절히 처리하기로 하죠.

하여튼 각 숫자에 해당하는 영어 단어를 생각하면 이 코드가 어떻게 구성되었는지 이해하기 쉬울 것입니다. 보니까 n이 20일 경우는 30, 40, 50일 경우와 합칠 수 있을 것 같아 보이는데 합쳐 보기로 하죠.

void g(n) {
    if (n < 13) {
        say_word(n, 0); /* word# 0, 1, ..., 12 */
        say_word(-1, 2);
    } else if (n < 20) {
        say_word(n + 1, 0); /* word# 14, 15, ..., 19, 20 */
        say_word(26, 2);
    } else if (n % 10 == 0) { /* 20, 30, 40, 50 */
        say_word(11 + n/10, 0); /* word# 13, 14, 15, 16 */
        say_word(-1, 2);
    } else {
        say_word(11 + n/10, 0);
        say_word(n % 10, 2);
    }
}

뒤에 이해를 돕기 위해 해당 코드가 참조하는 단어의 번호도 써 놓았습니다. 이제 p 맨 앞에 있는 단어 목록이 어떻게 구성되어 있는지 아시겠나요? 한 번 데이터에 주석을 달아 봤습니다.

unsigned char p[6789] =
    "\x06\x02\x05\x04\x09"             /* 0: zero */
    "\x04\x00\x03\x09"                 /* 1: one */
    "\x07\x04\x09"                     /* 2: two */
    "\x07\x05\x02\x09"                 /* 3: three */
    "\x06\x04\x05\x09"                 /* 4: four */
    "\x06\x00\x08\x09"                 /* 5: five */
    "\x06\x02\x06\x06\x09"             /* 6: six */
    "\x06\x01\x08\x01\x03\x09"         /* 7: seven */
    "\x01\x02\x07\x09"                 /* 8: eight */
    "\x03\x00\x03\x09"                 /* 9: nine */
    "\x07\x01\x03\x09"                 /* 10: ten */
    "\x02\x05\x01\x08\x01\x03\x09"     /* 11: eleven */
    "\x07\x04\x01\x08\x08\x09"         /* 12: twelve */
    "\x07\x04\x01\x03\x07\x02\x09"     /* 13: twent- */
    "\x07\x01\x05\x07\x02\x09"         /* 14: thirt- */
    "\x06\x04\x05\x07\x02\x09"         /* 15: fourt- */
    "\x06\x01\x06\x07\x02\x09"         /* 16: fift- */
    "\x06\x02\x06\x06\x07\x02\x09"     /* 17: sixt- */
    "\x06\x01\x08\x01\x03\x07\x02\x09" /* 18: sevent- */
    "\x01\x02\x07\x02\x09"             /* 19: eight- */
    "\x03\x00\x03\x07\x02\x09"         /* 20: ninet- */
    "\x07\x01\x07\x00\x03\x02\x06\x09" /* 21: The time is */
    "\x00\x01\x05\x05\x06\x09"         /* 22: hours */
    "\x03\x02\x02\x07\x06\x09"         /* 23: minutes */
    "\x06\x01\x07\x01\x03\x03\x06\x09" /* 24: seconds */
    "\x01\x03\x03\x09"                 /* 25: and */
    "\x03\x09"                         /* 26: -een */
    /* 이하 데이터 생략 */;

여기서 "thirty"와 같은 단어를 나타낼 때는 그냥 "thirt-"까지만 발음하고 끝내는 것을 볼 수 있습니다. 반면 "thirteen"은 "thirt-"와 "-een" 두 단어를 이어서 말하지요.

인코딩된 데이터를 보면 좀 더 황당한 것을 볼 수 있습니다. "zero", "four", "five", "six", "seven" 등은 모두 똑같은 발음(6번 사운드)으로 시작합니다! 이것은 저자가 힌트에서 말하듯 "s"와 "f" 같이 낮은 주파수 대역에서 비슷하게 들리는 사운드를 합쳐 버렸기 때문입니다. 출력되는 사운드는 본래 샘플 레이트가 8kHz 밖에 안 되는데다가 실 샘플 레이트는 4kHz에 불과하기 때문에 비슷하게 들리는 것도 당연합니다. (허프만 코드를 디코딩하는 과정에서 145바이트 뒷쪽은 두 글자씩 디코딩한 것을 생각해 보세요.)

이제 모든 코드를 분석했으니, 완벽하게 정돈된 코드만 있으면 될까요? 위에서 설명한 자료 구조들을 서로 다른 변수로 분리하고 코드를 정리한 제 코드는 다음과 같습니다.

#include <unistd.h> 
#include <time.h>

static const unsigned char samples[6246] =
    "\x60\x60\x80\x80\xa0\xa0\xc0\xc0\xa0\xa0\x60\x60\x50\x50\x80\x80\x90"
    "\x90\x80\x80\x60\x60\x40\x40\x60\x60\x80\x80\x90\x90\x80\x80\x70\x70"
    "\x70\x70\x80\x80\x90\x90\x80\x80\x70\x70\x70\x70\x70\x70\x80\x80\x80"
    "\x80\x80\x80\x70\x70\x70\x70\x80\x80\x80\x80\x80\x80\x70\x70\x70\x70"
    "\x70\x70\x70\x70\x70\x70\x70\x70\x70\x70\x70\x70\x80\x80\x80\x80\x80"
    /* ... 358줄 생략 ... */
    "\x80\x80\x80\x80\x80\x70\x70\x70\x70\x80\x80\x80\x80\x80\x80\x80\x80"
    "\x80\x80\x70\x70\x80\x80\x80\x80\x80\x80\x70\x70\x60\x60\x60\x60\x60"
    "\x60\x60\x60\x50\x50\x40\x40\x50\x50\x50\x50\xd0\xd0\xe0\xe0\x80\x80"
    "\x70\x70\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80"
    "\x80\x80\x80\x80\x80\x80\x80";

static const int sampleoff[9] = {0, 1000, 1600, 2400, 2900, 3900, 4200, 5000, 5600};
static const int samplelen[9] = {1000, 600, 800, 500, 1000, 300, 800, 600, 624};

static const int words[][8] = {
    {6, 2, 5, 4, -1},          /* 0: zero */
    {4, 0, 3, -1},             /* 1: one */
    {7, 4, -1},                /* 2: two */
    {7, 5, 2, -1},             /* 3: three */
    {6, 4, 5, -1},             /* 4: four */
    {6, 0, 8, -1},             /* 5: five */
    {6, 2, 6, 6, -1},          /* 6: six */
    {6, 1, 8, 1, 3, -1},       /* 7: seven */
    {1, 2, 7, -1},             /* 8: eight */
    {3, 0, 3, -1},             /* 9: nine */
    {7, 1, 3, -1},             /* 10: ten */
    {2, 5, 1, 8, 1, 3, -1},    /* 11: eleven */
    {7, 4, 1, 8, 8, -1},       /* 12: twelve */
    {7, 4, 1, 3, 7, 2, -1},    /* 13: twent- */
    {7, 1, 5, 7, 2, -1},       /* 14: thirt- */
    {6, 4, 5, 7, 2, -1},       /* 15: fourt- */
    {6, 1, 6, 7, 2, -1},       /* 16: fift- */
    {6, 2, 6, 6, 7, 2, -1},    /* 17: sixt- */
    {6, 1, 8, 1, 3, 7, 2, -1}, /* 18: sevent- */
    {1, 2, 7, 2, -1},          /* 19: eight- */
    {3, 0, 3, 7, 2, -1},       /* 20: ninet- */
    {7, 1, 7, 0, 3, 2, 6, -1}, /* 21: The time is */
    {0, 1, 5, 5, 6, -1},       /* 22: hours */
    {3, 2, 2, 7, 6, -1},       /* 23: minutes */
    {6, 1, 7, 1, 3, 3, 6, -1}, /* 24: seconds */
    {1, 3, 3, -1},             /* 25: and */
    {3, -1},                   /* 26: -een */
};

void say_word(int word) {
    int i;
    for (i = 0; words[word][i] >= 0; ++i) {
        int sample = words[word][i];
        write(1, samples + sampleoff[sample], samplelen[sample]);
    }
}

void put_silence(int length) {
    static const char silencedata[1] = {128};
    int i;
    for (i = 0; i < length; i++) {
        write(1, silencedata, 1);
    }
}

void say_number(int n) {
    if (n < 13) {
        say_word(n); /* zero, one, ..., twelve */
        put_silence(145 + 1745);
    } else if (n < 20) {
        say_word(n + 1); /* thirt-, fourt-, ..., ninet- */
        put_silence(145);
        say_word(26); /* -een */
        put_silence(1745);
    } else {
        say_word(11 + n / 10); /* twent-, thirt-, ... */
        put_silence(145);
        if (n % 10 > 0) say_word(n % 10); /* one, ..., nine */
        put_silence(1745);
    }
}

int main(void) {
    time_t t = time(NULL);
    struct tm *tm = localtime(&t);

    say_word(21); /* The time is */
    put_silence(3045);

    say_number(tm->tm_hour);
    say_word(22); /* hours */
    put_silence(1145);

    say_number(tm->tm_min);
    say_word(23); /* minutes */
    put_silence(1145);
    say_word(25); /* and */
    put_silence(145);

    say_number(tm->tm_sec);
    say_word(24); /* seconds */
    put_silence(145);

    return 0;
}

실제로 각 사운드에 어떤 발음이 들어 있는지 확인하는 건 여러분의 몫으로 남겨 두겠습니다. :3 아, 웨이브 파일로 바로 출력하게 만드는 것도 좋겠군요.


  1. 보통 많은 사운드 데이터가 44.1kHz나 48kHz의 샘플링 레이트를 가지고 있습니다. 이는 1초당 샘플의 갯수가 44100개 또는 48000개라는 것이고, 나이퀴스트-샤논 법칙에 따라 이렇게 저장된 소리는 22kHz 또는 24kHz까지의 주파수 대역을 보존할 수 있습니다. 물론 이는 인간이 들을 수 있는 주파수의 한계에 해당하죠.


Copyright © 1999–2009, Kang Seonghoon.