Skip to content

突发奇想,使用 TiKZ 绘制了一个音乐播放器, 其实单纯画这样一个播放器比较简单, 于是多做了一些

  • 背景图片,颜色支持自定义
  • 进度条自动解析
  • 歌词解析
  • 对当前正在播放的行歌词突出显示
  • ...

效果

效果效果

实现

编写 audio 宏包, audio.sty

latex3
\ProvidesPackage{audio}[xxx]

\ExplSyntaxOn

\tl_new:N \l__audio_name_tl % 歌曲名
\tl_new:N \l__audio_background_picture_tl % 背景图片 
\tl_new:N \l__audio_author_tl % 歌手
\tl_new:N \l__audio_words_filename_tl % 歌词
\tl_new:N \l__audio_current_line_style_tl % 歌词突出行样式
\tl_new:N \l__audio_words_style_tl % 歌词样式
\tl_new:N \l__audio_current_time_tl % 当前时间
\tl_new:N \l__audio_total_time_tl % 总时长
\tl_new:N \l__audio_background_color_tl 
\tl_new:N \l__audio_avatar_image_tl 

\ior_new:N \l__audio_words_ior
\bool_new:N \l__audio_play_bool % 控制是否为播放状态
\dim_new:N \l__audio_progress_line_length_dim % 进度条总长度
\dim_new:N \l__audio_words_line_skip_dim % 歌词行距
\int_new:N \l__audio_current_line_int % 播放当前行
\int_new:N \l__audio_max_show_lines_int % 显示多少行

% 临时变量

\seq_new:N \l__audio_tmpa_seq
\seq_new:N \l__audio_tmpb_seq
\seq_new:N \l__audio_tmpc_seq
\seq_new:N \l__audio_tmpd_seq

\int_new:N \l__audio_tmpa_int 
\int_new:N \l__audio_tmpb_int 
\int_new:N \l__audio_tmpc_int 
\int_new:N \l__audio_tmpd_int 
\int_new:N \l__audio_file_line_number_int 

\tl_new:N \l__audio_tmpa_tl
\tl_new:N \l__audio_tmpb_tl
\tl_new:N \l__audio_tmpc_tl
\tl_new:N \l__audio_tmpd_tl

\fp_new:N \l__audio_tmpa_fp 
\fp_new:N \l__audio_tmpb_fp 
\fp_new:N \l__audio_tmpc_fp 
\fp_new:N \l__audio_tmpd_fp 

\dim_new:N \l__audio_tmpa_dim 
\dim_new:N \l__audio_tmpb_dim 
\dim_new:N \l__audio_tmpc_dim 
\dim_new:N \l__audio_tmpd_dim 


\RequirePackage{tikz}

\cs_new:Npn \__audio_pre_icon:n #1 {
    \tikz[baseline, #1] {
        \draw[line~width = .2ex, color = white] (0, 0) --++ (0, 1ex);
        \fill[color = white] (0.1ex, .5ex) -- (1ex, 1ex) -- (1ex, 0) -- cycle;
    }
}

\cs_new:Npn \__audio_post_icon:n #1 {
    \tikz[baseline, xscale=-1, #1] {
        \draw[line~width = .15ex, color = white] (0, 0) --++ (0, 1ex);
        \fill[color = white] (0.1ex, .5ex) -- (1ex, 1ex) -- (1ex, 0) -- cycle;
    }    
}

\cs_new:Npn \__audio_stop_icon:n #1 {
    \tikz[baseline, #1] {
        \draw[line~width = .3ex, color = white] (0, 0) --++ (0, 1ex);
        \draw[line~width = .3ex, color = white] (.5ex, 0) --++ (0, 1ex);
    }    
}

\cs_new:Npn \__audio_play_icon:n #1 {
    \tikz[baseline, #1] {
        \fill[color = white] (0, 0) -- (0, 1ex) -- (1ex, 0.5ex) -- cycle;
    }    
}

\cs_new:Npn \__audio_eval_time:Nn #1#2 {
    \exp_args:NNx \seq_set_split:Nnn \l__audio_tmpa_seq {\c_colon_str} {#2}
    \fp_set:Nn #1 {
        \fp_eval:n{(\seq_item:Nn \l__audio_tmpa_seq{1}) * 60 + \seq_item:Nn  \l__audio_tmpa_seq{2}}
    }
}

