Merge branch 'SubtitleEdit:main' into master

This commit is contained in:
May Kittens Devour Your Soul 2023-10-30 08:12:05 +01:00 committed by GitHub
commit e02778b9c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1670 additions and 13 deletions

View File

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class CaptionScreen
{
public CcRow[] Rows = {
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
new CcRow(),
};
public int CurrentRow { get; set; } = Constants.ScreenRowCount - 1;
public int? NumberOfRollUpRows { get; set; }
public CaptionScreen()
{
Reset();
}
public SerializedRow[] Serialize()
{
var results = new List<SerializedRow>();
for (var i = 0; i < Constants.ScreenRowCount; i++)
{
var row = Rows[i];
if (row.IsEmpty())
{
continue;
}
results.Add(new SerializedRow
{
Row = i,
Position = row.FirstNonEmpty(),
Style = row.CurrentPenState.Serialize(),
Columns = row.Chars.Select(SerializeChar).ToArray()
});
}
return results.ToArray();
}
private SerializedStyledUnicodeChar SerializeChar(StyledUnicodeChar character)
{
return new SerializedStyledUnicodeChar
{
Character = character.Uchar,
Style = character.PenState.Serialize(),
};
}
public void Reset()
{
for (var i = 0; i < Constants.ScreenRowCount; i++)
{
Rows[i].Clear();
}
CurrentRow = Constants.ScreenRowCount - 1;
}
public bool Equals(CaptionScreen other)
{
var equal = true;
for (var i = 0; i < Constants.ScreenRowCount; i++)
{
if (!Rows[i].Equals(other.Rows[i]))
{
equal = false;
break;
}
}
return equal;
}
public void Copy(CaptionScreen other)
{
for (var i = 0; i < Constants.ScreenRowCount; i++)
{
Rows[i].Copy(other.Rows[i]);
}
}
public bool IsEmpty()
{
var empty = true;
for (var i = 0; i < Constants.ScreenRowCount; i++)
{
if (!Rows[i].IsEmpty())
{
empty = false;
break;
}
}
return empty;
}
public void BackSpace()
{
Rows[CurrentRow].BackSpace();
}
public void ClearToEndOfRow()
{
Rows[CurrentRow].ClearToEndOfRow();
}
public void InsertChar(int character)
{
Rows[CurrentRow].InsertChar(character);
}
public void SetPen(SerializedPenState styles)
{
Rows[CurrentRow].SetPenStyles(styles);
}
public void MoveCursor(int relPos)
{
Rows[CurrentRow].MoveCursor(relPos);
}
public void SetPac(PacData pacData)
{
var newRow = pacData.Row - 1;
CurrentRow = newRow;
var row = Rows[CurrentRow];
if (pacData.Indent != null)
{
var indent = pacData.Indent;
var prevPos = Math.Max(indent.Value - 1, 0);
row.Position = pacData.Indent.Value;
pacData.Color = row.Chars[prevPos].PenState.Foreground;
}
SetPen(new SerializedPenState
{
Foreground = pacData.Color ?? Constants.ColorWhite,
Underline = pacData.Underline,
Italics = pacData.Italics ?? false,
Background = Constants.ColorBlack,
Flash = false,
});
}
public void SetBkgData(SerializedPenState bkgData)
{
BackSpace();
SetPen(bkgData);
InsertChar(0x20); // Space
}
public void SetRollUpRows(int nrRows)
{
NumberOfRollUpRows = nrRows;
}
public void RollUp()
{
// if the row is empty we have nothing to roll-up
if (NumberOfRollUpRows == null || Rows[CurrentRow].IsEmpty())
{
return;
}
var rows = Rows.ToList();
rows.RemoveAt(CurrentRow - NumberOfRollUpRows.Value + 1);
rows.Add(new CcRow());
Rows = rows.ToArray();
}
}
}

View File

@ -0,0 +1,17 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class CcData
{
public CcData(int type, int data1, int data2)
{
Type = type;
Data1 = data1;
Data2 = data2;
}
public int Type { get; set; }
public int Data1 { get; set; }
public int Data2 { get; set; }
public ulong Time { get; set; }
}
}

View File

