Vegas ProのC#スクリプトでファイル名からテロップを作成する

# 概要
Vegas Pro の C# スクリプトを使用して、画像や動画のファイル名からテロップを作成する方法を説明します。
画像や動画のファイル名が「YYYY-MM-DDThhmmss_コメント.拡張子」（例：「2021-09-23T135100_おでかけしたよ.jpg」の画像ファイルであれば、撮影時刻は「2021年9月23日13時51分00秒」、コメントは「おでかけしたよ」。）の形式になっていれば、テロップとして「2021年9月23日 おでかけしたよ」が入ります。



ビルドした DLL はこちら（Win）：
https://github.com/miworky/Vegas_AddTextEventFromFilename/tree/main/bin

使用方法：
https://github.com/miworky/Vegas_AddTextEventFromFilename/blob/main/README.md

スクリプトを使いたいだけの人はここでおさらばです。

コードはこちら（C#）：
https://github.com/miworky/Vegas_AddTextEventFromFilename

コードの詳細を知りたい人は続けてお読みください。


# 背景
子供の写真や動画を共有するのに「みてね」を使っています。みてねから画像や動画をダウンロードして動画を作成したり、ブルーレイに焼いたりする際に、いくつか課題があります。 課題の一つは、画像や動画のテロップを作成するのが大変だということです。 テロップは画像や動画 1 つ 1 つに対して 1 つずつ手動で追加する必要があるので、画像や動画の数が多いと非常に時間がかかります。

このスクリプトを使用すれば、この作業を自動化できます。

なお、みてねから画像や動画をダウンロードする際には以下を使います：

https://qiita.com/miworky/items/4d10824117527d115113


# 処理の概要
処理の概要は以下です：

1. ダイアログを表示して、ログを出力するファイルを選択させる
1. タイムライン上にある Video トラックの静止画・動画のファイル名を全部集める
1. 集めたファイル名からテキストイベントを生成する
1. 生成したテキストイベントをトラックに追加する

# 追加するテキストイベントの仕様

- 画像・動画の時刻より 1 秒遅らせてテキストイベントを追加する
- テキストイベントの長さは 5 秒とする
- テキストイベントには 1 秒のフェードインとフェードアウトをかける


# コードの説明

## 1. ダイアログを表示して、ログを出力するファイルを選択させる

### 1.1 エントリーポイント
Vegas Pro のスクリプトのエントリーポイントは以下です：

```javascript:Class1.cs
    public class EntryPoint
    {
        public void FromVegas(Vegas vegas)
        {
```

引数の vegas を使うことで必要な情報を取り出したり設定したりします。

### 1.2 Project にトラックがなければ抜ける
Project にトラックがなければ抜けることにします：

```javascript:Class1.cs
            if (vegas.Project.Tracks.Count == 0)
            {
                // トラックがないときは何もしない
                return;
            }
```

### 1.3 ダイアログを表示
次に、ダイアログを表示して、ログを出力するファイルを選択させます：

```javascript:Class1.cs
            // ダイアログを開き出力するログのファイルパスをユーザーに選択させる
            string saveFilePath = GetFilePath(vegas.Project.FilePath, "AddTextEventFromFilename");
            if (saveFilePath.Length == 0)
            {
                return;
            }
```
GetFilePathは以下のようになっています。関数にすることで、別のスクリプトでも同じ処理をしやすくなります。

```javascript:Class1.cs
        // ダイアログを開きファイルパスをユーザーに選択させる
        private string GetFilePath(string rootFilePath, string preFix)
        {
            SaveFileDialog sfd = new SaveFileDialog();
            sfd.FileName = preFix + System.IO.Path.GetFileNameWithoutExtension(rootFilePath) + ".txt";
            sfd.InitialDirectory = System.IO.Path.GetDirectoryName(rootFilePath) + "\\";
            sfd.Filter = "テキストファイル(*.txt)|*.txt";
            if (sfd.ShowDialog() != DialogResult.OK)
            {
                return "";
            }

            return sfd.FileName;
        }
```

