はじめに
Unityでオーディオ編集を行う際、外部ツールを使わずにエディタ内で簡単にトリミングできると便利です。この記事では、波形を視覚的に確認しながらオーディオクリップをトリミングできるエディタ拡張を紹介します。
機能
このエディタ拡張には以下の機能があります。
- 波形の視覚化: オーディオクリップの波形をリアルタイム生成・表示
- ビジュアルな範囲選択: トリム範囲を波形上でカラー表示し、スライダーで調整
- プレビュー再生: トリム範囲をエディタ内で試聴
- WAVファイル出力: トリムしたオーディオを新しいWAVファイルとして保存
ファイル構成
Assets/
└── Editor/
└── AudioTrimmerWindow.cs
使い方
- Unityエディタのメニューから
Tools > Audio > Audio Trimmerを選択 -
Source Audio Clipにトリミングしたいオーディオクリップをドラッグ&ドロップ - 波形が自動生成される
- スライダーでトリム範囲を調整(波形上に緑の開始線・赤の終了線・青の選択範囲が表示される)
-
Preview Rangeで試聴 -
Trim and Saveで保存(元ファイルと同じフォルダに_trimmedサフィックス付きで保存)
コード全体
AudioTrimmerWindow.cs(クリックで展開)
using UnityEngine;
using UnityEditor;
using System.IO;
namespace U1W.Editor
{
/// <summary>
/// オーディオクリップをトリミングするためのエディターウィンドウ
/// </summary>
public class AudioTrimmerWindow : EditorWindow
{
private AudioClip _sourceClip;
private float _startTime = 0f;
private float _endTime = 1f;
private bool _isPlaying = false;
private Texture2D _waveformTexture;
private const int WAVEFORM_WIDTH = 800;
private const int WAVEFORM_HEIGHT = 200;
private Vector2 _scrollPosition;
[MenuItem("Tools/Audio/Audio Trimmer")]
public static void ShowWindow()
{
var window = GetWindow<AudioTrimmerWindow>("Audio Trimmer");
window.minSize = new Vector2(850, 400);
}
private void OnGUI()
{
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Audio Trimmer", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("オーディオクリップの波形を表示してトリミング範囲を選択できます。", MessageType.Info);
EditorGUILayout.Space(10);
// ソースクリップ選択
EditorGUI.BeginChangeCheck();
_sourceClip = (AudioClip)EditorGUILayout.ObjectField("Source Audio Clip", _sourceClip, typeof(AudioClip), false);
if (EditorGUI.EndChangeCheck() && _sourceClip != null)
{
_endTime = _sourceClip.length;
GenerateWaveform();
}
if (_sourceClip == null)
{
EditorGUILayout.HelpBox("オーディオクリップを選択してください。", MessageType.Warning);
return;
}
EditorGUILayout.Space(10);
// クリップ情報表示
EditorGUILayout.LabelField("Clip Info", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"Length: {_sourceClip.length:F3} sec");
EditorGUILayout.LabelField($"Frequency: {_sourceClip.frequency} Hz");
EditorGUILayout.LabelField($"Channels: {_sourceClip.channels}");
EditorGUILayout.LabelField($"Samples: {_sourceClip.samples}");
EditorGUILayout.Space(10);
// 波形表示
if (_waveformTexture != null)
{
EditorGUILayout.LabelField("Waveform", EditorStyles.boldLabel);
Rect waveformRect = GUILayoutUtility.GetRect(WAVEFORM_WIDTH, WAVEFORM_HEIGHT);
GUI.DrawTexture(waveformRect, _waveformTexture);
// トリム範囲の視覚的表示
float startX = waveformRect.x + (waveformRect.width * (_startTime / _sourceClip.length));
float endX = waveformRect.x + (waveformRect.width * (_endTime / _sourceClip.length));
// 選択範囲の背景
Rect selectionRect = new Rect(startX, waveformRect.y, endX - startX, waveformRect.height);
EditorGUI.DrawRect(selectionRect, new Color(0.3f, 0.7f, 1f, 0.2f));
// 開始・終了ライン
EditorGUI.DrawRect(new Rect(startX - 1, waveformRect.y, 2, waveformRect.height), Color.green);
EditorGUI.DrawRect(new Rect(endX - 1, waveformRect.y, 2, waveformRect.height), Color.red);
}
else if (_sourceClip != null)
{
if (GUILayout.Button("Generate Waveform", GUILayout.Height(30)))
{
GenerateWaveform();
}
}
EditorGUILayout.Space(10);
// トリム範囲設定
EditorGUILayout.LabelField("Trim Range", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Start Time (sec):", GUILayout.Width(120));
_startTime = EditorGUILayout.Slider(_startTime, 0f, _sourceClip.length);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("End Time (sec):", GUILayout.Width(120));
_endTime = EditorGUILayout.Slider(_endTime, 0f, _sourceClip.length);
EditorGUILayout.EndHorizontal();
// 範囲の妥当性チェック
if (_startTime >= _endTime)
{
_endTime = Mathf.Min(_startTime + 0.1f, _sourceClip.length);
}
float duration = _endTime - _startTime;
EditorGUILayout.LabelField($"Duration: {duration:F3} sec");
EditorGUILayout.Space(10);
// プレビュー再生
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Preview Range", GUILayout.Height(30)))
{
PreviewTrimmedAudio();
}
if (GUILayout.Button("Stop Preview", GUILayout.Height(30)))
{
StopAllClips();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
// トリム実行
GUI.backgroundColor = Color.green;
if (GUILayout.Button("Trim and Save", GUILayout.Height(40)))
{
TrimAndSaveAudio();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.Space(10);
EditorGUILayout.HelpBox(
"トリム後のオーディオは元のファイルと同じフォルダに '_trimmed' サフィックス付きで保存されます。",
MessageType.Info
);
}
private void GenerateWaveform()
{
if (_sourceClip == null) return;
// 既存のテクスチャを破棄
if (_waveformTexture != null)
{
DestroyImmediate(_waveformTexture);
}
_waveformTexture = new Texture2D(WAVEFORM_WIDTH, WAVEFORM_HEIGHT);
// 背景を黒で塗りつぶし
Color[] pixels = new Color[WAVEFORM_WIDTH * WAVEFORM_HEIGHT];
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = new Color(0.1f, 0.1f, 0.1f, 1f);
}
_waveformTexture.SetPixels(pixels);
// サンプルデータ取得
float[] samples = new float[_sourceClip.samples * _sourceClip.channels];
_sourceClip.GetData(samples, 0);
int samplesPerPixel = samples.Length / WAVEFORM_WIDTH;
// 波形描画
for (int x = 0; x < WAVEFORM_WIDTH; x++)
{
int startSample = x * samplesPerPixel;
int endSample = Mathf.Min(startSample + samplesPerPixel, samples.Length);
float min = 0f;
float max = 0f;
// この範囲内の最大・最小値を取得
for (int i = startSample; i < endSample; i++)
{
float sample = samples[i];
if (sample < min) min = sample;
if (sample > max) max = sample;
}
// 波形を描画
int yMin = (int)((1f - (min + 1f) / 2f) * WAVEFORM_HEIGHT);
int yMax = (int)((1f - (max + 1f) / 2f) * WAVEFORM_HEIGHT);
yMin = Mathf.Clamp(yMin, 0, WAVEFORM_HEIGHT - 1);
yMax = Mathf.Clamp(yMax, 0, WAVEFORM_HEIGHT - 1);
for (int y = yMax; y <= yMin; y++)
{
_waveformTexture.SetPixel(x, y, Color.cyan);
}
}
// 中心線を描画
int centerY = WAVEFORM_HEIGHT / 2;
for (int x = 0; x < WAVEFORM_WIDTH; x++)
{
_waveformTexture.SetPixel(x, centerY, new Color(0.5f, 0.5f, 0.5f, 1f));
}
_waveformTexture.Apply();
}
private void PreviewTrimmedAudio()
{
if (_sourceClip == null) return;
// Unity エディターでオーディオをプレビュー再生
var audioUtil = System.Type.GetType("UnityEditor.AudioUtil, UnityEditor");
if (audioUtil != null)
{
var playClipMethod = audioUtil.GetMethod(
"PlayPreviewClip",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
null,
new System.Type[] { typeof(AudioClip), typeof(int), typeof(bool) },
null
);
if (playClipMethod != null)
{
int startSample = (int)(_startTime * _sourceClip.frequency);
playClipMethod.Invoke(null, new object[] { _sourceClip, startSample, false });
}
}
}
private void StopAllClips()
{
var audioUtil = System.Type.GetType("UnityEditor.AudioUtil, UnityEditor");
if (audioUtil != null)
{
var stopMethod = audioUtil.GetMethod(
"StopAllPreviewClips",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public
);
if (stopMethod != null)
{
stopMethod.Invoke(null, null);
}
}
}
private void TrimAndSaveAudio()
{
if (_sourceClip == null)
{
EditorUtility.DisplayDialog("Error", "ソースクリップが選択されていません。", "OK");
return;
}
// ソースファイルのパスを取得
string sourcePath = AssetDatabase.GetAssetPath(_sourceClip);
if (string.IsNullOrEmpty(sourcePath))
{
EditorUtility.DisplayDialog("Error", "ソースファイルのパスを取得できません。", "OK");
return;
}
// 保存先パスを生成
string directory = Path.GetDirectoryName(sourcePath);
string fileName = Path.GetFileNameWithoutExtension(sourcePath);
string extension = Path.GetExtension(sourcePath);
string outputPath = Path.Combine(directory, $"{fileName}_trimmed{extension}");
// WAV形式でエクスポート
if (!ExportTrimmedWav(outputPath))
{
EditorUtility.DisplayDialog("Error", "オーディオのトリムに失敗しました。", "OK");
return;
}
AssetDatabase.Refresh();
EditorUtility.DisplayDialog(
"Success",
$"トリムされたオーディオを保存しました:\n{outputPath}",
"OK"
);
}
private bool ExportTrimmedWav(string outputPath)
{
try
{
// サンプルデータを取得
float[] samples = new float[_sourceClip.samples * _sourceClip.channels];
_sourceClip.GetData(samples, 0);
// トリム範囲のサンプルを抽出
int startSample = (int)(_startTime * _sourceClip.frequency) * _sourceClip.channels;
int endSample = (int)(_endTime * _sourceClip.frequency) * _sourceClip.channels;
int trimmedLength = endSample - startSample;
float[] trimmedSamples = new float[trimmedLength];
System.Array.Copy(samples, startSample, trimmedSamples, 0, trimmedLength);
// WAVファイルとして書き出し
WriteWavFile(outputPath, trimmedSamples, _sourceClip.frequency, _sourceClip.channels);
return true;
}
catch (System.Exception e)
{
Debug.LogError($"Failed to export trimmed audio: {e.Message}");
return false;
}
}
private void WriteWavFile(string path, float[] samples, int frequency, int channels)
{
using (FileStream fs = new FileStream(path, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs))
{
int sampleCount = samples.Length;
int byteRate = frequency * channels * 2; // 16bit = 2 bytes
// WAV ヘッダー
writer.Write(new char[4] { 'R', 'I', 'F', 'F' });
writer.Write(36 + sampleCount * 2); // ファイルサイズ - 8
writer.Write(new char[4] { 'W', 'A', 'V', 'E' });
// fmt チャンク
writer.Write(new char[4] { 'f', 'm', 't', ' ' });
writer.Write(16); // fmtチャンクサイズ
writer.Write((short)1); // フォーマット (1 = PCM)
writer.Write((short)channels);
writer.Write(frequency);
writer.Write(byteRate);
writer.Write((short)(channels * 2)); // ブロックアライン
writer.Write((short)16); // ビット深度
// data チャンク
writer.Write(new char[4] { 'd', 'a', 't', 'a' });
writer.Write(sampleCount * 2);
// サンプルデータを16bit PCMに変換して書き込み
for (int i = 0; i < sampleCount; i++)
{
short sample = (short)(samples[i] * 32767f);
writer.Write(sample);
}
}
}
private void OnDisable()
{
StopAllClips();
if (_waveformTexture != null)
{
DestroyImmediate(_waveformTexture);
}
}
}
}
コードの解説
1. EditorWindowの基本設定
public class AudioTrimmerWindow : EditorWindow
{
private AudioClip _sourceClip;
private float _startTime = 0f;
private float _endTime = 1f;
private Texture2D _waveformTexture;
private const int WAVEFORM_WIDTH = 800;
private const int WAVEFORM_HEIGHT = 200;
[MenuItem("Tools/Audio/Audio Trimmer")]
public static void ShowWindow()
{
var window = GetWindow<AudioTrimmerWindow>("Audio Trimmer");
window.minSize = new Vector2(850, 400);
}
}
-
EditorWindowを継承してカスタムエディタウィンドウを作成 -
[MenuItem]でメニューからウィンドウを開けるように設定 - 波形表示用のテクスチャサイズを定数で定義(800x200px)
2. 波形生成アルゴリズム
private void GenerateWaveform()
{
// サンプルデータ取得
float[] samples = new float[_sourceClip.samples * _sourceClip.channels];
_sourceClip.GetData(samples, 0);
int samplesPerPixel = samples.Length / WAVEFORM_WIDTH;
// 波形描画
for (int x = 0; x < WAVEFORM_WIDTH; x++)
{
int startSample = x * samplesPerPixel;
int endSample = Mathf.Min(startSample + samplesPerPixel, samples.Length);
float min = 0f;
float max = 0f;
// この範囲内の最大・最小値を取得
for (int i = startSample; i < endSample; i++)
{
float sample = samples[i];
if (sample < min) min = sample;
if (sample > max) max = sample;
}
// 波形をシアン色で描画
int yMin = (int)((1f - (min + 1f) / 2f) * WAVEFORM_HEIGHT);
int yMax = (int)((1f - (max + 1f) / 2f) * WAVEFORM_HEIGHT);
for (int y = yMax; y <= yMin; y++)
{
_waveformTexture.SetPixel(x, y, Color.cyan);
}
}
_waveformTexture.Apply();
}
アルゴリズムの流れ:
- オーディオクリップから全サンプルデータを取得
- 画面幅(800px)に合わせてサンプルをグループ化
- 各グループの最大値・最小値を取得
- 最大値・最小値を縦位置に変換
- 縦線として描画(ピーク波形表示)
利点:
- 長時間のオーディオでも軽量に描画
- 全体の音量変化が一目瞭然
3. トリム範囲の視覚化
// トリム範囲の視覚的表示
float startX = waveformRect.x + (waveformRect.width * (_startTime / _sourceClip.length));
float endX = waveformRect.x + (waveformRect.width * (_endTime / _sourceClip.length));
// 選択範囲の背景(青の半透明)
Rect selectionRect = new Rect(startX, waveformRect.y, endX - startX, waveformRect.height);
EditorGUI.DrawRect(selectionRect, new Color(0.3f, 0.7f, 1f, 0.2f));
// 開始・終了ライン(緑・赤)
EditorGUI.DrawRect(new Rect(startX - 1, waveformRect.y, 2, waveformRect.height), Color.green);
EditorGUI.DrawRect(new Rect(endX - 1, waveformRect.y, 2, waveformRect.height), Color.red);
- 開始位置を緑の線で表示
- 終了位置を赤の線で表示
- 選択範囲を青の半透明で塗りつぶし
4. エディタ内でのプレビュー再生
private void PreviewTrimmedAudio()
{
if (_sourceClip == null) return;
// エディター用の AudioUtil を使用(リフレクション)
var audioUtil = System.Type.GetType("UnityEditor.AudioUtil, UnityEditor");
if (audioUtil != null)
{
var playClipMethod = audioUtil.GetMethod(
"PlayPreviewClip",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
null,
new System.Type[] { typeof(AudioClip), typeof(int), typeof(bool) },
null
);
if (playClipMethod != null)
{
int startSample = (int)(_startTime * _sourceClip.frequency);
playClipMethod.Invoke(null, new object[] { _sourceClip, startSample, false });
}
}
}
ポイント:
-
UnityEditor.AudioUtilは公式ドキュメントに載っていない内部API - リフレクションを使ってエディタ内でのオーディオプレビューを実現
-
PlayPreviewClipで指定したサンプル位置から再生
注意点:
- 内部APIのため、Unityバージョンによっては動作しない可能性がある
- 本番コードではなく、エディタツール限定で使用すること
5. WAVファイルの書き出し
private bool ExportTrimmedWav(string outputPath)
{
// サンプルデータを取得
float[] samples = new float[_sourceClip.samples * _sourceClip.channels];
_sourceClip.GetData(samples, 0);
// トリム範囲のサンプルを抽出
int startSample = (int)(_startTime * _sourceClip.frequency) * _sourceClip.channels;
int endSample = (int)(_endTime * _sourceClip.frequency) * _sourceClip.channels;
int trimmedLength = endSample - startSample;
float[] trimmedSamples = new float[trimmedLength];
System.Array.Copy(samples, startSample, trimmedSamples, 0, trimmedLength);
// WAVファイルとして書き出し
WriteWavFile(outputPath, trimmedSamples, _sourceClip.frequency, _sourceClip.channels);
return true;
}
処理の流れ:
- オーディオクリップから全サンプルを取得
- トリム範囲のサンプルインデックスを計算
- 該当範囲のサンプルを新しい配列にコピー
- WAVファイルとして書き出し
6. WAVファイルフォーマット
private void WriteWavFile(string path, float[] samples, int frequency, int channels)
{
using (FileStream fs = new FileStream(path, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs))
{
int sampleCount = samples.Length;
int byteRate = frequency * channels * 2; // 16bit = 2 bytes
// WAV ヘッダー(RIFFチャンク)
writer.Write(new char[4] { 'R', 'I', 'F', 'F' });
writer.Write(36 + sampleCount * 2); // ファイルサイズ - 8
writer.Write(new char[4] { 'W', 'A', 'V', 'E' });
// fmt チャンク
writer.Write(new char[4] { 'f', 'm', 't', ' ' });
writer.Write(16); // fmtチャンクサイズ
writer.Write((short)1); // フォーマット (1 = PCM)
writer.Write((short)channels);
writer.Write(frequency);
writer.Write(byteRate);
writer.Write((short)(channels * 2)); // ブロックアライン
writer.Write((short)16); // ビット深度
// data チャンク
writer.Write(new char[4] { 'd', 'a', 't', 'a' });
writer.Write(sampleCount * 2);
// サンプルデータを16bit PCMに変換して書き込み
for (int i = 0; i < sampleCount; i++)
{
short sample = (short)(samples[i] * 32767f);
writer.Write(sample);
}
}
}
WAVフォーマットの構造:
- RIFFヘッダー: ファイル識別子("RIFF" + ファイルサイズ + "WAVE")
- fmtチャンク: 音声フォーマット情報(PCM、サンプリングレート、チャンネル数など)
- dataチャンク: 実際の音声データ(16bit PCM)
サンプルの変換:
- Unity内部では-1.0~1.0のfloat値
- WAVファイルでは-32768~32767のshort値
-
sample * 32767fで変換
カスタマイズ例
1. 波形の色変更
// 波形を緑色に
_waveformTexture.SetPixel(x, y, Color.green);
// グラデーション表示
float intensity = Mathf.Abs(sample);
Color waveColor = Color.Lerp(Color.yellow, Color.red, intensity);
_waveformTexture.SetPixel(x, y, waveColor);
2. マルチチャンネル対応
// ステレオの場合、左右チャンネルを個別に描画
for (int channel = 0; channel < _sourceClip.channels; channel++)
{
int channelHeight = WAVEFORM_HEIGHT / _sourceClip.channels;
int channelOffset = channel * channelHeight;
// チャンネルごとに波形を描画
// ...
}
3. フェードイン/フェードアウト
トリミング時にフェード処理を追加:
float fadeInDuration = 0.1f; // 0.1秒
float fadeOutDuration = 0.1f;
for (int i = 0; i < trimmedSamples.Length; i++)
{
float time = (float)i / (_sourceClip.frequency * _sourceClip.channels);
float totalDuration = (float)trimmedSamples.Length / (_sourceClip.frequency * _sourceClip.channels);
// フェードイン
if (time < fadeInDuration)
{
trimmedSamples[i] *= time / fadeInDuration;
}
// フェードアウト
if (time > totalDuration - fadeOutDuration)
{
trimmedSamples[i] *= (totalDuration - time) / fadeOutDuration;
}
}
応用例
- ループ音素材の作成: ゲーム用BGMのループポイント切り出し
- 効果音の無音除去: 録音した効果音の前後の無音部分をトリミング
- ボイス編集: 会話の一部を抽出してキャラクターのセリフとして使用
- サンプリング: 長いオーディオから特定のフレーズを切り出し
まとめ
このエディタ拡張により、Unity内でオーディオ編集が完結します。
主な利点:
- 外部ツール不要で作業効率UP
- 波形を見ながら正確にトリミング
- エディタ内でプレビュー再生可能
- WAV形式で高品質に保存
技術的な学び:
-
Texture2Dを使った波形の視覚化 - WAVファイルフォーマットの理解
- Unityエディタ内部APIの活用(リフレクション)
-
EditorWindowを使ったカスタムツール作成
ぜひプロジェクトに導入して、オーディオ編集を効率化してみてください!
動作環境
- Unity 6000.0.60f1 以降
- .NET Standard 2.1