@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class CcDataC608Parser
{
public delegate void DisplayScreenDelegate(DataOutput data);
public Cea608Channel[] Channels { get; set; }
public int? CurrentChannelNumber { get; set; }
public int? LastTime { get; set; }
private int? _lastCmdA;
private int? _lastCmdB;
public DisplayScreenDelegate DisplayScreen { get; set; }
public CcDataC608Parser()
{
Channels = new[]
{
new Cea608Channel(1, this),
new Cea608Channel(2, this),
};
CurrentChannelNumber = -1;
}
public void AddData(int t, int[] byteList)
{
LastTime = t;
for (var i = 0; i < byteList.Length; i += 2)
{
var a = byteList[i] & 0x7f;
var b = byteList[i + 1] & 0x7f;
if (a == 0 && b == 0)
{
continue;
}
if (!(ParseCmd(a, b) ||
ParseMidRow(a, b) ||
ParsePac(a, b) ||
ParseBackgroundAttributes(a, b)))
{
ParseCharacters(a, b);
}
}
}
private void ParseCharacters(int ccData1, int ccData2)
{
var charsFound = ParseChars(ccData1, ccData2);
if (charsFound.Length > 0)
{
if (CurrentChannelNumber != null && CurrentChannelNumber >= 0)
{
var channel = Channels[CurrentChannelNumber.Value - 1];
channel.InsertChars(charsFound);
}
else
{
System.Diagnostics.Debug.WriteLine("No channel found yet. TEXT-MODE?");
}
}
}
public int[] ParseChars(int a, int b)
{
var charCodes = new List<int>();
int charCode1;
if (a >= 0x19)
{
charCode1 = a - 8;
}
else
{
charCode1 = a;
}
if (0x11 <= charCode1 && charCode1 <= 0x13)
{
// Special character
int oneCode;
if (charCode1 == 0x11)
{
oneCode = b + 0x50;
}
else if (charCode1 == 0x12)
{
oneCode = b + 0x70;
}
else
{
oneCode = b + 0x90;
}
charCodes.Add(oneCode);
}
else if (0x20 <= a && a <= 0x7f)
{
charCodes.Add(a);
if (b > 0)
{
charCodes.Add(b);
}
}
return charCodes.ToArray();
}
private static bool HasCmd(int ccData1, int ccData2)
{
return ((ccData1 == 0x14 || ccData1 == 0x1C) && (0x20 <= ccData2 && ccData2 <= 0x2F)) ||
((ccData1 == 0x17 || ccData1 == 0x1F) && (0x21 <= ccData2 && ccData2 <= 0x23));
}
public bool ParseCmd(int a, int b)
{
if (HasCmd(a, b))
{
// Duplicate CMD commands get skipped once
if (_lastCmdA == a && _lastCmdB == b)
{
_lastCmdA = null;
_lastCmdB = null;
return true;
}
int chNr;
if (a == 0x14 || a == 0x17)
{
chNr = 1;
}
else
{
chNr = 2; // (a == 0x1C || a== 0x1f)
}
Channels[chNr - 1].RunCmd(a, b);
CurrentChannelNumber = chNr;
_lastCmdA = a;
_lastCmdB = b;
return true;
}
return false;
}
public bool ParseMidRow(int a, int b)
{
if (((a == 0x11) || (a == 0x19)) && 0x20 <= b && b <= 0x2f)
{
int chNr;
if (a == 0x11)
{
chNr = 1;
}
else
{
chNr = 2;
}
var channel = Channels[chNr - 1];
channel.cc_MidRow(b);
return true;
}
return false;
}
private static bool HasPac(int ccData1, int ccData2)
{
return (((0x11 <= ccData1 && ccData1 <= 0x17) || (0x19 <= ccData1 && ccData1 <= 0x1F)) && (0x40 <= ccData2 && ccData2 <= 0x7F)) ||
((ccData1 == 0x10 || ccData1 == 0x18) && (0x40 <= ccData2 && ccData2 <= 0x5F));
}
public bool ParsePac(int a, int b)
{
if (HasPac(a, b))
{
var chNr = (a <= 0x17) ? 1 : 2;
int row;
if (0x40 <= b && b <= 0x5F)
{
row = (chNr == 1) ? Constants.Channel1RowsMap[a] : Constants.Channel2RowsMap[a];
}
else
{ // 0x60 <= b <= 0x7F
row = (chNr == 1) ? (Constants.Channel1RowsMap[a] + 1) : (Constants.Channel2RowsMap[a] + 1);
}
var pacData = InterpretPac(row, b);
var channel = Channels[chNr - 1];
channel.SetPac(pacData);
CurrentChannelNumber = chNr;
return true;
}
return false;
}
public PacData InterpretPac(int row, int b)
{
var pacData = new PacData
{
Color = null,
Italics = false,
Indent = null,
Underline = false,
Row = row,
};
int pacIndex;
if (b > 0x5F)
{
pacIndex = b - 0x60;
}
else
{
pacIndex = b - 0x40;
}
pacData.Underline = (pacIndex & 1) == 1;
if (pacIndex <= 0xd)
{
pacData.Color = Constants.PacDataColors[(int)Math.Floor(pacIndex / 2.0)];
}
else if (pacIndex <= 0xf)
{
pacData.Italics = true;
pacData.Color = Constants.ColorWhite;
}
else
{
pacData.Indent = (int)((Math.Floor((pacIndex - 0x10) / 2.0)) * 4);
}
return pacData;
}
public bool ParseBackgroundAttributes(int a, int b)
{
var bkgData = new SerializedPenState();
if (!HasBackgroundAttributes(a, b))
{
return false;
}
if (a == 0x10 || a == 0x18)
{
var index = (int)Math.Round(Math.Floor((b - 0x20) / 2.0));
bkgData.Background = Constants.PacDataColors[index];
if (b % 2 == 1)
{
bkgData.Background += "_semi";
}
}
else if (b == 0x2d)
{
bkgData.Background = Constants.ColorTransparent;
}
else
{
bkgData.Foreground = Constants.ColorBlack;
if (b == 0x2f)
{
bkgData.Underline = true;
}
}
var chNr = (a < 0x18) ? 1 : 2;
var channel = Channels[chNr - 1];
channel.SetBkgData(bkgData);
return true;
}
private static bool HasBackgroundAttributes(int ccData1, int ccData2)
{
return (((ccData1 == 0x10 || ccData1 == 0x18) && (0x20 <= ccData2 && ccData2 <= 0x2f)) ||
((ccData1 == 0x17 || ccData1 == 0x1f) && (0x2d <= ccData2 && ccData2 <= 0x2f)));
}
}
}

167
src/libse/Cea608/CcRow.cs Normal file
View File

