Skip to content

Instantly share code, notes, and snippets.

@joonjoonjoon
Last active January 31, 2023 07:40
Show Gist options
  • Save joonjoonjoon/47d1693b345fe8a3d89d9e2156d4c911 to your computer and use it in GitHub Desktop.
Save joonjoonjoon/47d1693b345fe8a3d89d9e2156d4c911 to your computer and use it in GitHub Desktop.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Subtitle Default", menuName = "Subtitle SO", order = 1)]
public class SubtitleSO : ScriptableObject
{
#if UNITY_EDITOR
public AudioClip clip;
#endif
[FMODUnity.EventRef] public string fmodEvent;
public string localizationTerm;
public List<string> lines;
public List<float> timestamps;
public float length;
public bool isDictaphone;
}
using System;
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
class SubtitleWindow : EditorWindow
{
SubtitleSO _subtitle;
SubtitleSO _lastSubtitle;
Texture2D _lines; // buffer for the lines
Texture2D _waveform; // buffer for the waveform
Texture2D _display; // buffer for what actually gets displayed (lines + waveform)
float _time;
float _timeCurrent;
int _sampleCount;
Color32[] _clear;
GameObject _tempGameObject;
AudioSource _tempAudioSource;
GameObject GetTempGameObject
{
get
{
if(_tempGameObject == null) _tempGameObject = new GameObject();
_tempGameObject.hideFlags = HideFlags.DontSave;
_tempGameObject.name = "Subtitle Audio Source (temp)";
return _tempGameObject;
}
}
AudioSource GetTempAudioSource
{
get
{
if(_tempAudioSource == null) _tempAudioSource = GetTempGameObject.AddComponent<AudioSource>();
return _tempAudioSource;
}
}
[MenuItem ("Joon/Subtitles/Show Subtitler Window")]
public static void ShowWindow () {
var window = (SubtitleWindow)GetWindow(typeof(SubtitleWindow));
window.titleContent.text = "Subtitler";
window.Show();
}
void OnDisable()
{
if(_tempGameObject != null) DestroyImmediate(_tempGameObject);
_tempAudioSource = null;
_tempGameObject = null;
}
void OnGUI()
{
if (Application.isPlaying)
{
GUILayout.Label("Edit mode only, my friend.", EditorStyles.boldLabel);
return;
}
GUILayout.Label("Subtitle SO", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_subtitle = (SubtitleSO)EditorGUILayout.ObjectField(_subtitle, typeof(SubtitleSO), false);
if (GUILayout.Button("Refresh")) _lastSubtitle = null;
EditorGUILayout.EndHorizontal();
// Refresh on change
if (_subtitle != _lastSubtitle)
{
_waveform = null;
_lines = null;
_time = 0;
_timeCurrent = 0;
}
_lastSubtitle = _subtitle;
if(_subtitle == null)
{
GUILayout.Label("no subtitle selected", EditorStyles.boldLabel);
}
else
{
if (_subtitle.clip == null)
{
GUILayout.Label("subtitle is missing clip...", EditorStyles.boldLabel);
}
else
{
// redraw if the window changed size
if (_waveform == null || _waveform.width != (int) position.width)
{
_waveform = PaintWaveformSpectrum(_subtitle.clip, (int) position.width, 100, Color.black);
if(_waveform != null && _display!=null &&
_waveform.width == _display.width && _waveform.height == _display.height)
Graphics.CopyTexture(_waveform, _display);
else
{
_lastSubtitle = null;
}
_sampleCount = _subtitle.clip.samples; //AudioUtility.GetSampleCount(_subtitle.clip);
if (_subtitle.length != _subtitle.clip.length)
{
_subtitle.length = _subtitle.clip.length;
EditorUtility.SetDirty(_subtitle);
}
PaintLinesOnwaveform();
}
// show clip name
GUILayout.Label("Clip name: " + _subtitle.clip.name + " (" + _subtitle.clip.length.ToString("0.00") + "s)", EditorStyles.boldLabel);
if (Event.current.isMouse && Event.current.button == 0 &&
Event.current.mousePosition.y > 80 && Event.current.mousePosition.y < 180)
{
_time = Event.current.mousePosition.x / (float) position.width;
PlayAudioclip(_subtitle.clip, _time);
}
// calculate moving line
if (GetTempAudioSource.isPlaying)
{
_timeCurrent = GetTempAudioSource.time / _subtitle.clip.length;
PaintLinesOnwaveform();
}
// draw the preview
PaintLinesOnwaveform();
EditorGUI.DrawPreviewTexture(new Rect(0, 80, (int) position.width, 100), _display);
EditorGUI.DrawPreviewTexture(new Rect(0, 180, (int) position.width, 20), _lines);
}
EditorGUILayout.Space();
for (int i = 0; i < 7; i++)
{
GUILayout.Label("");
}
GUILayout.Label("FMOD event: " + _subtitle.fmodEvent);
GUILayout.Label("Localization term: " + _subtitle.localizationTerm);
GUILayout.Label("Lines", EditorStyles.boldLabel);
// reset button
if (GUILayout.Button("RESET ALL"))
{
for (int i = 0; i < _subtitle.timestamps.Count; i++)
{
_subtitle.timestamps[i] = 0;
}
EditorUtility.SetDirty(_subtitle);
}
// Rows of subtitles
for (int i = 0; i < _subtitle.lines.Count; i++)
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Set"))
{
_subtitle.timestamps[i] = _time * _subtitle.clip.length;
EditorUtility.SetDirty(_subtitle);
}
if (GUILayout.Button("Play"))
{
var normalizedTime = _subtitle.timestamps[i] / _subtitle.clip.length;
_time = normalizedTime;
PlayAudioclip(_subtitle.clip, normalizedTime);
}
var length = ((i + 1 < _subtitle.lines.Count ? _subtitle.timestamps[i + 1] : _subtitle.length) -
_subtitle.timestamps[i]);
string warning = (length < 0.75f ? "(< 0.75 seconds !!!!!!!!!!!!!!!!!!!!!!!!!!!!" : "");
EditorGUILayout.LabelField(i + ". " + _subtitle.lines[i] + warning,
GUILayout.Width(position.width * 0.65f));
if(_subtitle.timestamps.Count<=i) _subtitle.timestamps.Add(0);
EditorGUILayout.LabelField("[Chars: " + _subtitle.lines[i].Length.ToString() + "]", GUILayout.Width(position.width * 0.08f));
var origFontStyle = EditorStyles.label.fontStyle;
var origColor = EditorStyles.label.normal.textColor;
if (_subtitle.timestamps[i] == 0)
{
EditorStyles.label.fontStyle = FontStyle.Bold;
EditorStyles.label.normal.textColor = Color.red;
}
EditorGUILayout.LabelField("[S: " + _subtitle.timestamps[i].ToString("0.00") + "s]",
GUILayout.Width(position.width * 0.08f));
EditorStyles.label.fontStyle = origFontStyle;
EditorStyles.label.normal.textColor = origColor;
EditorGUILayout.LabelField("[Len: " + (length).ToString("0.00") + "s]" ,
GUILayout.Width(position.width * 0.08f));
EditorGUILayout.EndHorizontal();
}
}
}
void PlayAudioclip(AudioClip clip, float normalizedTime)
{
var time = normalizedTime * clip.length;
_tempAudioSource.Stop();
_tempAudioSource.clip = clip;
_tempAudioSource.time = time;
_tempAudioSource.Play();
}
void Update()
{
if(_subtitle != null && _subtitle.clip != null && GetTempAudioSource.isPlaying)
Repaint();
}
// Used from inside a custom inspector on SubtitleSO's
public static void ForceSubtitle(SubtitleSO subtitle)
{
var window = (SubtitleWindow)GetWindow(typeof(SubtitleWindow));
window._subtitle = subtitle;
window.Repaint();
}
// Waveform painting from audio-source
public Texture2D PaintWaveformSpectrum(AudioClip audio, int width, int height, Color color) {
Texture2D tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
float[] samples = new float[audio.samples * audio.channels];
float[] waveform = new float[width];
audio.GetData(samples, 0);
float packSize = ( samples.Length / (float)width ) + 1;
int s = 0;
for (float i = 0; i < samples.Length-1; i += packSize) {
Debug.Log(i + " " + samples.Length + " " +s + " " + waveform.Length);
waveform[s] = Mathf.Abs(samples[Mathf.RoundToInt(i)]);
s++;
}
// background
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
tex.SetPixel(x, y, Color.grey);
}
}
// waveform
for (int x = 0; x < waveform.Length; x++) {
for (int y = 0; y <= waveform[x] * ((float)height * .75f); y++) {
tex.SetPixel(x, ( height / 2 ) + y, color);
tex.SetPixel(x, ( height / 2 ) - y, color);
}
}
tex.Apply();
return tex;
}
// progress lines painmting
public void PaintLinesOnwaveform()
{
if(_display==null || _display.width != _waveform.width) _display = new Texture2D(_waveform.width, _waveform.height, TextureFormat.RGBA32, false);
if (_lines==null || _lines.width != _waveform.width) _lines = new Texture2D(_waveform.width, _waveform.height, TextureFormat.RGBA32, false);
if (_clear == null || _clear.Length != _lines.GetPixels32().Length)
{
_clear = new Color32[_lines.GetPixels32().Length];
for (int i = 0; i < _clear.Length; i++)
{
_clear[i] = new Color32(0,0,0,0);
}
}
_lines.SetPixels32(_clear);
// bars
for (int y = 0; y <= 100; y++) {
_lines.SetPixel((int) (_time * _lines.width), ( _lines.height / 2 ) + y, Color.white);
_lines.SetPixel((int)(_time * _lines.width), ( _lines.height / 2 ) - y, Color.blue);
}
for (int y = 0; y <= 100; y++) {
_lines.SetPixel((int) (_timeCurrent * _lines.width), ( _lines.height / 2 ) + y, Color.white);
_lines.SetPixel((int)(_timeCurrent * _lines.width), ( _lines.height / 2 ) - y, Color.blue);
}
_lines.Apply();
// TODO: For some reason this stopped working at some point and I never fixed it...
// The line used to be drawn on top of the main waveform texture,
// but there was something broken and I don't remember really...
//Graphics.CopyTexture(_waveform, _display);
//Graphics.CopyTexture(_lines, _display);
//_display.Apply();
}
}
@QuentWashington
Copy link

QuentWashington commented Jan 31, 2023

Thanks a lot for your work. Very valuable and useful work. By the way, you can evaluate how it works in practice on this page. For people who are just learning a language, a subtitle is an indispensable thing. So honor to you and praise for your work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment