複数のスライドのプロジェクタープレゼンテーションにサウンドを含める

複数のスライドのプロジェクタープレゼンテーションにサウンドを含める

Beamer プレゼンテーションにサウンドを含め、そのサウンドを特定のスライドで開始し、複数のオーバーレイまたは複数のスライドにわたって継続させたいと考えています。media9またはを使用してこれを実現しようとしましたmultimedia(ドキュメントによると、どちらも機能するはずです)。しかし、どちらの場合も (以下の MWE を参照)、サウンドは開始する必要があるときに開始されますが、項目 2 が表示されると停止します。サウンドを複数のスライドにわたって継続させるにはどうすればよいですか (パッケージの 1 つmedia9またはmultimedia、どちらでもかまいません)?

MWE 対象media9:

\documentclass{beamer}
\usepackage{media9}
\begin{document}
\begin{frame}{1}
1
\end{frame}

\begin{frame}{2}
\includemedia[label=my_sound, width=1ex, height=1ex, transparent, activate=pageopen, deactivate=onclick, addresource=./Sounds/MITM.mp3,
flashvars={
source=./Sounds/MITM.mp3
&autoPlay=true
},]{}{APlayer.swf}
\begin{itemize}
\item<2-> 2
\item<3-> 3
\end{itemize}
\end{frame}

\begin{frame}{3}
3
\end{frame}

\begin{frame}{4}
4
\end{frame}
\end{document}

MWE の場合multimedia: (ここでもautostartオプションは機能せず、サウンドを手動で開始する必要があり、オプションがないとサウンドはまったく出ませんinlinesound)

\documentclass{beamer}
\usepackage{multimedia}
\begin{document}
\begin{frame}{1}
1
\end{frame}

\begin{frame}{2}
\sound[autostart, inlinesound, bitspersample=16, channels=2, encoding=Signed, samplingrate=48000]{A}{./Sounds/MITM.au}
\begin{itemize}
\item<2-> 2
\item<3-> 3
\end{itemize}
\end{frame}

\begin{frame}{3}
3
\end{frame}

\begin{frame}{4}
4
\end{frame}
\end{document}

これが重要なら、私は Windows 11 で Adob​​e Reader (最新バージョン) を使用します。

答え1

私の知る限り、現在、一般的に使用されている PDF ビューアで複数のスライドにまたがるサウンドの再生をサポートしているものはありません。ただし、これをサポートできる独自のビューアを作成するのはかなり簡単です。