@ -0,0 +1,167 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class CcRow
{
public int Position { get; set; }
public PenState CurrentPenState = new PenState();
public StyledUnicodeChar[] Chars { get; } =
{
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
new StyledUnicodeChar(),
};
public bool Equals(CcRow other)
{
for (var i = 0; i < Constants.ScreenColCount; i++)
{
if (!Chars[i].Equals(other.Chars[i]))
{
if (!Chars[i].Equals(other.Chars[i]))
{
return false;
}
}
}
return true;
}
public void Copy(CcRow other)
{
for (var i = 0; i < Constants.ScreenColCount; i++)
{
Chars[i].Copy(other.Chars[i]);
}
}
public int FirstNonEmpty()
{
for (var i = 0; i < Constants.ScreenColCount; i++)
{
if (!Chars[i].IsEmpty())
{
return i;
}
}
return -1;
}
public bool IsEmpty()
{
for (var i = 0; i < Constants.ScreenColCount; i++)
{
if (!Chars[i].IsEmpty())
{
return false;
}
}
return true;
}
public void MoveCursor(int relPos)
{
var newPos = Position + relPos;
if (relPos > 1)
{
for (var i = Position + 1; i < newPos + 1; i++)
{
Chars[i].SetPenState(CurrentPenState);
}
}
Position = newPos;
}
public void BackSpace()
{
MoveCursor(-1);
Chars[Position].SetChar(Constants.EmptyChar, CurrentPenState);
}
public void InsertChar(int b)
{
if (b >= 0x90)
{ // Extended char
BackSpace();
}
var ch = GetCharForByte(b);
Chars[Position].SetChar(ch, CurrentPenState);
MoveCursor(1);
}
/// <summary>
/// Get Unicode Character from CEA-608 byte code.
/// </summary>
public static string GetCharForByte(int byteValue)
{
if (Constants.ExtendedCharCodes.TryGetValue(byteValue, out var v))
{
return char.ConvertFromUtf32(v);
}
return char.ConvertFromUtf32(byteValue);
}
public void ClearFromPos(int startPos)
{
for (var i = startPos; i < Constants.ScreenColCount; i++)
{
Chars[i].Reset();
}
}
public void Clear()
{
ClearFromPos(0);
Position = 0;
CurrentPenState.Reset();
}
public void ClearToEndOfRow()
{
ClearFromPos(Position);
}
public void SetPenStyles(SerializedPenState styles)
{
CurrentPenState.SetStyles(styles);
var currentChar = Chars[Position];
currentChar.SetPenState(CurrentPenState);
}
}
}

View File

@ -0,0 +1,11 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class CcStyle
{
public string Foreground { get; set; }
public bool? Underline { get; set; }
public bool? Italics { get; set; }
public string Background { get; set; }
public bool? Flash { get; set; }
}
}

View File

