U0 SongPuppet(CTask *task, I64 passes)
{
    CDebugInfo *debug_info;
    I64 i, start, end, rip, last_rip;
    CHashFun *tmpf=NULL;

    for (i = 0; i < 250 && TaskValidate(task); i++)
    {
        if (tmpf = HashFind("Song", task->hash_table, HTT_FUN))
            break;
        Sleep(1);
    }
    if (tmpf && (debug_info = tmpf->debug_info))
    {
        start = debug_info->body[0];
        end   = debug_info->body[debug_info->max_line + 1 - debug_info->min_line];
        last_rip = 0;
        while (TRUE)
        {
            i = 0;
            while (TaskValidate(task) && (rip = TaskCaller(task, i++)))
            {
                if (start <= rip < end) {
                    if (rip < last_rip && --passes <= 0)
                        return;
                    last_rip = rip;
                }
            }
            Sleep(1);
        }
    }
}

U0 JukeSongPuppet(CTask *juke_task, I64 passes, I64 song_num, U8 *name)
{
    Bool    found;
    CTask  *task;
    I64     i;

    if (FileExtDot(name))
        FileExtRemove(name);
    BirthWait(&juke_task->popup_task);
    TaskWait(juke_task->popup_task);
    MessagePost(juke_task->popup_task, MESSAGE_KEY_DOWN_UP, 0, SC_CURSOR_UP + SCF_CTRL);
    TaskWait(juke_task->popup_task);
    for (i = 0; i < song_num; i++)
    {
        MessagePost(juke_task->popup_task, MESSAGE_KEY_DOWN_UP, 0, SC_CURSOR_RIGHT);
        TaskWait(juke_task->popup_task);
    }

    MessagePost(juke_task->popup_task, MESSAGE_KEY_DOWN_UP, CH_SPACE, 0);
    TaskWait(juke_task->popup_task);
    Sleep(500);

    found = FALSE;
    task = Fs->next_task;
    while (task != Fs) {
        if (!StrCompare(task->task_title, name))
        {
            found = TRUE;
            break;
        }
        task = task->next_task;
    }

    if (found)
    {//Position cursor on song title during song
        TaskWait(juke_task->popup_task);
        MessagePost(juke_task->popup_task, MESSAGE_KEY_DOWN_UP, 0, SC_CURSOR_UP + SCF_CTRL);
        TaskWait(juke_task->popup_task);
        for (i = 0; i < song_num; i++)
        {
            MessagePost(juke_task->popup_task, MESSAGE_KEY_DOWN_UP, 0, SC_CURSOR_RIGHT);
            TaskWait(juke_task->popup_task);
        }
        SongPuppet(task, passes);
    }
    Kill(task);
}

public U0 JukeSongsPuppet(U8 *dirname="~/Psalmody", I64 passes=2, I64 start_song=0, I64 end_song=I64_MAX)
{//Help make music video by puppeting JukeBox task.
    I64          i;
    CDirEntry   *tmpde, *tmpde1;
    CTask       *juke_task = User("JukeBox(0x%X);\n", dirname);
    F64          t0;

    Cd(dirname);
    tmpde1 = FilesFind("*", FUF_RECURSE | FUF_JUST_TXT | FUF_JUST_FILES);
    for (tmpde = tmpde1, i = 0; tmpde && i < start_song; i++)
        tmpde = tmpde->next;
    if (screencast.record)
        t0 = screencast.t0_tS;
    else
        t0 = tS;
    for (i = start_song; tmpde && i < end_song; i++)
    {
        "%12.6fs %s\n", tS - t0, tmpde->full_name;
        JukeSongPuppet(juke_task, passes, i, tmpde->name);
        tmpde = tmpde->next;
    }
    DirTreeDel(tmpde1);
    Kill(juke_task);
}