viewer.html次の内容を(または<anything>.html必要に応じて)という名前のファイルに保存します。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="
            width=device-width,
            initial-scale=1,
            maximum-scale=1
        ">
        <style>
            /* Make the viewer take up the full screen */
            * {
                margin: 0;
                padding: 0;
                border: 0;
                outline: 0;
            }

            #viewer-container {
                overflow: clip;
                position: absolute;
                width: 100%;
                height: 100%;
            }

            /* Center the page */
            .page {
                margin: 0 auto 0;
            }
        </style>

        <script type="module">
            import "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.1.392/pdf.mjs"
            import "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.1.392/pdf_viewer.mjs"
            pdfjsLib.GlobalWorkerOptions.workerSrc =
            "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.1.392/pdf.worker.mjs"

            const bus = new pdfjsViewer.EventBus()
            const container = document.getElementById("viewer-container")

            const viewer = new pdfjsViewer.PDFSinglePageViewer({
                container,
                eventBus: bus,
                textLayerMode: 0,
                annotationMode: 0,
            })

            bus.on("pagechanging", () => {
                viewer.currentScaleValue = "page-fit"
            })

            const file_element = document.getElementById("file")

            async function open(event) {
                let file
                if (event instanceof window.DragEvent) {
                    event.dataTransfer.dropEffect = "copy"
                    file = event.dataTransfer.files[0]
                } else if (event.target instanceof window.HTMLInputElement) {
                    file = event.target.files[0]
                }
                file_element.remove()

                const doc = await pdfjsLib.getDocument(
                    await file.arrayBuffer()
                ).promise

                viewer.setDocument(doc)

                const attatchments = await doc.getAttachments()

                let audio

                async function sound() {
                    viewer.currentScaleValue = "page-fit"

                    const page = await doc.getPage(viewer.currentPageNumber)
                    const annotations = await page.getAnnotations()

                    for (const annotation of annotations) {
                        const directive = annotation?.contentsObj?.str
                        if (!directive) continue

                        const [cmd, file] = directive.split(" ")

                        if (["play", "stop"].includes(cmd) && audio) {
                            audio.pause()
                            audio = null
                        }

                        if (cmd === "play") {
                            const blob = new Blob([attatchments[file].content])
                            audio = new Audio(URL.createObjectURL(blob))
                            audio.play()
                        }
                    }
                }

                function move(direction) {
                    if (!document.fullscreenElement) {
                        document.documentElement.requestFullscreen()
                    } else {
                        if (direction === "next") {
                            viewer.nextPage()
                        } else if (direction === "previous") {
                            viewer.previousPage()
                        }
                    }
                    sound()
                }

                document.addEventListener("keydown", (event) => {
                    switch (event.key) {
                        case "ArrowRight":
                        case "ArrowDown":
                        case " ":
                        case "PageDown":
                        case "Enter":
                            event.preventDefault()
                            move("next")
                            break
                        case "ArrowLeft":
                        case "ArrowUp":
                        case "PageUp":
                        case "Backspace":
                            event.preventDefault()
                            move("previous")
                            break
                    }
                })

                document.addEventListener("click", (event) => {
                    event.preventDefault()
                    switch (event.button) {
                        case 0:
                            event.preventDefault()
                            move("next")
                            break
                        case 2:
                            event.preventDefault()
                            move("previous")
                            break
                    }
                })
            }

            file_element.addEventListener("change", open)
            document.addEventListener("drop", open)
            document.addEventListener("dragover", event => event.preventDefault())
        </script>
    </head>
    <body>
        <input type="file" id="file" accept="application/pdf">
        <div id="viewer-container">
            <div id="viewer"></div>
        </div>
    </body>
</html>

次に、pdfLaTeX または LuaLaTeX を使用して次のドキュメントをコンパイルします。

\documentclass{beamer}


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Load required packages %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Needed to embed the audio files
\usepackage{embedfile}

%% Needed for \pdfannot to work with LuaTeX
\ifdefined\directlua
    \usepackage{luatex85}
\fi

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Variable Declarations %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\ExplSyntaxOn
    \seq_new:N \g__example_files_seq %% Store all used files
    \tl_new:N \l__example_lastfile_tl %% The most recent file loaded
    \tl_new:N \g__example_currentfile_tl %% The currently playing file