@ -0,0 +1,365 @@
using System;
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class Cea608Channel
{
public CaptionScreen DisplayedMemory { get; set; }
public CaptionScreen NonDisplayedMemory { get; set; }
public CaptionScreen LastOutputScreen { get; set; }
public CaptionScreen WriteScreen { get; set; }
public CcRow CurrentRollUpRow { get; set; }
public int? CueStartTime { get; set; }
public string Mode { get; set; }
public int ChannelNumber { get; set; }
public CcDataC608Parser Parser { get; set; }
public Cea608Channel()
{
DisplayedMemory = new CaptionScreen();
NonDisplayedMemory = new CaptionScreen();
LastOutputScreen = new CaptionScreen();
}
public Cea608Channel(int chNr, CcDataC608Parser parser)
{
Parser = parser;
ChannelNumber = chNr;
DisplayedMemory = new CaptionScreen();
NonDisplayedMemory = new CaptionScreen();
LastOutputScreen = new CaptionScreen();
CurrentRollUpRow = DisplayedMemory.Rows[Constants.ScreenRowCount - 1];
WriteScreen = DisplayedMemory;
CueStartTime = null;
}
private void CmdMap(int cmd)
{
switch (cmd)
{
case 0x20:
cc_RCL();
break;
case 0x21:
cc_BS();
break;
case 0x22:
cc_AOF();
break;
case 0x23:
cc_AON();
break;
case 0x24:
cc_DER();
break;
case 0x25:
cc_RU(2);
break;
case 0x26:
cc_RU(3);
break;
case 0x27:
cc_RU(4);
break;
case 0x28:
cc_FON();
break;
case 0x29:
cc_RDC();
break;
case 0x2A:
cc_TR();
break;
case 0x2B:
cc_RTD();
break;
case 0x2C:
cc_EDM();
break;
case 0x2D:
cc_CR();
break;
case 0x2E:
cc_ENM();
break;
case 0x2F:
cc_EOC();
break;
default:
Console.WriteLine("Command not found");
break;
}
}
public void Reset()
{
Mode = null;
DisplayedMemory.Reset();
NonDisplayedMemory.Reset();
LastOutputScreen.Reset();
CurrentRollUpRow = DisplayedMemory.Rows[Constants.ScreenRowCount - 1];
WriteScreen = DisplayedMemory;
CueStartTime = null;
}
public void SetPac(PacData pacData)
{
WriteScreen.SetPac(pacData);
}
public void RunCmd(int ccData1, int ccData2)
{
if (ccData1 == 0x14 || ccData1 == 0x1C)
{
CmdMap(ccData2);
}
else
{ // a == 0x17 || a == 0x1F
cc_TO(ccData2 - 0x20);
}
}
public void SetBkgData(SerializedPenState bkgData)
{
WriteScreen.SetBkgData(bkgData);
}
public void SetMode(string newMode)
{
if (newMode == Mode)
{
return;
}
Mode = newMode;
if (Mode == "MODE_POP-ON")
{
WriteScreen = NonDisplayedMemory;
}
else
{
WriteScreen = DisplayedMemory;
WriteScreen.Reset();
}
if (Mode != "MODE_ROLL-UP")
{
DisplayedMemory.NumberOfRollUpRows = null;
NonDisplayedMemory.NumberOfRollUpRows = null;
}
Mode = newMode;
}
public void InsertChars(int[] chars)
{
foreach (var ch in chars)
{
WriteScreen.InsertChar(ch);
}
if (Mode == "MODE_PAINT-ON" || Mode == "MODE_ROLL-UP")
{
OutputDataUpdate();
}
}
/// <summary>
/// Resume Caption Loading (switch mode to Pop On).
/// </summary>
public void cc_RCL()
{
SetMode("MODE_POP-ON");
}
/// <summary>
/// BackSpace.
/// </summary>
public void cc_BS()
{
if (Mode == "MODE_TEXT")
{
return;
}
WriteScreen.BackSpace();
if (WriteScreen == DisplayedMemory)
{
OutputDataUpdate();
}
}
/// <summary>
/// Reserved (formerly Alarm Off).
/// </summary>
public void cc_AOF()
{
}
/// <summary>
/// Reserved (formerly Alarm On).
/// </summary>
public void cc_AON()
{
}
/// <summary>
/// Delete to End of Row.
/// </summary>
public void cc_DER()
{
WriteScreen.ClearToEndOfRow();
OutputDataUpdate();
}
/// <summary>
/// Roll-Up Captions-2,3,or 4 Rows.
/// </summary>
public void cc_RU(int nrRows)
{
WriteScreen = DisplayedMemory;
SetMode("MODE_ROLL-UP");
WriteScreen.SetRollUpRows(nrRows);
}
/// <summary>
/// Flash On.
/// </summary>
public void cc_FON()
{
WriteScreen.SetPen(new PacData { Flash = true });
}
/// <summary>
/// Resume Direct Captioning (switch mode to PaintOn).
/// </summary>
public void cc_RDC()
{
SetMode("MODE_PAINT-ON");
}
/// <summary>
/// Text Restart in text mode (not supported, however).
/// </summary>
public void cc_TR()
{
SetMode("MODE_TEXT");
}
/// <summary>
/// Resume Text Display in Text mode (not supported, however)
/// </summary>
public void cc_RTD()
{
SetMode("MODE_TEXT");
}
/// <summary>
/// Erase Displayed Memory.
/// </summary>
public void cc_EDM()
{
DisplayedMemory.Reset();
OutputDataUpdate();
}
/// <summary>
/// Carriage Return.
/// </summary>
public void cc_CR()
{
WriteScreen.RollUp();
OutputDataUpdate(true);
}
/// <summary>
/// Erase Non-Displayed Memory.
/// </summary>
public void cc_ENM()
{
NonDisplayedMemory.Reset();
}
/// <summary>
/// End of Caption (Flip Memories).
/// </summary>
public void cc_EOC()
{
if (Mode == "MODE_POP-ON")
{
(DisplayedMemory, NonDisplayedMemory) = (NonDisplayedMemory, DisplayedMemory);
WriteScreen = NonDisplayedMemory;
}
OutputDataUpdate();
}
/// <summary>
/// Tab Offset 1,2, or 3 columns
/// </summary>
public void cc_TO(int nrCols)
{
WriteScreen.MoveCursor(nrCols);
}
/// <summary>
/// Parse MIDROW command
/// </summary>
public void cc_MidRow(int secondByte)
{
//
var styles = new SerializedPenState { Flash = false };
styles.Underline = secondByte % 2 == 1;
styles.Italics = secondByte >= 0x2e;
if (!styles.Italics.Value)
{
var colorIndex = (int)Math.Floor(secondByte / 2.0) - 0x10;
styles.Foreground = Constants.PacDataColors[colorIndex];
}
else
{
styles.Foreground = Constants.ColorWhite;
}
WriteScreen.SetPen(styles);
}
public void OutputDataUpdate(bool rolling = false)
{
var t = Parser.LastTime;
if (t == null)
{
return;
}
if (CueStartTime == null && !DisplayedMemory.IsEmpty())
{ // Start of a new cue
CueStartTime = t;
}
else if (!DisplayedMemory.Equals(LastOutputScreen))
{
if (CueStartTime != null && !LastOutputScreen.IsEmpty())
{
Parser.DisplayScreen?.Invoke(new DataOutput
{
Channel = ChannelNumber,
Roll = rolling,
Start = CueStartTime ?? 0,
End = t ?? 0,
Screen = LastOutputScreen.Serialize(),
});
}
CueStartTime = DisplayedMemory.IsEmpty() ? null : t;
}
LastOutputScreen.Copy(DisplayedMemory);
}
}
}

View File

