PowerPoint: 挿入された音声の「開始自動＆アイコンを隠す」を一括設定

# 

音声付きスライドとともに生成できる動画版も、YouTube や Vimeo などに上げておけば、再生スピードを視聴側で変化させたり、柔軟に巻き戻しできるので、通信量の問題がないなら遠隔授業資料の選択肢として有力です。

スライドページごとの録音を使った場合，音声付きスライド (pptx) を作成して動画 (mp4) で書き出すには、以下の流れが最短と思われます。（動画だけ作るとします）

1. 各スライドで「挿入 > 録音 > オーディオの録音」を使って録音
2. 各スライドで、挿入されたオーディオアイコンを選択して以下を設定
  - 「再生 > 開始: 自動」を選択
  - 「再生 > スライドショーを実行中にサウンドのアイコンを隠す」にチェック
3. 「ファイル」> 「エクスポート」>「ビデオを作成」

このうち2は、各スライドで設定が必要なのでやや手間です。**この2のステップを自動化してみます。**
これにより、1の各スライドでの録音さえできれば、あとは自動化された2と、3のエクスポートとで、すぐに動画になるはず。（ポインターの動きを入れるならもう少しステップが必要ですが。）

[こちらの GitHub](https://hkawash.github.io/lecrec-tools/#hideaudio) で自動化ツールと使い方をまとめておきます。

（注意）通常のpptxの編集とは異なる方法のため、出力後のファイルが壊れる可能性もゼロではありません。また、元のファイルは上書きしませんが、念のためバックアップをとることをお勧めします。（ソースはMITライセンスとします。）

## 仕様

- 基本 Windows 用とし、バッチファイル ＋ PowerShellスクリプトを使う。（macOSでも PowerShellを入れると動くかもしれません。）
- 各ページへの録音が済んだpptx（複数可）を、バッチファイルにドラッグアンドドロップすると、各pptxの全スライドページに対し、「自動再生とアイコンを隠す設定」が一括でできる。
- 音声録音したあとに、オーディオの「再生」設定を変更していない（デフォルトのまま）と仮定する。

## 動画作成の流れ

[こちら](https://hkawash.github.io/lecrec-tools/#hideaudio) から `hideaudio.ps1` (PowerShell) と `hideaudio-dnd.bat` (バッチファイル) をダウンロードして、同じフォルダに置いておきます。

1. 録音
    1. 外付けマイクを接続し、自動音量調整（AGC: Auto Gain Control）を切る。（ここは環境次第でいろいろありうる）
    2. 各スライドで「挿入 > 録音 > オーディオの録音」を使って録音していく。
2. 保存した pptxファイルを `hideaudio-dnd.bat` にドラッグアンドドロップする。
3. `ppt-out` に出力された pptxファイルを開き、「ファイル」> 「エクスポート」>「ビデオを作成」
    1. 「記録されたタイミングとナレーションを使用しない」のままとする。
    2. 「各スライドの所要時間」で音声無しのスライドの表示時間を設定する。
4. （オプション）[ノイズ除去](#noise-reduction)


ここでは1～3をできるだけ最短でまわして、動画 (mp4) を作成ことにフォーカスしますが、動画作成だけでなく、音声付きスライドを作成する際にも使えそうです。
実際、遠隔授業でも、動画と音声付きスライドを合わせて用意しておき、通信環境に合わせて選んでもらうこともあるかと思います。その場合、[音声付きスライドのサイズ圧縮に関する解説](https://qiita.com/memakura/items/d74baa9b06756d4acc54)も参考にしてください。
ただ、自動再生スライドも配布する場合、補足で述べるように、さらにあと少し設定が必要です。

## 補足1: エクスポートについて

ポインターの動きを入れない動画の場合、3のステップでは以下の設定で動画をエクスポートできます。

- 「記録されたタイミングとナレーションを使用しない」のまま  

ポインターの動きを入れる場合、および自動再生スライドとしても配布したい場合、3の手前で以下の設定が必要です。

- 「スライドショー」の設定
    - ポインタの動きなどをさらに記録するなら「ナレーションの再生」にチェック
    - 自動再生するような pptx/ppsx も配布するなら「タイミングを使用」にチェック
    - 「スライドショーの記録」>「先頭から記録」>「記録アイコン」
- 音声が自動再生されるので、授業の流れでスライドを進めながら記録

音声付き PowerPoint 作成は、[阪大の YouTube](https://youtu.be/2n-4YHGrrlY) が参考になります。

<a id="noise-reduction"></a>

## 補足2: ノイズ除去について

たとえば以下の方法：

1. Audacity でノイズ除去し m4a 形式で保存
    1. Audacityを入れ、[ffmpeg 拡張](https://lame.buanzo.org/#lamewindl)) しておく。
    2. Audacity で mp4 を開く
    3. [「エフェクト > ノイズ除去」でノイズ除去する](https://taira-komori.jpn.org/09noiseremoval.html)
    4. 「ファイル > Export > オーディオの書き出し」でm4a 形式で保存する。（仮に `audio-nr.m4a` とする）
2. ffmpeg で音声を差し替える
    1. ffmpeg を入れておく
    2. 元の動画から映像だけ抜き出す（仮に `video-only.mp4` とする）  
       `ffmpeg -i <元のmp4> -an -vcodec copy video-only.mp4`
    3. 抜き出した映像とノイズ除去した音声を結合する  
       `ffmpeg -i video-only.mp4 -i audio-nr.m4a -c copy <出力先mp4>`


# 挿入された音声の「自動再生＆アイコン隠す」を一括設定

## スライドのxmlを変換

pptxファイルはzipなので、これを PowerShell で `Expand-Archive` で展開し、中にある一枚一枚のスライドに対して処理していきます。一枚のスライドを処理する関数を `ChangeAudioSetting` としており、これを `for` ループで回して各スライドを変換しています。最後に。`Compress-Archive` で再びzipで束ね、拡張子pptxに変更します。結果は `ppt-out`フォルダに出力されるようにしておきます。

以下のスクリプトでは、後半にこの流れが書かれており、前半に `ChangeAudioSetting` 関数の記述があります。
この関数では、最初にみつかったオーディオに対して、以下の設定を変更しています。

1. 再生「開始: 一連のクリック動作」 -> 「開始: 自動」
    1. `<p:cond evt="onBegin" delay="0">` の追加
    2. `nodeType="clickEffect"` を `nodeType="afterEffect"` に変更
2. 「スライドショーを実行中にサウンドのアイコンを隠す」にチェック
    1. `<p:cMediaNode vol="80000">` を `<p:cMediaNode showWhenStopped="0" vol="80000">` に変更

ノードは、XPath を使って `SelectSingleNode` で探し、最初に見つかったものを利用します。
ただし、XMLの名前空間を設定する必要があります。`XmlNamespaceManager` で設定しておきます。
`p` と `a` を使うので、これをルートノードの `xmlns:p` や `xmlns:a` 属性から取得し `AddNamespace` しておきます。
`CreateElement` で要素を追加する際も、忘れずに `LookupNamespace` を入れておきます。これを忘れると、たとえば `<p:cond>` が `<cond>` になります。

XMLを保存するときは、タブや改行をなくした上で、BOMも出力しないようにします。ただ、元のPowerpointのxmlは、一行目のXMLの宣言のあとだけに改行があるようです。ここは再現していませんが、影響があるかどうか・・

```powershell:hideaudio.ps1
# 1つのスライドxmlの設定を変更する
function ChangeAudioSetting ($xmlfile)
{
    Write-Host -NoNewline "[$xmlfile] "
    $xmldoc = [xml](Get-Content -Encoding UTF8 $xmlfile)

    $nsMgr = New-Object System.Xml.XmlNamespaceManager $xmldoc.NameTable  
    $nsMgr.AddNamespace('p', $xmldoc.DocumentElement.GetAttribute('xmlns:p'))
    $nsMgr.AddNamespace('a', $xmldoc.DocumentElement.GetAttribute('xmlns:a'))

    # オーディオファイルはあるか?
    $checkaudio = $xmldoc.SelectSingleNode('//a:audioFile', $nsMgr)
    if ($checkaudio -eq $null){
        Write-Host 'This slide has no audio'
        return 1
    }

    # 始めに見つかったオーディオを対象にする
    $node1 = $xmldoc.SelectSingleNode('/p:sld/p:timing/p:tnLst/p:par/p:cTn[@nodeType="tmRoot"]/p:childTnLst/p:seq/p:cTn[@nodeType="mainSeq"]/p:childTnLst/p:par/p:cTn', $nsMgr)
    if ($node1 -eq $null){  # 「開始: クリック時」になっていれば対象外
        Write-Host 'This audio is not a default setting'
        return 1
    }

    # --- 再生「開始: 一連のクリック動作」 -> 「開始: 自動」 ---
    $childnode1 = $node1.SelectSingleNode('p:stCondLst', $nsMgr)
    if ($childnode1 -eq $null){
        Write-Host 'Cannot find sdContLst'
        return 1
    }
    # cond 要素を追加
    $elm = $xmldoc.CreateElement('p:cond', $nsMgr.LookupNamespace('p'))  # namespace必要
    $elm.SetAttribute('evt', 'onBegin')
    $elm.SetAttribute('delay', '0')
    $elm.AppendChild($xmldoc.CreateElement('p:tn', $nsMgr.LookupNamespace('p'))).SetAttribute('val', '2')
    $childnode1.AppendChild($elm) | Out-Null

    $childnode2 = $node1.SelectSingleNode('p:childTnLst/p:par/p:cTn/p:childTnLst/p:par/p:cTn[@presetClass="mediacall" and @nodeType="clickEffect"]', $nsMgr)
    if ($childnode2 -eq $null){  # 既に「開始: 自動」なら対象外
        Write-Host 'Cannot find clickEffect'
        return 1
    }
    $childnode2.SetAttribute('nodeType', 'afterEffect')

    # --- スライドショーを実行中にサウンドのアイコンを隠す ---
    $node2 = $node1.SelectSingleNode('/p:sld/p:timing/p:tnLst/p:par/p:cTn[@nodeType="tmRoot"]/p:childTnLst/p:audio/p:cMediaNode', $nsMgr)
    if ($node2 -ne $null){
        $node2.SetAttribute('showWhenStopped', '0')
    }else{
        Write-Host 'Cannot find cMediaNode'
        return 1
    }

    $xmlSettings = New-Object System.Xml.XmlWriterSettings
    $xmlSettings.Indent = $true
    $xmlSettings.IndentChars = ""
    $xmlSettings.NewLineChars = ""
    $xmlSettings.Encoding = New-Object System.Text.UTF8Encoding($false)  # w/o BOM
    $xmlWriter = [System.XML.XmlWriter]::Create($xmlfile, $xmlSettings)
    $xmldoc.Save($xmlWriter)
    $xmlWriter.Dispose()  # Flush and Close

    Write-Host 'Setting changed!'
    return 0
}


#=====================================================================
# Main: ひとつのpptxファイルを処理する
#=====================================================================
if ($args.Length -ne 1) {  # pptx名を引数で指定
    Write-Host 'Need one argument'
    exit 1
}
$OUTPUT_DIR = 'ppt-out'
$WORK_DIR = '__work__'
$pptfile = $args[0]

New-Item -ItemType Directory -Force -Path $OUTPUT_DIR | Out-Null
New-Item -ItemType Directory -Force -Path $WORK_DIR | Out-Null

Write-Host $pptfile
if (-not (Test-Path $pptfile)){
    Write-Host "No such file: $pptfile"
    exit 1
}
$pptfname = [System.IO.Path]::GetFileName($pptfile)
$pptfbase = [System.IO.Path]::GetFileNameWithoutExtension($pptfile)
$zipfile = "$WORK_DIR\$pptfbase.zip"
$pptworkdir = "$WORK_DIR\$pptfbase"
# Write-Host $zipfile
Copy-Item -Path $pptfile -Destination $zipfile -Force

if (Test-Path $pptworkdir){
    Remove-Item $pptworkdir -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $pptworkdir | Out-Null

Expand-Archive -Path $zipfile -DestinationPath $pptworkdir

$slidedir = "$pptworkdir\ppt\slides"
$num_files = (Get-ChildItem -File $slidedir -Filter slide*.xml).Count
Write-Host "Converting $num_files slides"
for ($i=1; $i -le $num_files; $i++){
  $f = "$slidedir\slide$i.xml"
  ChangeAudioSetting $f | Out-Null
}

$newzipfile = "$OUTPUT_DIR\$pptfbase.zip"
$newpptfile = "$OUTPUT_DIR\$pptfname"
Compress-Archive -Path $pptworkdir\* -DestinationPath $newzipfile -Force
Move-Item $newzipfile $newpptfile -Force
Remove-Item $zipfile
Remove-Item -Recurse $pptworkdir
Write-Host "Output: $newpptfile"

exit 0
```

## ドラッグアンドドロップを使う場合

ドラッグアンドドロップするために、バッチファイルを追加しておきます。

ドラッグアンドドロップすると、カレントディレクトリがホームになってしまいます。

バッチファイルと同じフォルダにある PowerShell を実行するため、`cd /d %~dp0` でバッチファイルのあるフォルダをカレントディレクトリにしています。

```batch:hideaudio-dnd.bat
@echo off
setlocal enabledelayedexpansion

echo %0 begin
cd /d %~dp0
for %%f in (%*) do (
    powershell -NoProfile -ExecutionPolicy Unrestricted .\hideaudio.ps1 %%f
    if %ERRORLEVEL% neq 0 (
        echo Error occurred while processing [%%f]
        pause
        exit /b 1
    )  
)
echo done
```

## ダブルクリックを使う場合

ダブルクリックすると `ppt-in` フォルダに入った pptx を処理するようなバッチファイルを作成しておきます。

```batch:hideaudio-folder.bat
@echo off
setlocal enabledelayedexpansion

echo %0 begin
set INPUT_DIR=ppt-in

if not exist %INPUT_DIR% (
    echo Error: Folder [%INPUT_DIR%] does not exist
    pause
    exit /b 1
)

dir %INPUT_DIR%\*pptx
if %ERRORLEVEL% neq 0 (
    echo Error: No pptx/ppsx files in [%INPUT_DIR%] folder
    pause
    exit /b 1
)

for %%f in (%INPUT_DIR%\*.pptx) do (
    powershell -NoProfile -ExecutionPolicy Unrestricted .\hideaudio.ps1 %%f
    if %ERRORLEVEL% neq 0 (
        echo Error occurred while processing [%%f]
        pause
        exit /b 1
    )  
)
echo done
```

PowerShell は XML も編集しやすく結構便利ですね。（.NET のインタフェースそのままですし。）

ただし、改行コードはCRLFでないと正しく動作しないようです。LFで GitHub に上げてしまったときは、ダウンロード後に、XMLの要素がうまく挿入されなくなりました。（チェックアウトするなら .gitattributeが効くと思いますが、直接ダウンロードは要注意・・）