## 2. タイムライン上にある Video トラックの静止画・動画のファイル名を全部集める

### 2.1 Project から trackEvent を 1 つずつ取得する
全トラックは、vegas.Project.Tracks で取得できます。これを foreach で回せば 1 トラックずつ処理できます。さらに、各トラックの中の全イベントは track.Events で取得できます。これを foreach で回せば、 1 イベントずつ処理できます。コードにすると以下です：

```javascript:Class1.cs
            foreach (Track track in vegas.Project.Tracks)
            {
                foreach (TrackEvent trackEvent in track.Events)
                {
```

### 2.2 オーディオは無視する
トラックに動画を張り付けた場合、ビデオとオーディオの両方の TrackEvent がタイムラインに貼り付けられます。つまり、上記の foreach の内側には 1 つの動画につき、2 回きます。テキストイベントは 1 つだけ生成したいので、ここではビデオのみを処理することにします。

```javascript:Class1.cs
                    if (!trackEvent.IsVideo())
                    {
                        // ビデオトラック以外は無視する
                        continue;
                    }
```

### 2.3 アクティブテイクのみ対象とする
1 つの trackEvent には複数のテイクが登録できます。ここでは、アクティブテイクのみを対象にします。

```javascript:Class1.cs
                    // アクティブテイクのみを対象にする
                    Take take = trackEvent.ActiveTake;
                    if (take == null)
                    {
                        // アクティブテイクがなければ無視
                        continue;
                    }
```

### 2.4 ファイルパスの取得
いよいよメディアのファイルパスを取得します。メディアのファイルパスは以下のように取得します：

```javascript:Class1.cs
                    string filepath = GetMediaFilePath(take.Media);
                    if (filepath.Length == 0)
                    {
                        // ファイルパスが正常に取れなければ無視
                        continue;
                    }
```
GetMediaFilePathの中身は以下のようになっています：

```javascript:Class1.cs
        private string GetMediaFilePath(Media media)
        {
            if (media == null)
            {
                return "";
            }

            string filepath = media.FilePath;
            if (filepath.Length == 0)
            {
                // ファイルパスが正常に取れなかった
                return "";
            }

            // テキストイベントにもテキストの内容がファイルパスに入っている
            // それはほしいものではないので（そこにファイルはないので）除外する
            if (IsTextEvent(media))
            {
                // テキストイベントは無視
                return "";
            }

            return filepath;
        }
```

#### テキストイベントは無視する
media.FilePath にファイルパスが入っているように見えるので、普通に考えればこれを参照すればファイルパスが取れそうです。が、Vegas Pro の謎の仕様により、ファイルパスを取り出すには、追加の処理が必要になります。謎の仕様というのは、trackEvent がテキストイベントだった場合は、そのテキストの内容がファイルパスに入っているという仕様です。このときファイルパスには「VEGAS タイトルおよびテキスト 」というプリフィックスがついています。たとえば、「おでかけしたよ」というテキストイベントのファイルパスは、「VEGAS タイトルおよびテキスト おでかけしたよ」となっています。なんじゃそりゃ。

追加の処理として、前述のプリフィックスから始まるファイルパスであればテキストイベントであるから無視する、としてもよいのですが、それだと将来の仕様変更に耐えられません。その上、英語版では動かない気もします。以上を考慮して、ここではほかの方法をとりたいと思います。

ここでやりたいのは、テキストイベントなら無視したいのです。テキストイベントにはテキストを持っている Effect が入っているので、その Effect が取れれば無視する、としたいと思います。

IsTextEvent は以下のように、media から Effect を取り出して、取り出した Effect から textParam を取り出します。textParam が取り出せたらこのメディアはテキストイベントです。