@ -0,0 +1,161 @@
using System.Collections.Generic;
namespace Nikse.SubtitleEdit.Core.Cea608
{
public static class Constants
{
public const string ColorWhite = "white";
public const string ColorGreen = "green";
public const string ColorBlue = "blue";
public const string ColorCyan = "cyan";
public const string ColorRed = "red";
public const string ColorYellow = "yellow";
public const string ColorMagenta = "magenta";
public const string ColorBlack = "black";
public const string ColorTransparent = "transparent";
public const string EmptyChar = " ";
public static string[] PacDataColors = new string[]
{
ColorWhite,
ColorGreen,
ColorBlue,
ColorCyan,
ColorRed,
ColorYellow,
ColorMagenta,
ColorBlack,
ColorTransparent
};
public const int ScreenRowCount = 15;
public const int ScreenColCount = 32;
public static Dictionary<int, int> ExtendedCharCodes = new Dictionary<int, int>
{
{ 0x2a, 0xe1 }, // lowercase a, acute accent
{ 0x5c, 0xe9 }, // lowercase e, acute accent
{ 0x5e, 0xed }, // lowercase i, acute accent
{ 0x5f, 0xf3 }, // lowercase o, acute accent
{ 0x60, 0xfa }, // lowercase u, acute accent
{ 0x7b, 0xe7 }, // lowercase c with cedilla
{ 0x7c, 0xf7 }, // division symbol
{ 0x7d, 0xd1 }, // uppercase N tilde
{ 0x7e, 0xf1 }, // lowercase n tilde
{ 0x7f, 0x2588 }, // Full block
// THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
// THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F
// THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES
{ 0x80, 0xae }, // Registered symbol (R)
{ 0x81, 0xb0 }, // degree sign
{ 0x82, 0xbd }, // 1/2 symbol
{ 0x83, 0xbf }, // Inverted (open) question mark
{ 0x84, 0x2122 }, // Trademark symbol (TM)
{ 0x85, 0xa2 }, // Cents symbol
{ 0x86, 0xa3 }, // Pounds sterling
{ 0x87, 0x266a }, // Music 8'th note
{ 0x88, 0xe0 }, // lowercase a, grave accent
{ 0x89, 0x20 }, // transparent space (regular)
{ 0x8a, 0xe8 }, // lowercase e, grave accent
{ 0x8b, 0xe2 }, // lowercase a, circumflex accent
{ 0x8c, 0xea }, // lowercase e, circumflex accent
{ 0x8d, 0xee }, // lowercase i, circumflex accent
{ 0x8e, 0xf4 }, // lowercase o, circumflex accent
{ 0x8f, 0xfb }, // lowercase u, circumflex accent
// THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
// THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F
{ 0x90, 0xc1 }, // capital letter A with acute
{ 0x91, 0xc9 }, // capital letter E with acute
{ 0x92, 0xd3 }, // capital letter O with acute
{ 0x93, 0xda }, // capital letter U with acute
{ 0x94, 0xdc }, // capital letter U with diaresis
{ 0x95, 0xfc }, // lowercase letter U with diaeresis
{ 0x96, 0x2018 }, // opening single quote
{ 0x97, 0xa1 }, // inverted exclamation mark
{ 0x98, 0x2a }, // asterisk
{ 0x99, 0x2019 }, // closing single quote
{ 0x9a, 0x2501 }, // box drawings heavy horizontal
{ 0x9b, 0xa9 }, // copyright sign
{ 0x9c, 0x2120 }, // Service mark
{ 0x9d, 0x2022 }, // (round) bullet
{ 0x9e, 0x201c }, // Left double quotation mark
{ 0x9f, 0x201d }, // Right double quotation mark
{ 0xa0, 0xc0 }, // uppercase A, grave accent
{ 0xa1, 0xc2 }, // uppercase A, circumflex
{ 0xa2, 0xc7 }, // uppercase C with cedilla
{ 0xa3, 0xc8 }, // uppercase E, grave accent
{ 0xa4, 0xca }, // uppercase E, circumflex
{ 0xa5, 0xcb }, // capital letter E with diaresis
{ 0xa6, 0xeb }, // lowercase letter e with diaresis
{ 0xa7, 0xce }, // uppercase I, circumflex
{ 0xa8, 0xcf }, // uppercase I, with diaresis
{ 0xa9, 0xef }, // lowercase i, with diaresis
{ 0xaa, 0xd4 }, // uppercase O, circumflex
{ 0xab, 0xd9 }, // uppercase U, grave accent
{ 0xac, 0xf9 }, // lowercase u, grave accent
{ 0xad, 0xdb }, // uppercase U, circumflex
{ 0xae, 0xab }, // left-pointing double angle quotation mark
{ 0xaf, 0xbb }, // right-pointing double angle quotation mark
// THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
// THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F
{ 0xb0, 0xc3 }, // Uppercase A, tilde
{ 0xb1, 0xe3 }, // Lowercase a, tilde
{ 0xb2, 0xcd }, // Uppercase I, acute accent
{ 0xb3, 0xcc }, // Uppercase I, grave accent
{ 0xb4, 0xec }, // Lowercase i, grave accent
{ 0xb5, 0xd2 }, // Uppercase O, grave accent
{ 0xb6, 0xf2 }, // Lowercase o, grave accent
{ 0xb7, 0xd5 }, // Uppercase O, tilde
{ 0xb8, 0xf5 }, // Lowercase o, tilde
{ 0xb9, 0x7b }, // Open curly brace
{ 0xba, 0x7d }, // Closing curly brace
{ 0xbb, 0x5c }, // Backslash
{ 0xbc, 0x5e }, // Caret
{ 0xbd, 0x5f }, // Underscore
{ 0xbe, 0x7c }, // Pipe (vertical line)
{ 0xbf, 0x223c }, // Tilde operator
{ 0xc0, 0xc4 }, // Uppercase A, umlaut
{ 0xc1, 0xe4 }, // Lowercase A, umlaut
{ 0xc2, 0xd6 }, // Uppercase O, umlaut
{ 0xc3, 0xf6 }, // Lowercase o, umlaut
{ 0xc4, 0xdf }, // Esszett (sharp S)
{ 0xc5, 0xa5 }, // Yen symbol
{ 0xc6, 0xa4 }, // Generic currency sign
{ 0xc7, 0x2503 }, // Box drawings heavy vertical
{ 0xc8, 0xc5 }, // Uppercase A, ring
{ 0xc9, 0xe5 }, // Lowercase A, ring
{ 0xca, 0xd8 }, // Uppercase O, stroke
{ 0xcb, 0xf8 }, // Lowercase o, strok
{ 0xcc, 0x250f }, // Box drawings heavy down and right
{ 0xcd, 0x2513 }, // Box drawings heavy down and left
{ 0xce, 0x2517 }, // Box drawings heavy up and right
{ 0xcf, 0x251b } // Box drawings heavy up and left
};
public static Dictionary<int, int> Channel1RowsMap = new Dictionary<int, int>
{
{ 0x11, 1 },
{ 0x12, 3 },
{ 0x15, 5 },
{ 0x16, 7 },
{ 0x17, 9 },
{ 0x10, 11 },
{ 0x13, 12 },
{ 0x14, 14 }
};
public static Dictionary<int, int> Channel2RowsMap = new Dictionary<int, int>
{
{ 0x19, 1 },
{ 0x1A, 3 },
{ 0x1D, 5 },
{ 0x1E, 7 },
{ 0x1F, 9 },
{ 0x18, 11 },
{ 0x1B, 12 },
{ 0x1C, 14 }
};
}
}