%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Command Definitions %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%

    %% Embeds a file if it isn't already embedded, and stores its attachment
    %% name in "\l__example_lastfile_tl".
    \cs_new_protected:Nn \__example_add_file:n {
        \tl_set:Ne \l__example_lastfile_tl { \str_mdfive_hash:n { #1 } }

        \seq_if_in:NVF \g__example_files_seq \l__example_lastfile_tl {
            \seq_gput_right:NV \g__example_files_seq \l__example_lastfile_tl
            \embedfile [ filespec = \l__example_lastfile_tl ] { #1 }
        }
    }

    %% Writes to the PDF the given command and parameter.
    \cs_new_protected:Nn \__example_pdfcmd:nn {
        \pdfannot width 0pt~ height 0pt~ depth 0pt~ {%
            /Subtype~ /Text
            /Contents~ (#1~ #2)
            /F~ 1
        }
    }
    \cs_generate_variant:Nn \__example_pdfcmd:nn { nV }

    %% Plays the given audio file.
    \cs_new_protected:Nn \__example_play:n {
        \__example_add_file:n { #1 }
        \tl_if_eq:NNF \g__example_currentfile_tl \l__example_lastfile_tl {
            \tl_gset:NV \g__example_currentfile_tl \l__example_lastfile_tl
            \__example_pdfcmd:nV { play } \l__example_lastfile_tl
        }
    }

    %% Stops the currently playing audio file.
    \cs_new_protected:Nn \__example_stop: {
        \__example_pdfcmd:nn { stop } { }
        \tl_gclear:N \g__example_currentfile_tl
    }

    %% Expose the commands publicly
    \cs_set_eq:NN \playaudio \__example_play:n
    \cs_set_eq:NN \stopaudio \__example_stop:
\ExplSyntaxOff


%%%%%%%%%%%%%%%%%%%%%
%%% Demonstration %%%
%%%%%%%%%%%%%%%%%%%%%

%% There are no audio files in $TEXMFDIST, so we need to lookup the
%% (system-dependent) path to a sample audio file.
\ExplSyntaxOn
    \sys_get_shell:nnN { kpsewhich~ --format=source~ bird.mp3 } { } \l_tmpa_tl
    \iow_term:e {
        \iow_newline:
        ``bird.mp3''~ path:~ \tl_to_str:N \l_tmpa_tl
        \iow_newline:
    }
\ExplSyntaxOff

\begin{document}
    \begin{frame}{Title One}
        Body One
    \end{frame}

    \begin{frame}{Title Two}
        %% Replace this path with the path output to the terminal earlier
        \playaudio{/usr/local/texlive/2024/texmf-dist/source/latex/media9/files/bird.mp3}

        Body Two A
        \begin{itemize}
            \item<2-> Body Two B
            \item<3-> Body Two C
        \end{itemize}
    \end{frame}

    \begin{frame}{Title Three}
        \stopaudio
        Body Three
    \end{frame}

    \begin{frame}{Title Four}
        Body Four
    \end{frame}
\end{document}

最後に、Web ブラウザーで開き、「参照」viewer.htmlをクリックして、上記でコンパイルした PDF ファイルを開きます。任意の場所をクリックするとプレゼンテーションが全画面表示になり、[ Left Click、、、、] のいずれかをクリックすると 1 スライド進み、[、、、] のいずれかをクリックすると1スライド戻ります。Page ↓SpaceEnterPage ↑Backspace

使い方

TeX 側では、サウンド ファイルを PDF の「添付ファイル」として追加し、非表示の PDF「注釈」内に再生/停止コマンドを配置します。

HTML 側では、ビューアのベースとして使用する pdf.js をインポートし、各ページの注釈を解析して、添付されたオーディオ ファイルを再生/停止するタイミングを決定するように構成します。

視聴者はとても必要最低限​​の機能しかなく、それほど堅牢ではありませんが、基本的なスライドショーには十分でしょう。新しい機能を追加して堅牢性を高めることは、読者の課題として残しておきます :)

答え2

これは答えではありませんが、問題に対する私の解決策です。私は基本的に説明されている問題に直面しており、かなり試行錯誤した結果、現時点では PDF 表示の範囲外であるという結論に達しました。

そのため、私は Beamer ベースの PDF プレゼンテーションから、reveal.js などのフレームワークを使用した Web ベースのプレゼンテーションに切り替えました。これには、一般的な js ベースの LaTeX レンダリング エンジンが LaTeX のサブセットしか提供しないという欠点があり、そのためスライドの作成時間が長くなっています。

新しいアプローチの主な欠点は、より手間がかかることです。つまり、KaTex や同様の LaTex ベースの Web レンダラーでサポートされていない特定の LaTex レンダリングが必要な場合は、まず標準の LaTex を PNG ファイルにレンダリングし、必要なスライドに画像として埋め込みます。大きな利点は、オーディオ コンテンツやビデオ コンテンツの追加が容易になり、インタラクティブ コンテンツ (JavaScript プログラムの形式) の追加が非常に実行可能でありながら特注の作業になることです。私の作業にはアルゴリズムの実現が含まれることが多いため、プレゼンテーション内で作業をシームレスに表示できるのは便利です (Alt キーを押しながらタブを移動する必要はありません)。Reveal.js には、アニメーションやインタラクティブ コンテンツの上にマークアップやスケッチを追加できるライブラリがあります。このアプローチの主な欠点が時間の経過とともに克服され、すべての面で最良の選択肢が残されることを願っています。

関連情報