editor.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. import * as audio from './audio';
  2. import * as level from './level';
  3. import * as util from './util';
  4. import * as youtube from './youtube';
  5. export class Editor {
  6. audioManager: audio.AudioManager;
  7. urlElement: HTMLInputElement;
  8. loadElement: HTMLButtonElement;
  9. audioElement: HTMLInputElement;
  10. barElement: HTMLElement;
  11. markerListElement: HTMLElement;
  12. intervalListElement: HTMLElement;
  13. kanaElement: HTMLTextAreaElement;
  14. kanjiElement: HTMLTextAreaElement;
  15. displayElement: HTMLElement;
  16. jsonElement: HTMLInputElement;
  17. waveFormContainer: HTMLDivElement;
  18. track: audio.Track | null = null;
  19. markers: Marker[] = [];
  20. waveForm: WaveForm;
  21. currentMarker: Marker | null = null;
  22. constructor() {
  23. this.audioManager = new audio.AudioManager();
  24. this.urlElement = util.getElement(document, '#url');
  25. this.loadElement = util.getElement(document, '#load');
  26. this.loadElement.addEventListener('click', event => {
  27. this.loadAudio();
  28. });
  29. this.audioElement = util.getElement(document, '#audio');
  30. this.audioElement.addEventListener('change', event => {
  31. this.urlElement.value = '';
  32. this.loadAudio();
  33. });
  34. this.barElement = util.getElement(document, '.bar-overlay');
  35. this.markerListElement = util.getElement(document, '.markers');
  36. this.intervalListElement = util.getElement(document, '#intervals');
  37. this.kanaElement = util.getElement(document, '#kana');
  38. this.kanjiElement = util.getElement(document, '#kanji');
  39. this.displayElement = util.getElement(document, '#display');
  40. this.jsonElement = util.getElement(document, '#json');
  41. this.waveFormContainer = util.getElement(document, '.waveform-container');
  42. this.waveForm = new WaveForm(
  43. util.getElement(document, '#waveform'),
  44. util.getElement(document, '#waveform-overlay'),
  45. (time: number) => this.play(time)
  46. );
  47. this.markerListElement.addEventListener('click', (event: MouseEvent) => this.markersClick(event));
  48. document.querySelector('#play')!.addEventListener('click', () => this.play());
  49. document.querySelector('#pause')!.addEventListener('click', () => this.pause());
  50. document.querySelector('#insert-marker')!.addEventListener('click', () => this.insertMarker());
  51. document.querySelector<HTMLElement>('.bar')!.addEventListener('click', (event: MouseEvent) => this.scrubberClick(event));
  52. document.querySelector('#import')!.addEventListener('click', () => this.import());
  53. document.querySelector('#export')!.addEventListener('click', () => this.export());
  54. this.update();
  55. }
  56. loadAudio(): void {
  57. const url = this.urlElement.value;
  58. if (url != '') {
  59. const videoId = youtube.getVideoId(url);
  60. if (videoId !== null) {
  61. const element = util.getElement(document, '#youtube');
  62. this.audioManager.loadTrackFromYoutube(videoId, element, () => {}).then(t => {
  63. this.track = t;
  64. this.waveForm.clear();
  65. this.waveFormContainer.style.display = 'none';
  66. });
  67. return;
  68. }
  69. }
  70. let file = this.audioElement.files![0];
  71. if (file != null) {
  72. if (this.track != null) {
  73. this.track.stop();
  74. }
  75. this.clearMarkers();
  76. this.audioManager.loadTrackFromFile(file).then(t => {
  77. this.track = t;
  78. this.waveForm.setTrack(t);
  79. this.waveFormContainer.style.display = 'block';
  80. });
  81. }
  82. }
  83. update(): void {
  84. if (this.track != null) {
  85. let percentage = this.track.getTime() / this.track.getDuration() * 100;
  86. this.barElement.style.width = `${percentage}%`;
  87. if (this.track instanceof audio.FileTrack) {
  88. this.waveForm.update(this.markers);
  89. }
  90. if (this.currentMarker) {
  91. this.currentMarker.liElement.className = '';
  92. }
  93. let index = this.markers.findIndex(m => m.time > this.track!.getTime());
  94. if (index < 0) index = 0;
  95. this.currentMarker = this.markers[index - 1];
  96. if (this.currentMarker) {
  97. this.currentMarker.liElement.className = 'highlight';
  98. }
  99. let text = this.kanjiElement.value.split('\n')[index] || '';
  100. this.displayElement.textContent = text;
  101. }
  102. requestAnimationFrame(() => this.update());
  103. }
  104. scrubberClick(event: MouseEvent): void {
  105. let pos = event.clientX - 10;
  106. console.log(pos);
  107. let percentage = pos / this.markerListElement.clientWidth;
  108. let targetTime = percentage * this.track!.getDuration();
  109. this.play(targetTime);
  110. }
  111. markersClick(event: MouseEvent): void {
  112. let pos = event.clientX - 10;
  113. let percentage = pos / this.markerListElement.clientWidth;
  114. let targetTime = percentage * this.track!.getDuration();
  115. this.insertMarker(targetTime);
  116. }
  117. insertMarker(time?: number): void {
  118. let marker = new Marker(
  119. this.track!.getDuration(),
  120. (marker: Marker) => this.removeMarker(marker),
  121. (marker: Marker) => this.playMarker(marker)
  122. );
  123. if (time !== undefined) {
  124. marker.time = time;
  125. } else {
  126. marker.time = this.track!.getTime();
  127. }
  128. let insertIndex = this.markers.findIndex(m => m.time > marker.time);
  129. if (insertIndex >= 0) {
  130. this.markers.splice(insertIndex, 0, marker);
  131. } else {
  132. this.markers.push(marker);
  133. }
  134. this.markerListElement.appendChild(marker.markerElement);
  135. if (insertIndex >= 0) {
  136. this.intervalListElement.insertBefore(marker.liElement, this.markers[insertIndex+1].liElement);
  137. } else {
  138. this.intervalListElement.appendChild(marker.liElement);
  139. }
  140. }
  141. play(start?: number, duration?: number): void {
  142. this.track!.pause();
  143. this.track!.start(start, duration);
  144. }
  145. pause(): void {
  146. this.track!.pause();
  147. }
  148. playMarker(marker: Marker): void {
  149. let start = marker.time;
  150. let end = this.track!.getDuration();
  151. let index = this.markers.findIndex(m => m == marker);
  152. if (index < this.markers.length - 1) {
  153. end = this.markers[index + 1].time;
  154. }
  155. let duration = end - start;
  156. this.play(start, duration);
  157. this.highlightLine(this.kanjiElement, index + 1);
  158. }
  159. removeMarker(marker: Marker): void {
  160. let index = this.markers.findIndex(m => m == marker);
  161. this.markers.splice(index, 1);
  162. this.markerListElement.removeChild(marker.markerElement);
  163. this.intervalListElement.removeChild(marker.liElement);
  164. }
  165. clearMarkers(): void {
  166. this.markers.forEach(m => {
  167. this.markerListElement.removeChild(m.markerElement);
  168. this.intervalListElement.removeChild(m.liElement);
  169. });
  170. this.markers = [];
  171. }
  172. highlightLine(element: HTMLTextAreaElement, line: number) {
  173. let text = element.value;
  174. let index = 0;
  175. for (let i = 0; i < line; ++i) {
  176. index = text.indexOf('\n', index + 1);
  177. }
  178. let endIndex = text.indexOf('\n', index + 1);
  179. element.focus();
  180. element.setSelectionRange(index, endIndex);
  181. }
  182. import(): void {
  183. this.clearMarkers();
  184. let lines: level.Line[] = JSON.parse(this.jsonElement.value);
  185. let kanji = '';
  186. let kana = '';
  187. lines.forEach(line => {
  188. kanji += line.kanji + '\n';
  189. kana += line.kana + '\n';
  190. if (line.end != undefined) {
  191. this.insertMarker(line.end);
  192. }
  193. });
  194. this.kanjiElement.value = kanji;
  195. this.kanaElement.value = kana;
  196. }
  197. export(): void {
  198. let kanji = this.kanjiElement.value.split('\n');
  199. let kana = this.kanaElement.value.split('\n');
  200. let length = Math.max(kanji.length, kana.length, this.markers.length - 1);
  201. let lines = [];
  202. let lastStart = 0;
  203. for (let i = 0; i < length; ++i) {
  204. let data: level.Line = {
  205. kanji: kanji[i] || '@',
  206. kana: kana[i] || '@',
  207. }
  208. if (this.markers[i]) {
  209. data.start = lastStart;
  210. data.end = this.markers[i].time;
  211. lastStart = data.end;
  212. }
  213. lines.push(data);
  214. }
  215. this.jsonElement.value = JSON.stringify(lines);
  216. }
  217. start(): void {
  218. this.loadAudio();
  219. }
  220. }
  221. class Marker {
  222. markerElement: HTMLElement;
  223. liElement: HTMLElement;
  224. inputElement: HTMLInputElement;
  225. constructor(
  226. readonly duration: number,
  227. readonly remove: (marker: Marker) => void,
  228. readonly play: (marker: Marker) => void
  229. ) {
  230. this.markerElement = document.createElement('div');
  231. this.markerElement.className = 'marker';
  232. let fragment = util.loadTemplate(document, 'interval');
  233. this.liElement = util.getElement(fragment, '*');
  234. this.inputElement = util.getElement(fragment, '.interval');
  235. this.inputElement.addEventListener('change', () => {
  236. this.time = parseFloat(this.inputElement.value);
  237. });
  238. fragment.querySelector('.play-section')!.addEventListener('click', () => play(this));
  239. fragment.querySelector('.remove-section')!.addEventListener('click', () => remove(this));
  240. }
  241. get time(): number {
  242. return parseFloat(this.inputElement.value);
  243. }
  244. set time(t: number) {
  245. this.inputElement.value = t.toFixed(1);
  246. let percentage = t * 100 / this.duration;
  247. this.markerElement.style.left = `${percentage}%`;
  248. }
  249. }
  250. class WaveForm {
  251. ctx: CanvasRenderingContext2D;
  252. overlayCtx: CanvasRenderingContext2D;
  253. track: audio.FileTrack | null = null;
  254. data: Float32Array | null = null;
  255. stride: number = 0;
  256. currentSection: number = -1;
  257. constructor(
  258. readonly canvas: HTMLCanvasElement,
  259. readonly overlay: HTMLCanvasElement,
  260. readonly setTime: (time: number) => void
  261. ) {
  262. canvas.height = canvas.clientHeight;
  263. canvas.width = canvas.clientWidth;
  264. overlay.height = overlay.clientHeight;
  265. overlay.width = overlay.clientWidth;
  266. this.ctx = canvas.getContext('2d')!;
  267. this.overlayCtx = overlay.getContext('2d')!;
  268. this.overlayCtx.fillStyle = 'rgba(255, 0, 0, 0.5)';
  269. this.overlay.addEventListener('click', (event: MouseEvent) => {
  270. let pos = event.clientX - this.overlay.offsetLeft;
  271. let percentage = pos / this.overlay.width;
  272. let time = this.currentSection * 5 + percentage * 5;
  273. this.setTime(time);
  274. });
  275. }
  276. clear(): void {
  277. this.track = null;
  278. }
  279. setTrack(track: audio.FileTrack): void {
  280. this.track = track;
  281. this.stride = Math.floor(this.track.buffer.sampleRate / this.canvas.width * 5);
  282. this.currentSection = -1;
  283. }
  284. timeToX(time: number): number {
  285. return (time - this.currentSection * 5) / 5 * this.canvas.width
  286. }
  287. update(markers: Marker[]): void {
  288. let section = Math.floor(this.track!.getTime() / 5);
  289. let height = this.canvas.height;
  290. if (this.currentSection != section) {
  291. this.data = this.track!.buffer.getChannelData(0);
  292. this.ctx.clearRect(0, 0, this.canvas.width, height);
  293. this.ctx.beginPath();
  294. this.ctx.moveTo(0, height / 2);
  295. let offset = section * this.canvas.width * this.stride;
  296. for (let i = 0; i < this.canvas.width; ++i) {
  297. let index = offset + i * this.stride;
  298. let value = height / 2 + height / 2 * this.data[index];
  299. this.ctx.lineTo(i, value);
  300. }
  301. this.ctx.stroke();
  302. this.currentSection = section;
  303. }
  304. let marker = this.timeToX(this.track!.getTime());
  305. this.overlayCtx.clearRect(0, 0, this.canvas.width, height);
  306. this.overlayCtx.fillRect(0, 0, marker, height);
  307. markers.forEach(m => {
  308. if (m.time > section * 5 && m.time <= section * 5 + 5) {
  309. let x = this.timeToX(m.time);
  310. this.overlayCtx.beginPath();
  311. this.overlayCtx.moveTo(x, 0);
  312. this.overlayCtx.lineTo(x, height);
  313. this.overlayCtx.stroke();
  314. }
  315. })
  316. }
  317. }
  318. let e = new Editor();
  319. e.start();