```javascript:Class1.cs
        // media がテキストイベントであれば true
        private bool IsTextEvent(Media media)
        {
            OFXEffect ofxEffect = GetOFXEffect(media);
            if (ofxEffect == null)
            {
                return false;
            }

            OFXStringParameter textParam = ofxEffect.FindParameterByName("Text") as OFXStringParameter;
            if (textParam == null)
            {
                // テキストイベントではない
                return false;
            }

            return true;
        }
```

:::note info
関数名ですが、IsTextEvent にするか HasTextEvent にするかで悩みました。実装上は、Media が Text を持っているかどうかを確認しているので、 HasTextEvent にしたいところです。しかし、やりたいのは、Media が TextEvent なのかどうかを確認することですので IsTextEvent がよさそうです。

Vegas Pro のすべての仕様を知っているわけではないのですが（むしろ知らない仕様のほうが多い）、もし、TextEvent 以外のイベントでも Text を持っていたら、この関数はうまく動かないと思われます。
:::



### 2.5 画像・動画の開始時刻とファイルパスをリストアップ

画像・動画の開始時刻とファイルパス以下のようにリストに追加します：

```javascript:Class1.cs
                    // ファイルパスが見つかった
                    // このファイルが張り付けられているフレーム位置とファイルパスのペアを追加する
                    fileInfos.Add(Tuple.Create(trackEvent.Start.FrameCount, filepath)); 
```

fileInfos は以下のように時刻とファイルパスのペアのリストとして生成します：

```javascript:Class1.cs
            List<Tuple<long, string>> fileInfos = new List<Tuple<long, string>>();
```

- リストに追加する要素が増えるかもしれないので Tuple を使う
- C++ なら std::vector を使うところですが、C# では List を使うようなので、List を使う


## 3. ファイル名からテキストイベントを作成する

### 3.1 Generator を生成する

テキストイベントを生成するには、テキストイベントを表すメディアを生成します。メディアを生成するには、 Generator が必要です。Generator は以下のように取得します：

```javascript:Class1.cs
            // Titles & Text の Generator を取得する
            PlugInNode generator = GetGeneratorTitlesAndText(vegas);
            if (generator == null)
            {
                MessageBox.Show("Cannot get Titles & Text generator");
                return;
            }
```

GetGeneratorTitlesAndText は以下のようにします：

```javascript:Class1.cs
        // Titles & Text のジェネレータを取得する
        private PlugInNode GetGeneratorTitlesAndText(Vegas vegas)
        {
            PlugInNode generator = vegas.Generators.GetChildByUniqueID("{Svfx:com.vegascreativesoftware:titlesandtext}"); //  Titles & Text
            return generator;
        }
```

UniqueID を使って取得することで、バージョンアップによる仕様変更の影響を避けることができるはず。

最初に Vegas Pro のスクリプトを書こうとしたときに、なぜこのように生成する必要があるのかわからなかったのですが、Vegas Pro の画面と対応付ければこうする必然性がわかってきます。 Vegas Pro の画面だと「メディアジェネレーター」タブの「Titles & Text」からテキストイベントを作成しますからね。


### 3.2 ファイル名からテキストイベントを生成する

リストアップした fileInfos を一つずつ処理します：

```javascript:Class1.cs
            foreach (var fileInfo in fileInfos)
            {
                string filename = fileInfo.Item2;
                Timecode timecodeStart = new Timecode();
                timecodeStart.FrameCount = fileInfo.Item1;
```

filename にはファイル名、timecodeStart にはそのファイルが張り付けられている開始時刻（単位：frame）が入ります。

次に、Media を生成します：

```javascript:Class1.cs
                // TextEvent を表す新しい Media を生成する
                Media media = Media.CreateInstance(vegas.Project, generator);
```

生成した media を望みのテキストイベントにするには Effect の設定を変更します。Effect は以下のように取得します：

```javascript:Class1.cs
                // Media の Effect を取得する
                OFXEffect ofxEffect = GetOFXEffect(media);
                if (ofxEffect == null)
                {
                    continue;
                }
```