View File

@ -0,0 +1,11 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class DataOutput
{
public int Channel { get; set; }
public bool Roll { get; set; }
public int End { get; set; }
public int Start { get; set; }
public SerializedRow[] Screen { get; set; }
}
}

View File

@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.IO;
namespace Nikse.SubtitleEdit.Core.Cea608
{
public static class GetCcDataHelper
{
public static List<CcData> GetCcData(Stream fs, uint startPos, ulong size)
{
var fieldData = new List<CcData>();
for (var i = startPos; i < startPos + size - 5; i++)
{
var buffer = new byte[4];
fs.Seek(i, SeekOrigin.Begin);
fs.Read(buffer, 0, buffer.Length);
var nalSize = GetUInt32(buffer, 0);
var flag = fs.ReadByte();
if (IsRbspNalUnitType(flag & 0x1F))
{
//parseCCDataFromSEI(getSEIData(raw, i + 5, i + nalSize + 3), fieldData);
var seiData = GetSeiData(fs, i + 5, i + nalSize + 3);
ParseCcDataFromSei(seiData, fieldData);
}
i += nalSize + 3;
}
return fieldData;
}
private static bool IsRbspNalUnitType(int unitType)
{
return unitType == 0x06;
}
private static byte[] GetSeiData(Stream fs, uint startPos, uint endPos)
{
var data = new List<byte>();
var buffer = new byte[endPos - startPos];
fs.Seek(startPos, SeekOrigin.Begin);
fs.Read(buffer, 0, buffer.Length);
for (var x = startPos; x < endPos; x++)
{
var idx = x - startPos;
if (x + 2 < endPos && buffer[idx] == 0x00 && buffer[idx + 1] == 0x00 && buffer[idx + 2] == 0x03)
{
data.Add(0x00);
data.Add(0x00);
x += 2;
}
else
{
data.Add(buffer[idx]);
}
}
return data.ToArray();
}
private static void ParseCcDataFromSei(byte[] buffer, List<CcData> fieldData)
{
var x = 0;
while (x < buffer.Length)
{
var payloadType = 0;
var payloadSize = 0;
int now;
do
{
now = buffer[x++];
payloadType += now;
} while (now == 0xFF);
do
{
now = buffer[x++];
payloadSize += now;
} while (now == 0xFF);
if (IsStartOfCcDataHeader(payloadType, buffer, x))
{
var pos = x + 10;
var ccCount = pos + (buffer[pos - 2] & 0x1F) * 3;
for (var i = pos; i < ccCount; i += 3)
{
var b = buffer[i];
if ((b & 0x4) > 0)
{
var ccType = b & 0x3;
if (IsCcType(ccType))
{
var ccData1 = buffer[i + 1];
var ccData2 = buffer[i + 2];
if (IsNonEmptyCcData(ccData1, ccData2))
{
fieldData.Add(new CcData(ccType, ccData1, ccData2));
}
}
}
}
}
x += payloadSize;
}
}
private static bool IsCcType(int type)
{
return type == 0 || type == 1;
}
private static bool IsNonEmptyCcData(int ccData1, int ccData2)
{
return (ccData1 & 0x7f) > 0 || (ccData2 & 0x7f) > 0;
}
private static bool IsStartOfCcDataHeader(int payloadType, byte[] buffer, int pos)
{
return payloadType == 4 &&
GetUInt32(buffer, pos) == 3036688711 &&
GetUInt32(buffer, pos + 4) == 1094267907;
}
private static uint GetUInt32(byte[] buffer, int pos)
{
return (uint)((buffer[pos + 0] << 24) + (buffer[pos + 1] << 16) + (buffer[pos + 2] << 8) + buffer[pos + 3]);
}
}
}

View File

@ -0,0 +1,9 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class PacData: SerializedPenState
{
public int Row { get; set; }
public string Color { get; set; }
public int? Indent { get; set; }
}
}

View File

@ -0,0 +1,93 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class PenState
{
public string Foreground { get; set; } = Constants.ColorWhite;
public bool Underline;
public bool Italics { get; set; }
public string Background { get; set; } = Constants.ColorBlack;
public bool Flash { get; set; }
public PenState()
{
}
public PenState(string foreground, bool underline, bool italics, string background, bool flash1)
{
Foreground = foreground;
Underline = underline;
Italics = italics;
Background = background;
Flash = flash1;
}
public void Reset()
{
Foreground = Constants.ColorWhite;
Underline = false;
Italics = false;
Background = Constants.ColorBlack;
Flash = false;
}
public CcStyle Serialize()
{
return new CcStyle
{
Foreground = Foreground,
Underline = Underline,
Italics = Italics,
Flash = Flash,
Background = Background
};
}
public void Copy(PenState newPenState)
{
Foreground = newPenState.Foreground;
Underline = newPenState.Underline;
Italics = newPenState.Italics;
Background = newPenState.Background;
Flash = newPenState.Flash;
}
public bool Equals(PenState other)
{
return Foreground == other.Foreground &&
Underline == other.Underline &&
Italics == other.Italics &&
Background == other.Background &&
Flash == other.Flash;
}
public bool IsDefault()
{
return Foreground == Constants.ColorWhite && !Underline && !Italics && Background == Constants.ColorBlack && !Flash;
}
public void SetStyles(SerializedPenState styles)
{
if (styles.Foreground != null)
{
Foreground = styles.Foreground;
}
if (styles.Underline != null)
{
Underline = styles.Underline.Value;
}
if (styles.Background != null)
{
Background = styles.Background;
}
if (styles.Flash != null)
{
Flash = styles.Flash.Value;
}
if (styles.Italics != null)
{
Italics = styles.Italics.Value;
}
}
}
}