\cs_generate_variant:Nn \__audio_eval_time:Nn {NV}

\cs_new:Npn \__audio_show_progress_line:NNNx #1#2#3#4 {
    \group_begin:
    \__audio_eval_time:NV \l__audio_tmpa_fp #1
    \__audio_eval_time:NV \l__audio_tmpb_fp #2
    \fp_set:Nn \l__audio_tmpc_fp { \fp_eval:n { \l__audio_tmpa_fp / \l__audio_tmpb_fp } }
    \exp_args:NNV \dim_set:Nn \l__audio_tmpa_dim #3
    \dim_set:Nn \l__audio_tmpb_dim {\fp_to_dim:n { \l__audio_tmpc_fp *  \l__audio_tmpa_dim }}

    \tikz[baseline] {
        \draw[white, line~width = .2ex, opacity = .5] (0, 0) -- ++ (\l__audio_tmpa_dim, 0);
        \draw[white, line~width = .2ex] (0, 0) -- ++ (\l__audio_tmpb_dim, 0);
        \fill[white] (\l__audio_tmpb_dim, 0) circle (4pt); 
        \node[anchor = south~west, text = white, inner~xsep = 3pt, inner~ysep = 7pt] at (0, 0) {#4};
        \node[anchor = south~east, text = white, inner~xsep = 3pt, inner~ysep = 7pt] at (\l__audio_tmpa_dim, 0) {#1/#2};
    }
    \group_end:
}

\cs_generate_variant:Nn \__audio_show_progress_line:nnnx {VVVx}

\cs_new:Npn \__audio_words_parser:N #1 {
    \group_begin:
    \exp_args:NNV \ior_open:Nn \l__audio_words_ior #1

    % 计算上下方最多显示行数
    \int_set:Nn \l__audio_tmpc_int {\int_eval:n {\l__audio_max_show_lines_int - 1} / 2}

    \ior_map_inline:Nn \l__audio_words_ior {
        \int_incr:N \l__audio_file_line_number_int
        \int_compare:nT 
        {\int_abs:n {\l__audio_file_line_number_int - \l__audio_current_line_int} < \l__audio_tmpc_int} 
        {
            \int_compare:nTF { \l__audio_file_line_number_int  = \l__audio_current_line_int } 
            {
                \tl_set:NV \l__audio_tmpa_tl \l__audio_current_line_style_tl
            }
            {
                \tl_set:NV \l__audio_tmpa_tl \l__audio_words_style_tl
            }
    
            \__audio_eval_shift_dim:NNN \l__audio_file_line_number_int \l__audio_current_line_int \l__audio_tmpa_dim
            \exp_args:NNx \tl_put_right:Nn \l__audio_tmpb_tl 
            {
                \exp_not:N \node[] at ([shift = {(0, \dim_use:N \l__audio_tmpa_dim)}]current~page.center) {\exp_not:o{\l__audio_tmpa_tl} ##1};
            }
        }

    }
    \tl_use:N \l__audio_tmpb_tl
    \group_end:
}

% #1 current line, #2 line number #3 shift dim
\cs_new:Npn \__audio_eval_shift_dim:NNN #1#2#3 {
    % l__audio_words_line_skip_dim
    \int_set:Nn \l__audio_tmpa_int {#2 - #1}
    \dim_set:Nn #3 
    {
        \fp_to_dim:n { (\l__audio_tmpa_int) * (\l__audio_words_line_skip_dim) }
    }
}

\keys_define:nn {audio} {
    name.tl_set:N = \l__audio_name_tl,
    author.tl_set:N = \l__audio_author_tl,
    avatar.tl_set:N = \l__audio_avatar_image_tl,
    background~image.tl_set:N = \l__audio_background_picture_tl,
    background~color.tl_set:N = \l__audio_background_color_tl,
    play.bool_set:N = \l__audio_play_bool,
    play.default:n = true,
    progress.meta:nn = {audio/progress}{#1},
    words.meta:nn = {audio/words}{#1},

}

\keys_define:nn {audio/words} {
    line~skip.dim_set:N = \l__audio_words_line_skip_dim,
    current~line~number.int_set:N = \l__audio_current_line_int,
    source.tl_set:N = \l__audio_words_filename_tl,
    style.tl_set:N = \l__audio_words_style_tl,
    current~line~style.tl_set:N = \l__audio_current_line_style_tl,
    max~lines.tl_set:N = \l__audio_max_show_lines_int
}

\keys_define:nn {audio/progress} {
    current~time.tl_set:N = \l__audio_current_time_tl,
    total~time.tl_set:N = \l__audio_total_time_tl,
    length.dim_set:N = \l__audio_progress_line_length_dim
}

\cs_new:Npn \audio_show:n #1 {
    \group_begin:
    \keys_set:nn {audio} {#1}
    \titlepage 
        \begin{tikzpicture}[remember~picture, overlay]
        \node[inner~sep = 0pt, outer~sep = 0pt, anchor = north~west, opacity = .2] at (current~page.north~west) {\includegraphics[width = \paperwidth]{\tl_use:N \l__audio_background_picture_tl}};
        \fill[color = \l__audio_background_color_tl, opacity = .5] (current~page.north~west) rectangle (current~page.south~east);

        \node[scale = 1.5, anchor = west, outer~sep = 0pt, ] at ([shift={(3, 3)}]current~page.south~west) {
            \__audio_pre_icon:n {scale = 1.5} \quad 
            \bool_if:NTF \l__audio_play_bool 
            {
                \__audio_stop_icon:n {scale = 1.5}
            }
            {
                \__audio_play_icon:n {scale = 1.5}
            }\quad
            \__audio_post_icon:n {scale = 1.5} 
        };

        \node[anchor = west, outer~sep = 0pt,] at ([shift={(6, 3)}]current~page.south~west) {
            \__audio_show_progress_line:NNNx
            \l__audio_current_time_tl
            \l__audio_total_time_tl
            \l__audio_progress_line_length_dim
            {\l__audio_name_tl ( \l__audio_author_tl )}
        };

        \__audio_words_parser:N \l__audio_words_filename_tl

        \clip ([shift = {(4, -4)}]current~page.north~west) circle (2cm);
        \node[anchor = north~west, opacity = .5, inner~sep = 0pt] at ([shift = {(2, -2)}]current~page.north~west) {\includegraphics[width = 4cm, height = 4cm]{\tl_use:N \l__audio_avatar_image_tl}};
        \end{tikzpicture}
    \endtitlepage
    \group_end:
}
\NewDocumentCommand{\audio}{}{\audio_show:n}
\NewDocumentCommand{\audiosetup}{m}{\keys_set:nn {audio} {#1}}
\ExplSyntaxOff
\definecolor{bgc}{RGB}{109, 118, 103}
% initial
\audiosetup {
    background image = {bgc.jpg},
    background color = {bgc},
    play,
    progress = {
        current time = 01:23,
        total time = {05:32},
        length = {22.7cm}
    },
    words = {
        line skip = {20pt},
        source = {words.txt},
        style = \color{white}\bfseries\small,
        current line style = \color{red}\bfseries\Large,
        max lines = 15
    }
}

使用

latex
\documentclass[11pt]{ctexart}

\usepackage[paperwidth = 32cm, paperheight = 18cm, margin = 5cm]{geometry}

\usepackage{audio}

\begin{document}

\audio
{
    name = {起风了},
    author = {买辣椒也用券},
    avatar = {avatar},
    words = {
        current line number = 9
    }
}

\audio
{
    name = {如果爱忘了},
    author = {戚薇},
    background image = {bgc1.jpg},
    background color = {bgc},
    avatar = {avatar1},
    play = false,
    progress = {
        current time = {03:23},
        total time = {04:32}
    },
    words = {
        line skip = {24pt},
        source = {wordscopy.txt},
        style = \color{white}\bfseries\small,
        current line style = \color{purple}\bfseries\Large,
        current line number = 9,
        max lines = 10
    }
}
\end{document}