<details><summary>GetOFXEffect は次のように実装します：</summary><div>

```javascript:Class1.cs
        private OFXEffect GetOFXEffect(Media media)
        {
            if (media == null)
            {
                return null;
            }

            Effect generator = media.Generator;
            if (generator == null)
            {
                return null;
            }

            OFXEffect ofxEffect = generator.OFXEffect;
            if (ofxEffect == null)
            {
                return null;
            }

            return ofxEffect;
        }
```
</div>
</details>

これから変更するのは以下です：

1. テキスト
1. テキストの表示位置
1. アウトラインの幅
1. アウトラインの色

コードとしては以下のようになります。今は自分の好みで決めうちで定数にしています。

```javascript:Class1.cs
                long err = 0;

                // テキストを変える
                {
                    string newText = ToComment(filename); // ファイル名からコメントに変換する
                    float fontSize = 14;
                    err = ChangeText(ofxEffect, newText, fontSize);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                // テキストの表示位置を変える
                {
                    double x = 0.5;
                    double y = 0.1;
                    err = ChangeLocation(ofxEffect, x, y);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                // アウトラインの幅を変える
                {
                    double outlineWidth = 10.0;
                    err = ChangeOutlineWidth(ofxEffect, outlineWidth);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                // アウトラインの色を変える
                {
                    double r = 0.0;
                    double g = 0.0;
                    double b = 0.0;
                    double a = 1.0;
                    err = ChangeOutlineColor(ofxEffect, r, g, b, a);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                ofxEffect.AllParametersChanged();
```

パラメータを変更したあとに、ofxEffect.AllParametersChanged(); で変更通知を発行しています。

#### 3.2.1 テキストを変更する

テキストを変更するには、OFXEffect からテキストを表す OFXStringParameter を取り出します。OFXStringParameter にはテキストがリッチテキストで格納されています。リッチテキストには生のテキストのほかにもフォントやフォントサイズなどの書式もセットで格納されています。

ここで変更したいのはテキストとフォントサイズです。RichTextBox のドキュメントを見たりしてごにょごにょといろいろ試してみたら以下のようにすればできました：

```javascript:Class1.cs
        // テキストとフォントサイズを変える
        private long ChangeText(OFXEffect ofxEffect, string newText, float fontSize)
        {
            OFXStringParameter textParam = ofxEffect.FindParameterByName("Text") as OFXStringParameter;
            if (textParam == null)
            {
                return -1;
            }

            {
                string rtfData = textParam.Value;   // デフォルトで入っているテキスト

                RichTextBox richtextBox = new RichTextBox();
                FontFamily fontFamily = richtextBox.SelectionFont.FontFamily;
                richtextBox.Rtf = rtfData;
                richtextBox.Text = newText;
                richtextBox.SelectAll();    // 全テキストが対象

                richtextBox.SelectionFont = new System.Drawing.Font(fontFamily, fontSize);    // フォント変更

                textParam.Value = richtextBox.Rtf;
            }

            return 0;
        }
```

#### 3.2.2 テキストの表示位置を変更する

テキストの表示位置を変更するには、OFXEffect からテキストの表示位置を表す OFXDouble2DParameter を取り出します。取り出した OFXDouble2DParameter の Value に x, y 座標を設定します：

```javascript:Class1.cs
        // テキストの表示位置を変える
        private long ChangeLocation(OFXEffect ofxEffect, double x, double y)
        {
            OFXDouble2DParameter locationParam = ofxEffect.FindParameterByName("Location") as OFXDouble2DParameter;
            if (locationParam == null)
            {
                return -1;
            }

            OFXDouble2D location;
            location.X = x;
            location.Y = y;

            locationParam.Value = location;

            return 0;
        }
```

#### 3.2.3 アウトラインの幅を変更する

アウトラインの幅を変更するには、OFXEffect からアウトラインの幅を表す OFXDoubleParameter を取り出します。取り出した OFXDoubleParameter の Value に width を設定します：