View File

@ -0,0 +1,11 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class SerializedPenState
{
public string Foreground { get; set; }
public bool? Underline { get; set; }
public bool? Italics { get; set; }
public string Background { get; set; }
public bool? Flash { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class SerializedRow
{
public int Row { get; set; }
/// <summary>
/// Column indentation.
/// </summary>
public int Position { get; set; }
public CcStyle Style { get; set; }
public SerializedStyledUnicodeChar[] Columns { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class SerializedStyledUnicodeChar
{
public CcStyle Style { get; set; }
public string Character { get; set; }
}
}

View File

@ -0,0 +1,52 @@
namespace Nikse.SubtitleEdit.Core.Cea608
{
public class StyledUnicodeChar
{
public PenState PenState { get; set; }
public string Uchar { get; set; } = Constants.EmptyChar;
public string Foreground { get; set; }
public bool? Underline { get; set; }
public bool? Italics { get; set; }
public string Background { get; set; }
public bool? Flash { get; set; }
public StyledUnicodeChar()
{
PenState = new PenState(null, false, false, null, false);
}
public void Reset()
{
Uchar = Constants.EmptyChar;
PenState.Reset();
}
public void SetChar(string uchar, PenState newPenState)
{
Uchar = uchar;
PenState.Copy(newPenState);
}
public void SetPenState(PenState newPenState)
{
PenState.Copy(newPenState);
}
public bool Equals(StyledUnicodeChar other)
{
return Uchar == other.Uchar && PenState.Equals(other.PenState);
}
public void Copy(StyledUnicodeChar newChar)
{
Uchar = newChar.Uchar;
PenState.Copy(newChar.PenState);
}
public bool IsEmpty()
{
return Uchar == Constants.EmptyChar && PenState.IsDefault();
}
}
}

View File

@ -8,6 +8,7 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
{
public byte[] Buffer;
public ulong Position;
public ulong StartPosition;
public string Name;
public ulong Size;
@ -21,6 +22,11 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
return (uint)((Buffer[index] << 24) + (Buffer[index + 1] << 16) + (Buffer[index + 2] << 8) + Buffer[index + 3]);
}
public int GetInt(int index)
{
return (int)((Buffer[index] << 24) + (Buffer[index + 1] << 16) + (Buffer[index + 2] << 8) + Buffer[index + 3]);
}
public ulong GetUInt64(int index)
{
return (ulong)Buffer[index] << 56 | (ulong)Buffer[index + 1] << 48 | (ulong)Buffer[index + 2] << 40 | (ulong)Buffer[index + 3] << 32 |
@ -60,6 +66,11 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
internal bool InitializeSizeAndName(System.IO.Stream fs)
{
if (StartPosition == 0)
{
StartPosition = (ulong)fs.Position -8;
}
Buffer = new byte[8];
var bytesRead = fs.Read(Buffer, 0, Buffer.Length);
if (bytesRead < Buffer.Length)

View File

@ -30,11 +30,11 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
if (version == 1)
{
BaseMediaDecodeTime = GetUInt(8);
BaseMediaDecodeTime = GetUInt64(4);
}
else
{
BaseMediaDecodeTime = GetUInt64(8);
BaseMediaDecodeTime = GetUInt(4);
}
}
}

View File

@ -30,6 +30,10 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
{
Tfhd = new Tfhd(fs, Size);
}
else if (Name == "tfdt")
{
Tfdt = new Tfdt(fs, Size);
}
fs.Seek((long)Position, SeekOrigin.Begin);
}

View File

@ -11,6 +11,7 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
public class Trun : Box
{
public List<TimeSegment> Samples { get; set; }
public uint? DataOffset { get; set; }
public Trun(Stream fs, ulong maximumLength)
{
@ -28,6 +29,7 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
}
var versionAndFlags = GetUInt(0);
var version = versionAndFlags >> 24;
var flags = versionAndFlags & 0xFFFFFF;
var sampleCount = GetUInt(4);
if (sampleCount == 0)
@ -35,6 +37,18 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
return;
}
if ((flags & 0x1) > 0)
{
Buffer = new byte[4];
readCount = fs.Read(Buffer, 0, Buffer.Length);
if (readCount < Buffer.Length)
{
return;
}
DataOffset = GetUInt(0);
}
var sampleLength = Math.Min(sampleCount * 16 + 24, maximumLength);
Buffer = new byte[sampleLength];
readCount = fs.Read(Buffer, 0, Buffer.Length);
@ -45,11 +59,6 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
var pos = 0;
if ((flags & 0x000001) > 0)
{
pos += 4;
}
// skip "first_sample_flags" if present
if ((flags & 0x000004) > 0)
{
@ -99,7 +108,7 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4.Boxes
// read "sample_time_offset" if present
if ((flags & 0x000800) > 0)
{
sample.TimeOffset = GetUInt(pos);
sample.TimeOffset = version == 1 ? GetInt(pos) : (long)GetUInt(pos);
pos += 4;
}

View File

@ -107,7 +107,7 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4
if (presentation.Duration.HasValue)
{
var startTime = presentation.TimeOffset.HasValue ? presentation.BaseMediaDecodeTime + presentation.TimeOffset.Value : presentation.BaseMediaDecodeTime;
var startTime = presentation.TimeOffset.HasValue ? (ulong)((long)presentation.BaseMediaDecodeTime + presentation.TimeOffset.Value) : presentation.BaseMediaDecodeTime;
var currentTime = startTime + presentation.Duration.Value;
// The payload can be null as that would mean that it was a VTTE and

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Nikse.SubtitleEdit.Core.Cea608;
namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4
{
@ -21,6 +22,9 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4
public Subtitle VttcSubtitle { get; private set; }
public string VttcLanguage { get; private set; }
public Subtitle TrunCea608Subtitle { get; private set; }
private List<Cea608.CcData> _trunCea608CcData = new List<Cea608.CcData>();
public List<Trak> GetSubtitleTracks()
{
var list = new List<Trak>();
@ -161,6 +165,36 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4
else if (Name == "moof")
{
Moof = new Moof(fs, Position);
if (Moof.Traf?.Trun?.DataOffset != null && Moof.Traf.Tfdt != null)
{
var dts = Moof.Traf.Tfdt.BaseMediaDecodeTime;
var startPosition = (uint)(Moof.StartPosition + Moof.Traf.Trun.DataOffset.Value);
for (var index = 0; index < Moof.Traf.Trun.Samples.Count; index++)
{
var sample = Moof.Traf.Trun.Samples[index];
if (sample.Size.HasValue)
{
var ccData = GetCcDataHelper.GetCcData(fs, startPosition, sample.Size.Value);
if (ccData.Count > 0)
{
if (sample.TimeOffset.HasValue)
{
ccData[0].Time = (ulong)((long)dts + sample.TimeOffset.Value);
}
_trunCea608CcData.Add(ccData[0]); //TODO: can there be more than one?
}
startPosition += sample.Size.Value;
}
if (sample.Duration.HasValue)
{
dts += sample.Duration.Value;
}
}
}
}
else if (Name == "mdat" && Moof != null && Moof?.Traf?.Trun?.Samples?.Count > 0)
{
@ -216,6 +250,52 @@ namespace Nikse.SubtitleEdit.Core.ContainerFormats.Mp4
var merged = MergeLinesSameTextUtils.MergeLinesWithSameTextInSubtitle(VttcSubtitle, false, 250);
VttcSubtitle = merged;
}
CheckForTrunCea608();
}
private void CheckForTrunCea608()
{
try
{
TrunCea608Subtitle = new Subtitle();
var sortedData = _trunCea608CcData.OrderBy(p => p.Time).ToList();
var parser = new CcDataC608Parser();
parser.DisplayScreen += DisplayScreen;
foreach (var cc in sortedData)
{
parser.AddData((int)cc.Time, new[] { cc.Data1, cc.Data2 });
}
}
catch (Exception e)
{
SeLogger.Error(e, "Error while parsing MP4 TRUN CEA 608");
}
}
private void DisplayScreen(DataOutput data)
{
var timeScale = Moov?.Mvhd?.TimeScale ?? 1000.0;
var startMs = data.Start / timeScale * 1000.0;
var endMs = data.End / timeScale * 1000.0;
var p = new Paragraph(GetText(data.Screen), startMs, endMs);
TrunCea608Subtitle.Paragraphs.Add(p);
}
private static string GetText(SerializedRow[] dataScreen)
{
var sb = new StringBuilder();
foreach (var row in dataScreen)
{
foreach (var column in row.Columns)
{
sb.Append(column.Character);
}
sb.AppendLine();
}
return sb.ToString().Trim();
}
private void ReadVttWithSize(Mdat mdat, List<TimeSegment> trunSamples, ref double timeTotalMs)

View File

@ -4,7 +4,7 @@
{
public uint? Duration { get; set; }
public uint? Size { get; set; }
public uint? TimeOffset { get; set; }
public long? TimeOffset { get; set; }
public ulong BaseMediaDecodeTime { get; set; }
}
}

View File

@ -1198,7 +1198,7 @@ namespace Nikse.SubtitleEdit.Core.Forms
if (Settings.RemoveIfOnlyMusicSymbols)
{
if (text.Replace('♪', ' ').Replace('♫', ' ').Trim().Length == 0)
if (string.IsNullOrWhiteSpace(HtmlUtil.RemoveHtmlTags(text, true).RemoveChar('♪', '♫')))
{
return string.Empty;
}

View File

@ -133,15 +133,24 @@ namespace Nikse.SubtitleEdit.Forms.AudioToText
public static void InitializeWhisperEngines(NikseComboBox cb)
{
cb.Items.Clear();
var is64BitOs = IntPtr.Size * 8 == 64;
if (!is64BitOs)
{
cb.Items.Add(WhisperChoice.Cpp);
cb.SelectedIndex = 0;
return;
}
var engines = new List<string> { WhisperChoice.OpenAi };
if (Configuration.IsRunningOnWindows && IntPtr.Size * 8 == 64)
if (Configuration.IsRunningOnWindows)
{
engines.Add(WhisperChoice.PurfviewFasterWhisper);
engines.Add(WhisperChoice.ConstMe);
}
engines.Add(WhisperChoice.Cpp);
engines.Add(WhisperChoice.CTranslate2);
// engines.Add(WhisperChoice.StableTs);
engines.Add(WhisperChoice.StableTs);
engines.Add(WhisperChoice.WhisperX);
foreach (var engine in engines)

View File

@ -15599,6 +15599,23 @@ namespace Nikse.SubtitleEdit.Forms
return true;
}
if (mp4Parser.TrunCea608Subtitle?.Paragraphs.Count > 0)
{
MakeHistoryForUndo(_language.BeforeImportFromMatroskaFile);
_subtitleListViewIndex = -1;
FileNew();
_subtitle = mp4Parser.TrunCea608Subtitle;
UpdateSourceView();
SubtitleListview1.Fill(_subtitle, _subtitleOriginal);
_subtitleListViewIndex = -1;
SubtitleListview1.FirstVisibleIndex = -1;
SubtitleListview1.SelectIndexAndEnsureVisible(0, true);
_fileName = Utilities.GetPathAndFileNameWithoutExtension(fileName) + GetCurrentSubtitleFormat().Extension;
_converted = true;
SetTitle();
return true;
}
MessageBox.Show(_language.NoSubtitlesFound);
return false;
}