diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..726d15d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +/releases/ + +nbactions.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce226f9 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# 内容物について + +ZIPファイルを適当な場所に解凍すると以下のファイルが展開されます。 + +フォルダ bin +ファイル bindmov.cmd +ファイル clean.bat +ファイル exec.cmd +ファイル png2mov.cmd +ファイル README.txt + +フォルダbinの中には、制御用のファイル lyricsmovie-1.0.0-jar-with-dependencies.jar +が入っています。 + +# セットアップについて + +1)フォルダ c:\misc\lyricsmovie を作って、内容物を全部移動してください。 + +cドライブの中にmiscフォルダを作り、その中にlyricsmoveフォルダを作り、さらにその中に内容物を全部移動します。 + +2)ffmpegをウェブサイトから入手し配置します。 + +ffmpegは、https://gyan.dev/ffmpeg/builds/ にあるffmpeg-relase-full.7z がおすすめです。 +あまり知られていない圧縮形式ですが、lhaplusで解凍出来ます。 + +ffmpegも同様にフォルダ c:\misc\ffmpeg を作って、この中に配置します。 +c:\misc\ffmpeg\binの中にffmpeg.exe, ffplay.exe, ffprobe.exeが配置されるようにしてください。 + +# 使い方について + +このプログラムは次の3つの作業ステップを想定しています。 + +ステップ1.Musicxmlからテロップの下書きファイルを作る。 +ステップ2.下書きファイルを編集して、ひらがなに対応する文を追加する。 +ステップ3.編集の済んだ下書きファイルから動画ファイルを作る + +●1.Musicxmlから下書きファイルを作る。 +・lyricsmoveフォルダにmusicxmlファイルをコピーします。 +・exec.cmd にコピーしたmusicxmlファイルをドラッグドロップします。 + いくつかファイルが作られますが、json.txtファイルが下書きファイルです。 + +例) AAAAA.musicxmlをドロップすると、AAAAA.json, AAAAA.mid01.json, AAAAA.mid02.json, AAAAA.json.txt +が作られる。この中のAAAAA.json.txtが下書きファイル。 + +●2.下書きファイルを編集する + メモ帳などで開くと、lyricの項目に、ひらがなで歌詞が書いてあると思います。 + これに対応する通常の歌詞を、captionの項目に記入します。 + ※他の項目は動画の生成に使うパラメータなので触らないでください。 + +例) 下書きファイル一部抜粋 +{"NOTES": [ + { + "sound_start": 0, + "dulation_start": 0, + "sound_length": 4194.9152542372885, + "lyric": "いつもーとかわらぬーけだるいあさがはじまあった", + "sound_end": 4194.9152542372885, + "caption": "いつもと変わらぬけだるい朝が始まった",  <--追記 + "dulation_end": 132, + "dulation_length": 132 + }, +   + + 追記が終わったら、上書き保存してください。 + ※下書きファイルは必ずUTF-8で保存するようにしてください。 + ※違う形式を選ぶとテロップ動画が文字化けします。 + +●3.下書きファイルから動画を生成する。 + 編集が終わったら、下書きファイルをexec.cmdにドラッグドロップします。 + 文節ごとに動画が作られ、最後に全部の文節を結合した動画ファイルが、 + ○○.bind.mkvというファイル名で作られます。 + + 例) AAAAA.json.txt → AAAAA.json.bind.mkv + + +■その他 +clean.cmdというコマンドファイルで、作業ファイルを一括で消すことが出来ます。 +その際に、下書きファイルとmusicxmlは消さないので、必要に応じて手動で消してください。 diff --git a/bindmov.cmd b/bindmov.cmd new file mode 100644 index 0000000..a138c2c --- /dev/null +++ b/bindmov.cmd @@ -0,0 +1,7 @@ +set BIN=c:\misc\ffmpeg\bin +set DST=%~n1.mkv + +echo %BIN%\ffmpeg -f concat -i bindlist.txt -c copy %DST% + +%BIN%\ffmpeg -y -f concat -i bindlist.txt -c copy %DST% + diff --git a/clean.cmd b/clean.cmd new file mode 100644 index 0000000..04ebe90 --- /dev/null +++ b/clean.cmd @@ -0,0 +1,3 @@ +del *.png /q +del *.json /q +del *.mkv /q \ No newline at end of file diff --git a/exec.cmd b/exec.cmd new file mode 100644 index 0000000..b57ee6c --- /dev/null +++ b/exec.cmd @@ -0,0 +1,19 @@ +;�������牺�͐G��Ȃ� + +set CMD=java -jar target\lyricsmovie-1.0.0-jar-with-dependencies.jar +set WORKDIR=%~dp0 +set SRC=%~nx1 +set FFMPEG=-ffmpeg png2mov.cmd +set BIND=-bind bindmov.cmd + +;�������牺�͕K�v�ɉ����ĕύX�”\ + +set MODE=-smart + +echo %CMD% -w %WORKDIR% -i %SRC% %FFMPEG% %BIND% %MODE% + +%CMD% -w %WORKDIR% -i %SRC% %FFMPEG% %BIND% %MODE% + +pause + + diff --git a/png2mov.cmd b/png2mov.cmd new file mode 100644 index 0000000..c239445 --- /dev/null +++ b/png2mov.cmd @@ -0,0 +1,14 @@ +@echo off +rem +rem �����̑���12�̔{�����t���[�����[�g�Ɏg���ƁA��Ԑ����덷�����Ȃ��B +rem + +set BIN=c:\misc\ffmpeg\bin +set SRC=%~nf1 +set DST=%~n1.mkv +set TIME=%~n2 + +rem echo %BIN%\ffmpeg -y -loop 1 -i %SRC% -vf scale=1920:1080 -profile:v 4444 -c:v prores_ks -pix_fmt rgba -t %TIME% %DST% +rem %BIN%\ffmpeg -y -loop 1 -i %SRC% -vf scale=1920:1080 -profile:v 4444 -c:v prores_ks -pix_fmt rgba -t %TIME% -r 24 %DST% + +%BIN%\ffmpeg -y -loop 1 -i %SRC% -vf scale=1920:1080 -profile:v 4444 -c:v prores_ks -pix_fmt rgba -vframes %TIME% -r 24 %DST% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3ca68b0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + com.tmworks + lyricsmovie + 1.0.0 + jar + + + ${project.groupId} + telopGenerator + ${project.version} + + + + org.json + json + 20211205 + + + javax + javaee-api + 8.0 + jar + + + javax.json + javax.json-api + 1.1 + + + + org.glassfish + javax.json + 1.1 + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.6 + + + jar-with-dependencies + + + + club.tmworks.lyricsmovie.Main + + + + + + package + + single + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + club.tmworks.lyricsmovie.Main + + + + + + + + UTF-8 + 11 + 11 + + diff --git a/src/main/java/club/tmworks/lyricsmovie/FFMPEGController.java b/src/main/java/club/tmworks/lyricsmovie/FFMPEGController.java new file mode 100644 index 0000000..f242726 --- /dev/null +++ b/src/main/java/club/tmworks/lyricsmovie/FFMPEGController.java @@ -0,0 +1,134 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package club.tmworks.lyricsmovie; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.json.JsonArray; +import javax.json.JsonObject; + +/** + * + * @author MURAKAMI Takahiro + */ +public class FFMPEGController { + + private String ffmpegPath = ""; + private String bindCmd = ""; + + private String bindlist = "bindlist.txt"; + + FFMPEGController(String ffmpegPath, String bindCmd) { + this.ffmpegPath = ffmpegPath; + this.bindCmd = bindCmd; + } + + public int bindMKV(JsonObject src, String dstFileName) { + int rvalue = -1; + + PrintWriter pw = null; + try { + JsonArray srcFiles = src.getJsonArray(PhoneticJsonGenerator.PNG); + + String bindList = ""; + for (int i = 0; i < srcFiles.size(); i++) { + JsonObject fileInfo = srcFiles.getJsonObject(i); + String fileName = fileInfo.getString(PhoneticJsonGenerator.PNG); + fileName = Generator.getExtChanged(fileName, ".mkv"); + bindList += "file " + fileName + "\n"; + } + bindList = bindList.trim(); + + pw = new PrintWriter( + new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(this.bindlist), "UTF-8"))); + pw.print(bindList); + pw.flush(); + pw.close(); + + String[] cmd = new String[]{ + this.bindCmd, + dstFileName + }; + + String cmdline = ""; + for (String c : cmd) { + cmdline += c + " "; + } + + System.out.println(cmdline.trim()); + + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); + + Process p = pb.start(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = r.readLine()) != null) { + System.out.println(line); + } + r.close(); + } + p.waitFor(); + + } catch (UnsupportedEncodingException | FileNotFoundException ex) { + Logger.getLogger(FFMPEGController.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException | InterruptedException ex) { + Logger.getLogger(FFMPEGController.class.getName()).log(Level.SEVERE, null, ex); + } finally { + if (pw != null) { + pw.close(); + } + } + + return rvalue; + } + + /** + * ffmpegをキックして、テロップを指定時間分生成する。 + * + * @param srcfile + * @param length + * @return + */ + public int generateMKV(String srcfile, double length) { + int rvalue = -1; + try { + String[] cmd = new String[]{ + ffmpegPath, + srcfile, + String.valueOf(length) + }; + + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); + + Process p = pb.start(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = r.readLine()) != null) { + System.out.println(line); + } + } + p.waitFor(); + } catch (IOException | InterruptedException ex) { + Logger.getLogger(FFMPEGController.class.getName()).log(Level.SEVERE, null, ex); + } + return rvalue; + } + +} diff --git a/src/main/java/club/tmworks/lyricsmovie/Generator.java b/src/main/java/club/tmworks/lyricsmovie/Generator.java new file mode 100644 index 0000000..cb7171f --- /dev/null +++ b/src/main/java/club/tmworks/lyricsmovie/Generator.java @@ -0,0 +1,327 @@ +package club.tmworks.lyricsmovie; + +import com.tmworks.telopgenerator.PngGenerator; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonValue; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * + * @author MURAKAMI Takahiro + */ +public class Generator { + + public static final int MODE_BUILD_PHONETIC = 0; + public static final int MODE_BUILD_MOVIE = 1; + public static final int MODE_BUILD_ONEPATH = 2; + + private int mode = 0; + + private String workingPath = ""; + + private String srcFile = ""; + + private String mediaFileDst = ""; + + private String mediaFile01 = ""; + + private String mediaFile02 = ""; + + private String mediaFile03 = ""; + + private String ffmpegPath = ""; + + private String bindCommand = ""; + + private String packingSize = ""; + + PhoneticJsonGenerator parser = null; + + boolean onlyPicture = false; + + boolean onlyJson = false; + + boolean smart = false; + + private PngGenerator pngGenerator = null; + + public Generator() { + this.pngGenerator = new PngGenerator(); + } + + public void loadArgs(String[] args) { + if (args.length == 0) { + this.showUsage(); + } else { + for (int i = 0; i < args.length; i++) { + if (args[i].equals("-i")) { + this.srcFile = args[i + 1]; + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "Source File : {0}", this.srcFile); + + this.mediaFileDst = Generator.getExtChanged(srcFile, ".json"); + this.mediaFile01 = Generator.getExtChanged(mediaFileDst, ".mid01.json"); + this.mediaFile02 = Generator.getExtChanged(mediaFileDst, ".mid02.json"); + this.mediaFile03 = Generator.getExtChanged(mediaFileDst, ".mid03.json"); + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "Media file : {0}", this.mediaFileDst); + i++; + } + + if (args[i].equals("-w")) { + this.workingPath = args[i + 1]; + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "WorkPath : {0}", this.workingPath); + i++; + } + + if (args[i].equals("-ffmpeg")) { + this.ffmpegPath = args[i + 1]; + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "ffmpeg Path : {0}", this.ffmpegPath); + i++; + } + + if (args[i].equals("-bind")) { + this.bindCommand = args[i + 1]; + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "bindcommand Path : {0}", this.bindCommand); + i++; + } + + if (args[i].equals("-p")) { + this.onlyPicture = true; + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "start only picure generation : {0}", this.onlyPicture); + i++; + } + + if (args[i].equals("-j")) { + this.onlyJson = true; + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "start only json generation : {0}", this.onlyJson); + i++; + } + + if (args[i].equals("-smart")) { + this.smart = true; + Logger.getLogger(Generator.class.getName()).log(Level.INFO, "start smart generation : {0}", this.smart); + i++; + } + + } + } + } + + /** + * 拡張子の取得 + * + * @param src + * @return + */ + public static String getExtention(String src) { + File f = new File(src); + String rvalue = f.getName().substring(f.getName().lastIndexOf(".") + 1); + return rvalue; + } + + /** + * 拡張子の変更 + * + * @param src + * @param newExt + * @return + */ + public static String getExtChanged(String src, String newExt) { + File f = new File(src); + String rvalue = f.getName().substring(0, f.getName().lastIndexOf(".")); + rvalue += newExt; + return rvalue; + } + + public void showUsage() { + System.out.println("使い方:"); + System.out.println("データファイルはmusicxmlなら最初から、json.txtなら途中から作業開始"); + System.out.println("java -jar lyricsmovie-1.0.jar -w <作業ディレクトリ> -i <データファイル>"); + } + + public void execute() { + try { + if (this.srcFile.length() > 0) { + + if (this.smart) { + if (Generator.getExtention(this.srcFile).toLowerCase().equals("txt")) { + this.onlyJson = false; + this.onlyPicture = true; + } + if (Generator.getExtention(this.srcFile).toLowerCase().equals("musicxml")) { + this.onlyJson = true; + this.onlyPicture = false; + } + } + + // 抽出 + JsonObject midFile = null; + + if (!this.onlyPicture) { + this.parser = new PhoneticJsonGenerator(); + midFile = this.parser.parse( + this.workingPath + File.separator + this.srcFile + ); + this.saveJsonFile(this.mediaFile01, midFile); + + // 圧縮 + midFile = this.parser.compress01(midFile); + this.saveJsonFile(this.mediaFile02, midFile); + + // 圧縮その2 + midFile = this.parser.compress02(midFile); + this.saveJsonFile(this.mediaFileDst, midFile); + File f = new File(this.mediaFileDst + ".txt"); + // caption編集用ファイルも生成 + if (!f.exists()) { + this.saveJsonFile(this.mediaFileDst + ".txt", midFile); + } + + } else { + midFile = this.loadJson(this.workingPath + File.separator + this.srcFile); + } + + if (!this.onlyJson) { + // Png生成 + JsonArray notes = midFile.getJsonArray(PhoneticJsonGenerator.NOTES); + String fileBase = Generator.getExtChanged(this.mediaFileDst, ""); + int count = 0; + + // Pngのファイル名と生成する時間を格納する。 + JsonArray pngItems = Json.createArrayBuilder().build(); + for (JsonValue note : notes) { + JsonObject noteObject = note.asJsonObject(); + + String pngFileName = ""; + + if (noteObject.get(PhoneticJsonGenerator.LYRIC) != null) { + pngFileName = fileBase + String.format("-%03d", count) + ".png"; + + // pngファイル書き出し + String ly = ""; + if (noteObject.getString(PhoneticJsonGenerator.CAPTION).length() > 0) { + ly = note.asJsonObject().getString(PhoneticJsonGenerator.CAPTION); + } else { + ly = note.asJsonObject().getString(PhoneticJsonGenerator.LYRIC); + } + this.pngGenerator.generate(pngFileName, ly); + System.out.println(ly); + + // ファイル名と再生時間の収容 + JsonObject pngFileInfo = (JsonObject) Json.createObjectBuilder() + .add(PhoneticJsonGenerator.CAPTION, ly) + .add(PhoneticJsonGenerator.PNG, pngFileName) + //.add(PhoneticJsonGenerator.SOUND_START, noteObject.getJsonNumber(PhoneticJsonGenerator.SOUND_START).doubleValue()) + //.add(PhoneticJsonGenerator.SOUND_END, noteObject.getJsonNumber(PhoneticJsonGenerator.SOUND_END).doubleValue()) + .add(PhoneticJsonGenerator.SOUND_LENGTH, noteObject.getJsonNumber(PhoneticJsonGenerator.SOUND_LENGTH).doubleValue()) + .build(); + pngItems = Json.createArrayBuilder(pngItems).add(pngFileInfo).build(); + + count++; + } + } + JsonObject pngFileList = (JsonObject) Json.createObjectBuilder().add(PhoneticJsonGenerator.PNG, pngItems).build(); + this.saveJsonFile(this.mediaFile03, pngFileList); + + // MKV生成 + FFMPEGController ffmpegController = new FFMPEGController(this.ffmpegPath, this.bindCommand); + for (int i = 0; i < pngItems.size(); i++) { + JsonObject targetPng = pngItems.getJsonObject(i); + + String srcFileName = targetPng.getString(PhoneticJsonGenerator.PNG); + + double length = targetPng.getJsonNumber(PhoneticJsonGenerator.SOUND_LENGTH).doubleValue(); + length = length * 24d; + length = length / 1000d; + BigDecimal bd = new BigDecimal(length).setScale(0, RoundingMode.HALF_UP); + length = bd.doubleValue(); + + int result = ffmpegController.generateMKV(srcFileName, length); + } + + // MKV結合 + String dstFile = Generator.getExtChanged(this.srcFile, ".bind.mkv"); + ffmpegController.bindMKV(pngFileList, dstFile); + } + } + } catch (NumberFormatException | JSONException ex) { + Logger.getLogger(Generator.class.getName()).log(Level.SEVERE, ex.getMessage()); + } + } + + private JsonObject loadJson(String loadFileName) { + FileInputStream fis = null; + JsonObject rvalue = null; + try { + File f = new File(loadFileName); + fis = new FileInputStream(f); + InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); + BufferedReader br = new BufferedReader(isr); + + String jsonSrc = ""; + String line = ""; + while ((line = br.readLine()) != null) { + jsonSrc += line; + } + + JsonReader jsonReader = Json.createReader(new StringReader(jsonSrc)); + rvalue = jsonReader.readObject(); + } catch (FileNotFoundException ex) { + Logger.getLogger(Generator.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(Generator.class.getName()).log(Level.SEVERE, null, ex); + } finally { + try { + fis.close(); + } catch (IOException ex) { + Logger.getLogger(Generator.class.getName()).log(Level.SEVERE, null, ex); + } + } + return rvalue; + } + + private void saveJsonFile(String saveFileName, JsonObject src) { + JSONObject jobj = new JSONObject(src.toString()); + String midStr = jobj.toString(4); + + String mediafilepath = this.workingPath + File.separator + saveFileName; + try (PrintWriter pw = new PrintWriter( + new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(mediafilepath), "UTF-8") + ) + )) { + pw.print(midStr); + pw.flush(); + } catch (UnsupportedEncodingException | FileNotFoundException ex) { + Logger.getLogger(Generator.class + .getName()).log(Level.SEVERE, null, ex); + } + + } + + public static void main(String[] args) { + Generator main = new Generator(); + main.loadArgs(args); + main.execute(); + } +} diff --git a/src/main/java/club/tmworks/lyricsmovie/Note.java b/src/main/java/club/tmworks/lyricsmovie/Note.java new file mode 100644 index 0000000..a9ffc70 --- /dev/null +++ b/src/main/java/club/tmworks/lyricsmovie/Note.java @@ -0,0 +1,256 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package club.tmworks.lyricsmovie; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonString; +import javax.json.JsonValue; + +/** + * + * @author MURAKAMI Takahiro + */ +public class Note implements JsonObject, Cloneable { + + public static final String LYRIC = "lyric"; + public static final String CAPTION = "caption"; + public static final String TYPE = "type"; + public static final String DULATION_LENGTH = "dulation_length"; + public static final String DULATION_AMOUNTS = "dulation_amounts"; + public static final String DULATION_START = "dulation_start"; + public static final String DULATION_END = "dulation_end"; + public static final String SOUND_LENGTH = "sound_length"; + public static final String SOUND_AMOUNTS = "sound_amounts"; + public static final String SOUND_START = "sound_start"; + public static final String SOUND_END = "sound_end"; + public static final String BIND_SRC = "bind_src"; + public static final String ID = "id"; + + private JsonObject data; + + public Note() { + this.data = this.createNote(); + } + + public Note(JsonObject data) { + this.data = data; + } + + public JsonObject getJsonObject() { + return this.data; + } + + public Note set(String key, String value) { + this.data = (JsonObject) Json.createObjectBuilder(this.data).add(key, value).build(); + return this; + } + + public Note set(String key, double value) { + this.data = (JsonObject) Json.createObjectBuilder(this.data).add(key, value).build(); + return this; + } + + public Note set(String key, int value) { + this.data = (JsonObject) Json.createObjectBuilder(this.data).add(key, value).build(); + return this; + } + + public Note set(String key, JsonArray value) { + this.data = (JsonObject) Json.createObjectBuilder(this.data).add(key, value).build(); + return this; + } + + @Override + public String getString(String key) { + return this.data.getString(key); + } + + @Override + public JsonNumber getJsonNumber(String key) { + return this.data.getJsonNumber(key); + } + + private JsonObject createNote() { + JsonArray bindSrc = Json.createArrayBuilder().build(); + JsonObject rvalue = Json.createObjectBuilder() + .add(CAPTION, "") + .add(LYRIC, "") + .add(DULATION_START, 0.0d) + .add(DULATION_END, 0.0d) + .add(DULATION_LENGTH, 0.0d) + .add(SOUND_START, 0.0d) + .add(SOUND_END, 0.0d) + .add(SOUND_LENGTH, 0.0d) + .add(ID, 0) + .add(BIND_SRC, bindSrc) + .build(); + return rvalue; + } + + public Note clone() { + Note rvalue = new Note(); + rvalue.set(CAPTION, this.getString(CAPTION)); + rvalue.set(LYRIC, this.getString(LYRIC)); + rvalue.set(DULATION_START, this.getJsonNumber(DULATION_START).doubleValue()); + rvalue.set(DULATION_END, this.getJsonNumber(DULATION_END).doubleValue()); + rvalue.set(DULATION_LENGTH, this.getJsonNumber(DULATION_LENGTH).doubleValue()); + rvalue.set(SOUND_START, this.getJsonNumber(SOUND_START).doubleValue()); + rvalue.set(SOUND_END, this.getJsonNumber(SOUND_END).doubleValue()); + rvalue.set(SOUND_LENGTH, this.getJsonNumber(SOUND_LENGTH).doubleValue()); + rvalue.set(BIND_SRC, this.getJsonArray(BIND_SRC)); + return rvalue; + } + + public Note marge(Note prev, Note post) { + Note rvalue = new Note(); + + rvalue.set(CAPTION, prev.getString(CAPTION) + post.getString(CAPTION)); + rvalue.set(LYRIC, prev.getString(LYRIC) + post.getString(LYRIC)); + rvalue.set(DULATION_START, prev.getJsonNumber(DULATION_START).doubleValue()); + rvalue.set(DULATION_END, post.getJsonNumber(DULATION_END).doubleValue()); + rvalue.set(DULATION_LENGTH, prev.getJsonNumber(DULATION_LENGTH).doubleValue() + + post.getJsonNumber(DULATION_LENGTH).doubleValue() + ); + rvalue.set(SOUND_START, prev.getJsonNumber(SOUND_START).doubleValue()); + rvalue.set(SOUND_END, post.getJsonNumber(SOUND_END).doubleValue()); + rvalue.set(SOUND_LENGTH, prev.getJsonNumber(SOUND_LENGTH).doubleValue() + + post.getJsonNumber(SOUND_LENGTH).doubleValue() + ); + + JsonArray bindPrev = prev.getJsonArray(BIND_SRC); + JsonArray bindPost = post.getJsonArray(BIND_SRC); + + if (bindPrev.size() < 1) { + bindPrev = Json.createArrayBuilder().add(prev).build(); + } + if (bindPost.size() < 1) { + bindPost = Json.createArrayBuilder().add(post).build(); + } + + for (JsonValue bindPost1 : bindPost) { + bindPrev = Json.createArrayBuilder(bindPrev).add(bindPost1).build(); + } + rvalue.set(Note.BIND_SRC, bindPrev); + return rvalue; + } + + @Override + public JsonArray getJsonArray(String string) { + return this.data.getJsonArray(string); + } + + @Override + public JsonObject getJsonObject(String string) { + return this.data.getJsonObject(string); + } + + @Override + public JsonString getJsonString(String string) { + return this.data.getJsonString(string); + } + + @Override + public String getString(String string, String string1) { + return this.data.getString(string, string1); + } + + @Override + public int getInt(String string) { + return this.data.getInt(string); + } + + @Override + public int getInt(String string, int i) { + return this.data.getInt(string, i); + } + + @Override + public boolean getBoolean(String string) { + return this.data.getBoolean(string); + } + + @Override + public boolean getBoolean(String string, boolean bln) { + return this.data.getBoolean(string, bln); + } + + @Override + public boolean isNull(String string) { + return this.data.isNull(string); + } + + @Override + public ValueType getValueType() { + return this.data.getValueType(); + } + + @Override + public int size() { + return this.data.size(); + } + + @Override + public boolean isEmpty() { + return this.data.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.data.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.data.containsValue(value); + } + + @Override + public JsonValue get(Object key) { + return this.data.get(key); + } + + @Override + public JsonValue put(String key, JsonValue value) { + return this.data.put(key, value); + } + + @Override + public JsonValue remove(Object key) { + return this.data.remove(key); + } + + @Override + public void putAll(Map m) { + this.data.putAll(m); + } + + @Override + public void clear() { + this.data.clear(); + } + + @Override + public Set keySet() { + return this.data.keySet(); + } + + @Override + public Collection values() { + return this.data.values(); + } + + @Override + public Set> entrySet() { + return this.data.entrySet(); + } + +} diff --git a/src/main/java/club/tmworks/lyricsmovie/PhoneticJsonGenerator.java b/src/main/java/club/tmworks/lyricsmovie/PhoneticJsonGenerator.java new file mode 100644 index 0000000..69d74bd --- /dev/null +++ b/src/main/java/club/tmworks/lyricsmovie/PhoneticJsonGenerator.java @@ -0,0 +1,363 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package club.tmworks.lyricsmovie; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * + * @author MURAKAMI Takahiro + */ +public class PhoneticJsonGenerator { + + public static final String NOTES = "NOTES"; + public static final String PNG = "PNG"; + + public static final String LYRIC = "lyric"; + public static final String CAPTION = "caption"; + public static final String TYPE = "type"; + public static final String DULATION_LENGTH = "dulation_length"; + public static final String DULATION_AMOUNTS = "dulation_amounts"; + public static final String DULATION_START = "dulation_start"; + public static final String DULATION_END = "dulation_end"; + public static final String SOUND_LENGTH = "sound_length"; + public static final String SOUND_AMOUNTS = "sound_amounts"; + public static final String SOUND_START = "sound_start"; + public static final String SOUND_END = "sound_end"; + public static final String BIND_SRC = "bind_src"; + public static final String ID = "id"; + + private String srcPath = ""; + + // これより短いと前の節に結合する。 + private double bindBlankLength = 1000.0d; + + private double divisions = 0.0d; + + private double divmodrate = 0.9d; + + /** + * XMLより抜き出してJSONに変換したデータ + */ + private JsonObject phonetic; + + private double baseLength = 0; + + public PhoneticJsonGenerator() { + this.phonetic = Json.createObjectBuilder().build(); + } + + /** + * XMLより作業用のJSONデータを抜き出す。 + * + * @param srcPath + * + * @return + */ + public JsonObject parse(String srcPath) { + FileInputStream is = null; + try { + this.srcPath = srcPath; + Logger.getLogger(PhoneticJsonGenerator.class.getName()).log(Level.INFO, "PARSE MUSICXML : {0}", this.srcPath); + + is = new FileInputStream(Paths.get(this.srcPath).toFile()); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(is); + + // 2.XPathの処理を実行するXPathのインスタンスを取得する + XPath xpath = XPathFactory.newInstance().newXPath(); + + // Divisonの取得 + XPathExpression e = xpath.compile("//divisions"); + Node divNode = (Node) e.evaluate(document, XPathConstants.NODE); + this.divisions = Double.valueOf(divNode.getTextContent()); + + // テンポの取得 + XPathExpression expr = xpath.compile("//sound"); + Node node = (Node) expr.evaluate(document, XPathConstants.NODE); + String tempo = node.getAttributes().getNamedItem("tempo").getNodeValue(); + Logger.getLogger(PhoneticJsonGenerator.class.getName()).log(Level.INFO, "TEMPO : {0}", tempo); + + this.phonetic = (JsonObject) Json.createObjectBuilder(this.phonetic).add("tempo", tempo).build(); + + // 基底長は 1分 / (tempo * divisions) + this.baseLength = (60000d / (Double.valueOf(tempo) * this.divisions)); + //this.baseLength = 1000d / this.divisions; + + // Jsonルートノード + this.phonetic = (JsonObject) Json.createObjectBuilder(this.phonetic) + .add("base_length", String.valueOf(baseLength)) + .build(); + + // 文字の取得 + expr = xpath.compile("//note"); + NodeList notes = (NodeList) expr.evaluate(document, XPathConstants.NODESET); + + JsonArray jsonNoteParams = (JsonArray) Json.createArrayBuilder().build(); + double damounts = 0d; + double soundamounts = 0d; + for (int i = 0; i < notes.getLength(); i++) { + Node note = notes.item(i); + NodeList noteParams = note.getChildNodes(); + + String type = ""; + boolean tie = false; + double dlength = 0d; + double soundlength = 0d; + + Note jsonNote = new Note(); + + for (int j = 0; j < noteParams.getLength(); j++) { + Node noteParam = noteParams.item(j); + if (noteParam.getNodeName().equals(LYRIC)) { + Node noteText = this.findNode(noteParam, "text"); + jsonNote.set(LYRIC, noteText.getTextContent()); + } + if (noteParam.getNodeName().equals(TYPE)) { + type = noteParams.item(j).getTextContent(); + jsonNote.set(TYPE, type); + } + if (noteParam.getNodeName().equals("tie")) { + tie = true; + } + if (noteParam.getNodeName().equals("duration")) { + dlength = Double.valueOf(noteParam.getTextContent()); + damounts += dlength; + soundlength = dlength * this.baseLength; + soundamounts = damounts * this.baseLength; + jsonNote.set(DULATION_LENGTH, dlength); + jsonNote.set(DULATION_AMOUNTS, damounts); + jsonNote.set(SOUND_LENGTH, soundlength); + jsonNote.set(SOUND_AMOUNTS, soundamounts); + } + } + + if (tie && jsonNote.getString(LYRIC).length() < 1) { + jsonNote.set(LYRIC, "ー"); + } + + jsonNoteParams = (JsonArray) Json.createArrayBuilder(jsonNoteParams).add(jsonNote).build(); + + } + this.phonetic = (JsonObject) Json.createObjectBuilder(this.phonetic).add("NOTES", jsonNoteParams).build(); + + } catch (FileNotFoundException ex) { + Logger.getLogger(PhoneticJsonGenerator.class.getName()).log(Level.SEVERE, null, ex); + } catch (ParserConfigurationException | SAXException | XPathExpressionException | IOException ex) { + Logger.getLogger(PhoneticJsonGenerator.class.getName()).log(Level.SEVERE, null, ex); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ex) { + Logger.getLogger(PhoneticJsonGenerator.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + return this.phonetic; + } + + /** + * 前後の節の合計が1小節以下なら結合する。 + * + * @param src + * @return + */ + public JsonObject compress02(JsonObject src) { + JsonArray notesSrc = src.getJsonArray(NOTES); + JsonArray notesDst = Json.createArrayBuilder().build(); + + double mLength = this.baseLength * this.divisions * 4d; //1小節の時間msec + + for (int i = 0; i < notesSrc.size(); i++) { + Note current = new Note(notesSrc.getJsonObject(i)); + System.out.println(current.getString(LYRIC)); + Note next = null; + + if (i != notesSrc.size() - 1) { + next = new Note(notesSrc.getJsonObject(i + 1)); + } + + Note newNote = current.clone(); + if (next != null) { + double currentLength = current.getJsonNumber(SOUND_LENGTH).doubleValue(); + double nextLength = next.getJsonNumber(SOUND_LENGTH).doubleValue(); + if (currentLength + nextLength < mLength) { + Logger.getLogger(this.getClass().getName()).info(current.getString(LYRIC) + " is binded next"); + newNote = newNote.marge(current, next); + i += 1; + } + } + + notesDst = Json.createArrayBuilder(notesDst).add(newNote).build(); + } + + JsonObject rvalue = Json.createObjectBuilder() + .add(NOTES, notesDst) + .build(); + + return rvalue; + } + + private Note cloneJsonObject(JsonObject src) { + Note rvalue = new Note() + .set(CAPTION, src.getString(CAPTION)) + .set(LYRIC, src.getString(LYRIC)) + .set(DULATION_START, src.getJsonNumber(DULATION_START).doubleValue()) + .set(DULATION_END, src.getJsonNumber(DULATION_END).doubleValue()) + .set(DULATION_LENGTH, src.getJsonNumber(DULATION_LENGTH).doubleValue()) + .set(SOUND_START, src.getJsonNumber(SOUND_START).doubleValue()) + .set(SOUND_END, src.getJsonNumber(SOUND_END).doubleValue()) + .set(SOUND_LENGTH, src.getJsonNumber(SOUND_LENGTH).doubleValue()); + return rvalue; + } + + /** + * PrevとPostを結合する。 + * + * @param prev + * @param post + * @return + */ + public JsonObject margeNote(JsonObject prev, JsonObject post) { + JsonObject rvalue = this.createNote(); + JsonArray bindSrc = Json.createArrayBuilder().add(prev).add(post).build(); + rvalue = (JsonObject) Json.createObjectBuilder(rvalue) + .add(CAPTION, prev.getString(CAPTION) + post.getString(CAPTION)) + .add(LYRIC, prev.getString(LYRIC) + post.getString(LYRIC)) + .add(DULATION_START, prev.getJsonNumber(DULATION_START).doubleValue()) + .add(DULATION_END, post.getJsonNumber(DULATION_END).doubleValue()) + .add(DULATION_LENGTH, prev.getJsonNumber(DULATION_LENGTH).doubleValue() + + post.getJsonNumber(DULATION_LENGTH).doubleValue()) + .add(SOUND_START, prev.getJsonNumber(SOUND_START).doubleValue()) + .add(SOUND_END, post.getJsonNumber(SOUND_END).doubleValue()) + .add(SOUND_LENGTH, prev.getJsonNumber(SOUND_LENGTH).doubleValue() + + post.getJsonNumber(SOUND_LENGTH).doubleValue()) + .add(BIND_SRC, bindSrc) + .build(); + return rvalue; + } + + public JsonObject createNote() { + JsonArray bindSrc = Json.createArrayBuilder().build(); + JsonObject rvalue = Json.createObjectBuilder() + .add(CAPTION, "") + .add(LYRIC, "") + .add(DULATION_START, 0.0d) + .add(DULATION_END, 0.0d) + .add(DULATION_LENGTH, 0.0d) + .add(SOUND_START, 0.0d) + .add(SOUND_END, 0.0d) + .add(SOUND_LENGTH, 0.0d) + .add(ID, 0) + .add(BIND_SRC, bindSrc) + .build(); + return rvalue; + } + + /** + * 初期のテロップデータを格納する処理 + * + * @param src + * @return + */ + public JsonObject compress01(JsonObject src) { + + JsonArray notesSrc = src.getJsonArray("NOTES"); + JsonArray notesDst = Json.createArrayBuilder().build(); + + Note noteDst = new Note(notesSrc.getJsonObject(0)); + + double start = 0.0d; + double end = 0d; + int id = 1; + for (int i = 1; i < notesSrc.size(); i++) { + + Note noteSrc = new Note(notesSrc.getJsonObject(i)); + // 書き出しバッファなし + if (noteDst.getString(LYRIC).length() < 1) { + // 歌詞なし + if (noteSrc.getString(LYRIC).length() < 1) { + noteDst = noteDst.marge(noteDst, noteSrc); + } // 歌詞あり + else { + // 節が途切れるので、古いnodesDstを保存し、あらたに新規生成 + notesDst = (JsonArray) Json.createArrayBuilder(notesDst).add(noteDst).build(); + + id++; + noteDst = noteSrc.clone(); + noteDst.set(ID, id); + + } // 歌詞なし + } // 書き出しバッファあり + else { + // 歌詞なし + if (noteSrc.getString("lyric").length() < 1) { + // 節が途切れるので、古いnodesDstを保存し、あらたに新規生成 + notesDst = (JsonArray) Json.createArrayBuilder(notesDst).add(noteDst).build(); + + id++; + noteDst = noteSrc.clone(); + noteDst.set(ID, id); + + } // 歌詞あり + else { + noteDst = noteDst.marge(noteDst, noteSrc); + } + } + + } + notesDst = (JsonArray) Json.createArrayBuilder(notesDst).add(noteDst).build(); + + JsonObject rvalue = (JsonObject) Json.createObjectBuilder() + .add("base_length", src.getString("base_length")) + .add("tempo", src.getString("tempo")) + .add(NOTES, notesDst) + .build(); + + return rvalue; + } + + private Node findNode(Node parent, String targetNodeName) { + Node rvalue = null; + NodeList items = parent.getChildNodes(); + for (int i = 0; i < items.getLength(); i++) { + Node buf = items.item(i); + if (buf.getNodeName().equals(targetNodeName)) { + rvalue = buf; + break; + } + } + return rvalue; + } + + private double soundLength(double duration) { + return duration * this.baseLength; + } + +} diff --git a/src/main/manifest.mf b/src/main/manifest.mf new file mode 100644 index 0000000..2521b44 --- /dev/null +++ b/src/main/manifest.mf @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Main-Class: club.tmworks.lyricsmovie.Main \ No newline at end of file