```javascript:Class1.cs
        // アウトラインの幅を変える
        private long ChangeOutlineWidth(OFXEffect ofxEffect, double width)
        {
            OFXDoubleParameter outlineWidthParam = ofxEffect.FindParameterByName("OutlineWidth") as OFXDoubleParameter;
            if (outlineWidthParam == null)
            {
                return -1;
            }

            outlineWidthParam.Value = width;

            return 0;
        }
```

#### 3.2.4 アウトラインの色を変更する

アウトラインの色を変更するには、OFXEffect からアウトラインの色を表す OFXRGBAParameter を取り出します。取り出した OFXRGBAParameter の Value に RGBA を表す OFXColor を設定します：

```javascript:Class1.cs
        // アウトラインの色を変える
        private long ChangeOutlineColor(OFXEffect ofxEffect, double r, double g, double b, double a)
        {
            OFXRGBAParameter outlineColorParam = ofxEffect.FindParameterByName("OutlineColor") as OFXRGBAParameter;
            if (outlineColorParam == null)
            {
                return -1;
            }

            outlineColorParam.Value = new OFXColor(r, g, b, a);
  
            return 0;
        }
```

#### 3.2.5 ほかのパラメータを変えるには

上記の例では OFXEffect から FindParameterByName を使って「名前」と「クラス名」を指定して個々のパラメータのインスタンスを取得していました。この「名前」と「クラス名」は以下のようなコードを書けば取得できます：

```javascript:Class1.cs
            {
                string names = "";
                Media media = Media.CreateInstance(vegas.Project, generator);

                OFXEffect ofxEffect = media.Generator.OFXEffect;
                foreach (var parameter in ofxEffect.Parameters)
                {

                    string thisName = parameter.Name + " " + parameter.GetType().Name;
                    names = names + thisName + "\n";
                }

                MessageBox.Show(names);
            }
```

- parameter.Name が FindParameterByName で指定する名前
- parameter.GetType().Name が クラス名

「名前」は Vegas Pro のビデオメディアジェネレータ画面の各パラメータに対応しているようです。クラス名がわかれば、公式のドキュメントを見ることで使い方が少しはわかります。Vegas Pro 19.0 build 381 では以下が取得できます：

|  名前      | クラス名     |
|:-----------|------------:|
|Text        |OFXStringParameter     |
|TextColor   |OFXRGBAParameter       |
|collection  |OFXChoiceParameter     |
|AnimationName |OFXStringParameter   |
|Scale         |OFXDoubleParameter   |
|Location      |OFXDouble2DParameter |
|Alignment     |OFXChoiceParameter   |
|AdvancedGroup |OFXParameter         |
|Background    |OFXRGBAParameter     |
|Tracking      |OFXDoubleParameter   |
|LineSpacing   |OFXDoubleParameter   |
|OutlineGroup  |OFXParameter         |
|OutlineWidth  |OFXDoubleParameter   |
|OutlineColor  |OFXRGBAParameter     |
|ShadowGroup   |OFXParameter         |
|ShadowEnable  |OFXBooleanParameter  |
|ShadowColor   |OFXRGBAParameter     |
|ShadowOffsetX |OFXDoubleParameter   |
|ShadowOffsetY |OFXDoubleParameter   |
|ShadowBlur    |OFXDoubleParameter   |
|Controls      |OFXParameter         |




## 4. テキストイベントをトラックに追加する

テキストイベントを追加するトラックは、決め打ちで先頭のトラックとします。

```javascript:Class1.cs
            VideoTrack textVideoTrack = vegas.Project.Tracks[0] as VideoTrack;    // テキストを追加するビデオトラック（先頭のトラックに追加する）
            if (textVideoTrack == null)
            {
                return;
            }
```

テキストイベントの仕様は以下のように決め打ちにします：

