TEMPO: play a sound in animated GIF/PDF/SVG
Click on the image below to see the ticking metronome (SVG version) in action:
This animate
-based example comes in two versions.
The PDF version requires Acrobat Reader and is thus restricted to Windows and OSX platforms for desktop PCs. (Mobile devices are not supported.)
The PDF version uses media9
for playing the embedded sound file click.mp3
, taken from the animate
package which already provides a ticking metronome example in its manual.
The standalone SVG version runs in most contemporary Web browsers (Firefox, Chrome, Opera ...), even on mobile devices. Here, the sound is embedded as a Base64-encoded Blob using the HTML5 <audio>
tag. (click.mp3
was encoded with "base64 click.mp3
" on the command line.) The physically embedded sound file makes the SVG self-contained.
In either case, playback of the sound is started by JavaScript on every frame change of the running animation. The "timeline" feature of animate
is used to associate the JS code with each frame (timeline input frames.txt
).
The tempo marker is implemented with a second animateinline
environment. It provides 39 typical metronome speeds that can be stepped trough with the +/- buttons. The buttons make use of the animate
JavaScript interface to increment and decrement the tempo marker frame number. Another timeline file, tempo.txt
, associates each marker frame with a JS snippet that scales the metronome speed accordingly, again using the JS interface of animate
.
PDF version, to be compiled with pdflatex
, lualatex
, latex+dvips+ps2pdf
, or xelatex
:
\documentclass[varwidth,border=12pt]{standalone}
\usepackage{tikz}
\usepackage{animate}
\usepackage{media9}
\usepackage{fontawesome5}
% tempo markers
\def\tempos{{40,42,44,46,48,50,52,54,56,58,60,63,66,69,72,76,80,84,88,92,96,100,104,108,112,116,120,126,132,138,144,152,160,168,176,184,192,200,208}}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% timeline files
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% metronome
\usepackage{filecontents}
\begin{filecontents*}{frames.txt}
::0: annotRM['click'].callAS('play');
::1: annotRM['click'].callAS('play');
::2: annotRM['click'].callAS('play');
::3: annotRM['click'].callAS('play');
\end{filecontents*}
% tempo
\newwrite\TimeLineFile
\immediate\openout\TimeLineFile=tempo.txt
\foreach \i in {0,1,...,38} {
%JavaScript for scaling the metronome's default speed (60 frames per min)
\pgfmathparse{\tempos[\i]/60}
\immediate\write\TimeLineFile{::\i:anim.metronome.speed=\pgfmathresult;}
}
\immediate\closeout\TimeLineFile
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\setlength{\textwidth}{4.4cm}
\begin{document}
\begin{center}
%the sound player
\makebox[0pt][r]{\includemedia[
width=1ex,height=1ex,
label=click,
addresource=click.mp3,
activate=pageopen,transparent,noplaybutton,
flashvars={source=click.mp3&hideBar=true}
]{}{APlayer.swf}}%
\makebox[4cm][l]{The \LaTeX{} Metronome}\\[1ex]
%%%%%%%%%%%
% metronome
%%%%%%%%%%%
\begin{animateinline}[label=metronome,nomouse,loop,timeline=frames.txt]{1}
\multiframe{4}{n=1+1}{
\begin{tikzpicture}
\path (0.3,-0.5) rectangle (4.7,0.5);
\shade[inner color=green,outer color=green!10] (\n,0) circle(.6);
\foreach \i in {1,...,4} \fill[cyan] (\i,0) circle(.3);
\end{tikzpicture}
}
\end{animateinline}
%%%%%%%%%%%
% controls
%%%%%%%%%%%
\makebox[4cm]{%
\mediabutton[
jsaction={
if(anim.metronome.isPlaying) anim.metronome.stopFirst();
else anim.metronome.playFwd();
}
]{\faPowerOff}\hfill
% tempo
\mediabutton[
jsaction={ try {--anim.tempo.frameNum;} catch (e) {} }
]{\faMinusCircle}\,%
\begin{animateinline}[label=tempo,step,nomouse,poster=10,timeline=tempo.txt]{1}
\multiframe{39}{i=0+1}{
\pgfmathparse{\tempos[\i]}
\makebox[\widthof{000}][c]{\strut\pgfmathresult}
}
\end{animateinline}\,%
\mediabutton[
jsaction={ try {++anim.tempo.frameNum;} catch (e) {} }
]{\faPlusCircle}}
\end{center}
\end{document}
SVG version, to be compiled with
latex tempoForWeb.tex
latex tempoForWeb.tex
dvisvgm --font-format=woff --exact --zoom=-1 tempoForWeb.dvi
The SVG can be viewed standalone in a Web browser, or embedded into a Web page by means of the <object>
HTML tag:
\documentclass[dvisvgm,varwidth,preview]{standalone}
\usepackage{tikz}
\usepackage{animate}
\usepackage{fontawesome5}
% tempo markers
\def\tempos{{40,42,44,46,48,50,52,54,56,58,60,63,66,69,72,76,80,84,88,92,96,100,104,108,112,116,120,126,132,138,144,152,160,168,176,184,192,200,208}}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% timeline files
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% metronome
\usepackage{filecontents}
\begin{filecontents*}{frames.txt}
::0:$('click').play();
::1:$('click').play();
::2:$('click').play();
::3:$('click').play();
\end{filecontents*}
% tempo
\newwrite\TimeLineFile
\immediate\openout\TimeLineFile=tempo.txt
\foreach \i in {0,1,...,38} {
%JavaScript for scaling the metronome's default speed (60 frames per min)
\pgfmathparse{\tempos[\i]/60}
\immediate\write\TimeLineFile{::\i:anim.metronome.speed=\pgfmathresult;}
}
\immediate\closeout\TimeLineFile
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% for producing push buttons
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\ExplSyntaxOn
\def\controlButton#1#2{% #1:button face, #2 JavaScript
\sbox{0}{#1}
\makebox[0pt][l]{\usebox{0}}
\pbs_pdfannot:nnnn{\wd0}{\ht0}{\dp0}{cursor='pointer'~onmousedown='#2'}
\phantom{\usebox{0}}
}
\ExplSyntaxOff
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\setlength{\textwidth}{4.4cm}
\begin{document}
% embed sound
\special{dvisvgm:rawdef
<audio xmlns="http://www.w3.org/1999/xhtml" id='click'>
<source
src="data:audio/mpeg;base64,
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
qqqqqqqqqqqq" type="audio/mpeg"/>
% <source src="click.mp3" type="audio/mpeg"/> % alternatively: separate mp3
</audio>
<script type="text/javascript">
<![CDATA[
function $(id) {return document.getElementById(id);};
]]>
</script>
}
\begin{center}
\makebox[4cm][l]{The \LaTeX{} Metronome}\\[1ex]
%%%%%%%%%%%
% metronome
%%%%%%%%%%%
\begin{animateinline}[label=metronome,nomouse,loop,timeline=frames.txt]{1}
\multiframe{4}{n=1+1}{
\begin{tikzpicture}
\path (0.3,-0.5) rectangle (4.7,0.5);
\shade[inner color=green,outer color=green!10] (\n,0) circle(.6);
\foreach \i in {1,...,4} \fill[cyan] (\i,0) circle(.3);
\end{tikzpicture}
}
\end{animateinline}
%%%%%%%%%%%
% controls
%%%%%%%%%%%
\makebox[4cm]{%
\controlButton{\faPowerOff}{%
if(anim.metronome.isPlaying) anim.metronome.stopFirst();
else anim.metronome.playFwd();
}\hfill
% tempo
\controlButton{\faMinusCircle}{%
try {--anim.tempo.frameNum;} catch (e) {}
}\,%
\begin{animateinline}[label=tempo,step,nomouse,poster=10,timeline=tempo.txt]{1}
\multiframe{39}{i=0+1}{
\pgfmathparse{\tempos[\i]}
\makebox[\widthof{000}][c]{\strut\pgfmathresult}
}
\end{animateinline}\,%
\controlButton{\faPlusCircle}{%
try {++anim.tempo.frameNum;} catch (e) {}
}}
\end{center}
\end{document}
Standalone, animated GIF is dumb and deaf, afaik, and there does not seem to exist an easily accessible method to synchronize an embedded Gif (in a Web page) with a sound. But SVG is much superior, since it is vectorial and thus freely scalable.