- 画像・動画の時刻より 1 秒遅らせてテキストイベントを追加する
- テキストイベントの長さは 5 秒とする
- テキストイベントには 1 秒のフェードインとフェードアウトをかける

コードでは以下のようになっています：

```javascript:Class1.cs
            Timecode textFadeLength = Timecode.FromString("00:00:01;00");   // 追加するテキストのフェード時間

            long textEventOffsetMs = 1000;  // 追加するテキストは、動画や静止画よりも少し遅らせて追加する。遅らせる時間を ms で指定する
            Timecode textLength = Timecode.FromString("00:00:05;00");  // テキストの表示時間。固定時間としているが、動画や静止画がこれより短いとテキストが正しく表示されないので、動画や静止画の長さをチェックする必要がある
```

テキストイベントをトラックに追加するには以下のようにします：

```javascript:Class1.cs
                Timecode textStartTimecode = timecodeStart + Timecode.FromMilliseconds(textEventOffsetMs);
                VideoEvent videoEvent = new VideoEvent(textStartTimecode, textLength);

                // テキストをトラックに追加する
                textVideoTrack.Events.Add(videoEvent);

                Take take = new Take(media.GetVideoStreamByIndex(0));
                videoEvent.Takes.Add(take);

                videoEvent.FadeIn.Length = textFadeLength;
                videoEvent.FadeOut.Length = textFadeLength;
```

# コード全体

コード全体は以下のようになります：

```javascript:Class1.cs
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using ScriptPortal.Vegas;

using System.Drawing;



namespace vegastest1
{

    public class EntryPoint
    {
        public void FromVegas(Vegas vegas)
        {
            if (vegas.Project.Tracks.Count == 0)
            {
                // トラックがないときは何もしない
                return;
            }

            // ダイアログを開き出力するログのファイルパスをユーザーに選択させる
            string saveFilePath = GetFilePath(vegas.Project.FilePath, "AddTextEventFromFilename");
            if (saveFilePath.Length == 0)
            {
                return;
            }

            // タイムライン上にある Video トラックの静止画・動画のファイル名を全部集める
            List<Tuple<long, string>> fileInfos = new List<Tuple<long, string>>();
            foreach (Track track in vegas.Project.Tracks)
            {
                foreach (TrackEvent trackEvent in track.Events)
                {
                    if (!trackEvent.IsVideo())
                    {
                        // ビデオトラック以外は無視する
                        continue;
                    }

                    // アクティブテイクのみを対象にする
                    Take take = trackEvent.ActiveTake;
                    if (take == null)
                    {
                        // アクティブテイクがなければ無視
                        continue;
                    }

                    string filepath = GetMediaFilePath(take.Media);
                    if (filepath.Length == 0)
                    {
                        // ファイルパスが正常に取れなければ無視
                        continue;
                    }

                    // ファイルパスが見つかった
                    // このファイルが張り付けられているフレーム位置とファイルパスのペアを追加する
                    fileInfos.Add(Tuple.Create(trackEvent.Start.FrameCount, filepath));                    
                }
            }


            // Titles & Text の Generator を取得する
            PlugInNode generator = GetGeneratorTitlesAndText(vegas);
            if (generator == null)
            {
                MessageBox.Show("Cannot get Titles & Text generator");
                return;
            }

            // ログ出力用のファイルを開く
            System.IO.StreamWriter writer = new System.IO.StreamWriter(saveFilePath, false, Encoding.GetEncoding("Shift_JIS"));


            // 抽出したファイル名を、最初のトラックの同時刻に追加する
            Timecode textFadeLength = Timecode.FromString("00:00:01;00");   // 追加するテキストのフェード時間
            VideoTrack textVideoTrack = vegas.Project.Tracks[0] as VideoTrack;    // テキストを追加するビデオトラック（先頭のトラックに追加する）
            if (textVideoTrack == null)
            {
                return;
            }

            long textEventOffsetMs = 1000;  // 追加するテキストは、動画や静止画よりも少し遅らせて追加する。遅らせる時間を ms で指定する
            Timecode textLength = Timecode.FromString("00:00:05;00");  // テキストの表示時間。固定時間としているが、動画や静止画がこれより短いとテキストが正しく表示されないので、動画や静止画の長さをチェックする必要がある

            // ファイル名からテキストイベントを作成する
            foreach (var fileInfo in fileInfos)
            {
                string filename = fileInfo.Item2;
                Timecode timecodeStart = new Timecode();
                timecodeStart.FrameCount = fileInfo.Item1;

                // TextEvent を表す新しい Media を生成する
                Media media = Media.CreateInstance(vegas.Project, generator);

                // Media の Effect を取得する
                OFXEffect ofxEffect = GetOFXEffect(media);
                if (ofxEffect == null)
                {
                    continue;
                }

                // 取得した Effect のパラメータを変更して、望むテロップにする

                long err = 0;

                // テキストを変える
                {
                    string newText = ToComment(filename); // ファイル名からコメントに変換する
                    float fontSize = 14;
                    err = ChangeText(ofxEffect, newText, fontSize);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                // テキストの表示位置を変える
                {
                    double x = 0.5;
                    double y = 0.1;
                    err = ChangeLocation(ofxEffect, x, y);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                // アウトラインの幅を変える
                {
                    double outlineWidth = 10.0;
                    err = ChangeOutlineWidth(ofxEffect, outlineWidth);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                // アウトラインの色を変える
                {
                    double r = 0.0;
                    double g = 0.0;
                    double b = 0.0;
                    double a = 1.0;
                    err = ChangeOutlineColor(ofxEffect, r, g, b, a);
                    if (err != 0)
                    {
                        // エラーは無視
                    }
                }

                ofxEffect.AllParametersChanged();

                // テロップを入れたい位置に video Event を生成する
                Timecode textStartTimecode = timecodeStart + Timecode.FromMilliseconds(textEventOffsetMs);
                VideoEvent videoEvent = new VideoEvent(textStartTimecode, textLength);

                // テキストをトラックに追加する
                textVideoTrack.Events.Add(videoEvent);

                Take take = new Take(media.GetVideoStreamByIndex(0));
                videoEvent.Takes.Add(take);

                videoEvent.FadeIn.Length = textFadeLength;
                videoEvent.FadeOut.Length = textFadeLength;

                // ログに出力する
                writer.WriteLine(timecodeStart.ToString() + " " + ToComment(filename).Replace('\n', ' '));
            }

            writer.Close();


            MessageBox.Show("終了しました。");
        }

        // Titles & Text のジェネレータを取得する
        private PlugInNode GetGeneratorTitlesAndText(Vegas vegas)
        {
            PlugInNode generator = vegas.Generators.GetChildByUniqueID("{Svfx:com.vegascreativesoftware:titlesandtext}"); //  Titles & Text
            return generator;
        }

        // ダイアログを開きファイルパスをユーザーに選択させる
        private string GetFilePath(string rootFilePath, string preFix)
        {
            SaveFileDialog sfd = new SaveFileDialog();
            sfd.FileName = preFix + System.IO.Path.GetFileNameWithoutExtension(rootFilePath) + ".txt";
            sfd.InitialDirectory = System.IO.Path.GetDirectoryName(rootFilePath) + "\\";
            sfd.Filter = "テキストファイル(*.txt)|*.txt";
            if (sfd.ShowDialog() != DialogResult.OK)
            {
                return "";
            }

            return sfd.FileName;
        }

        private OFXEffect GetOFXEffect(Media media)
        {
            if (media == null)
            {
                return null;
            }

            Effect generator = media.Generator;
            if (generator == null)
            {
                return null;
            }

            OFXEffect ofxEffect = generator.OFXEffect;
            if (ofxEffect == null)
            {
                return null;
            }

            return ofxEffect;
        }

        private string GetMediaFilePath(Media media)
        {
            if (media == null)
            {
                return "";
            }

            string filepath = media.FilePath;
            if (filepath.Length == 0)
            {
                // ファイルパスが正常に取れなかった
                return "";
            }

            // テキストイベントにもテキストの内容がファイルパスに入っている
            // それはほしいものではないので（そこにファイルはないので）除外する
            if (IsTextEvent(media))
            {
                // テキストイベントは無視
                return "";
            }

            return filepath;
        }

        // media がテキストイベントであれば true
        private bool IsTextEvent(Media media)
        {
            OFXEffect ofxEffect = GetOFXEffect(media);
            if (ofxEffect == null)
            {
                return false;
            }

            OFXStringParameter textParam = ofxEffect.FindParameterByName("Text") as OFXStringParameter;
            if (textParam == null)
            {
                // テキストイベントではない
                return false;
            }

            return true;
        }


        // ファイル名からテキストイベントのテキストに変換する
        // ファイル名は  2021-06-30T145043_comment.mp4 のようになっていることが前提
        // 以下のようなテキストが得られる：
        // 2021.06.30
        // comment

        private  string ToComment(string name)
        {
            string filename = Path.GetFileNameWithoutExtension(name);

            // filenameから撮影日を取得する
            var firstTIndex = filename.IndexOf('T');
            if (firstTIndex < 0)
            {
                return "";
            }

            string date_ = filename.Substring(0, firstTIndex);
            string date = date_.Replace('-', '.');

            // filenameからコメントを取得する
            var firstUnderbarIndex = filename.IndexOf('_');
            if (firstUnderbarIndex < 0)
            {
                return "";
            }

            string comment = filename.Substring(firstUnderbarIndex + 1);

            // 撮影日とコメントを連結する
            string text = date + " \n" + comment;

            return text;
        }

        // テキストを変える
        private long ChangeText(OFXEffect ofxEffect, string newText, float fontSize)
        {
            OFXStringParameter textParam = ofxEffect.FindParameterByName("Text") as OFXStringParameter;
            if (textParam == null)
            {
                return -1;
            }

            {
                string rtfData = textParam.Value;   // デフォルトで入っているテキスト

                RichTextBox richtextBox = new RichTextBox();
                FontFamily fontFamily = richtextBox.SelectionFont.FontFamily;
                richtextBox.Rtf = rtfData;
                richtextBox.Text = newText;
                richtextBox.SelectAll();    // 全テキストが対象

                richtextBox.SelectionFont = new System.Drawing.Font(fontFamily, fontSize);    // フォント変更

                textParam.Value = richtextBox.Rtf;
            }

            return 0;
        }

        // テキストの表示位置を変える
        private long ChangeLocation(OFXEffect ofxEffect, double x, double y)
        {
            OFXDouble2DParameter locationParam = ofxEffect.FindParameterByName("Location") as OFXDouble2DParameter;
            if (locationParam == null)
            {
                return -1;
            }

            OFXDouble2D location;
            location.X = x;
            location.Y = y;

            locationParam.Value = location;

            return 0;
        }

        // アウトラインの幅を変える
        private long ChangeOutlineWidth(OFXEffect ofxEffect, double width)
        {
            OFXDoubleParameter outlineWidthParam = ofxEffect.FindParameterByName("OutlineWidth") as OFXDoubleParameter;
            if (outlineWidthParam == null)
            {
                return -1;
            }

            outlineWidthParam.Value = width;

            return 0;
        }

        // アウトラインの色を変える
        private long ChangeOutlineColor(OFXEffect ofxEffect, double r, double g, double b, double a)
        {
            OFXRGBAParameter outlineColorParam = ofxEffect.FindParameterByName("OutlineColor") as OFXRGBAParameter;
            if (outlineColorParam == null)
            {
                return -1;
            }

            outlineColorParam.Value = new OFXColor(r, g, b, a);
  
            return 0;
        }

    }

}

```