|
@@ -1,387 +0,0 @@
|
|
-import * as audio from './audio';
|
|
|
|
-import * as level from './level';
|
|
|
|
-import * as util from './util';
|
|
|
|
-import * as youtube from './youtube';
|
|
|
|
-
|
|
|
|
-export class Editor {
|
|
|
|
- audioManager: audio.AudioManager;
|
|
|
|
- urlElement: HTMLInputElement;
|
|
|
|
- loadElement: HTMLButtonElement;
|
|
|
|
- audioElement: HTMLInputElement;
|
|
|
|
- barElement: HTMLElement;
|
|
|
|
- markerListElement: HTMLElement;
|
|
|
|
- intervalListElement: HTMLElement;
|
|
|
|
- kanaElement: HTMLTextAreaElement;
|
|
|
|
- kanjiElement: HTMLTextAreaElement;
|
|
|
|
- displayElement: HTMLElement;
|
|
|
|
- jsonElement: HTMLInputElement;
|
|
|
|
- waveFormContainer: HTMLDivElement;
|
|
|
|
- track: audio.Track | null = null;
|
|
|
|
- markers: Marker[] = [];
|
|
|
|
- waveForm: WaveForm;
|
|
|
|
- currentMarker: Marker | null = null;
|
|
|
|
-
|
|
|
|
- constructor() {
|
|
|
|
- this.audioManager = new audio.AudioManager();
|
|
|
|
- this.urlElement = util.getElement(document, '#url');
|
|
|
|
- this.loadElement = util.getElement(document, '#load');
|
|
|
|
- this.loadElement.addEventListener('click', (event) => {
|
|
|
|
- this.loadAudio();
|
|
|
|
- });
|
|
|
|
- this.audioElement = util.getElement(document, '#audio');
|
|
|
|
- this.audioElement.addEventListener('change', (event) => {
|
|
|
|
- this.urlElement.value = '';
|
|
|
|
- this.loadAudio();
|
|
|
|
- });
|
|
|
|
- this.barElement = util.getElement(document, '.bar-overlay');
|
|
|
|
- this.markerListElement = util.getElement(document, '.markers');
|
|
|
|
- this.intervalListElement = util.getElement(document, '#intervals');
|
|
|
|
- this.kanaElement = util.getElement(document, '#kana');
|
|
|
|
- this.kanjiElement = util.getElement(document, '#kanji');
|
|
|
|
- this.displayElement = util.getElement(document, '#display');
|
|
|
|
- this.jsonElement = util.getElement(document, '#json');
|
|
|
|
- this.waveFormContainer = util.getElement(document, '.waveform-container');
|
|
|
|
- this.waveForm = new WaveForm(
|
|
|
|
- util.getElement(document, '#waveform'),
|
|
|
|
- util.getElement(document, '#waveform-overlay'),
|
|
|
|
- (time: number) => this.play(time)
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- this.markerListElement.addEventListener('click', (event: MouseEvent) =>
|
|
|
|
- this.markersClick(event)
|
|
|
|
- );
|
|
|
|
- document
|
|
|
|
- .querySelector('#play')!
|
|
|
|
- .addEventListener('click', () => this.play());
|
|
|
|
- document
|
|
|
|
- .querySelector('#pause')!
|
|
|
|
- .addEventListener('click', () => this.pause());
|
|
|
|
- document
|
|
|
|
- .querySelector('#insert-marker')!
|
|
|
|
- .addEventListener('click', () => this.insertMarker());
|
|
|
|
- document
|
|
|
|
- .querySelector<HTMLElement>('.bar')!
|
|
|
|
- .addEventListener('click', (event: MouseEvent) =>
|
|
|
|
- this.scrubberClick(event)
|
|
|
|
- );
|
|
|
|
- document
|
|
|
|
- .querySelector('#import')!
|
|
|
|
- .addEventListener('click', () => this.import());
|
|
|
|
- document
|
|
|
|
- .querySelector('#export')!
|
|
|
|
- .addEventListener('click', () => this.export());
|
|
|
|
-
|
|
|
|
- this.update();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- loadAudio(): void {
|
|
|
|
- const url = this.urlElement.value;
|
|
|
|
- if (url != '') {
|
|
|
|
- const videoId = youtube.getVideoId(url);
|
|
|
|
- if (videoId !== null) {
|
|
|
|
- const element = util.getElement(document, '#youtube');
|
|
|
|
- this.audioManager
|
|
|
|
- .loadTrackFromYoutube(videoId, element, () => {})
|
|
|
|
- .then((t) => {
|
|
|
|
- this.track = t;
|
|
|
|
- this.waveForm.clear();
|
|
|
|
- this.waveFormContainer.style.display = 'none';
|
|
|
|
- });
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- let file = this.audioElement.files![0];
|
|
|
|
- if (file != null) {
|
|
|
|
- if (this.track != null) {
|
|
|
|
- this.track.stop();
|
|
|
|
- }
|
|
|
|
- this.clearMarkers();
|
|
|
|
- this.audioManager.loadTrackFromFile(file).then((t) => {
|
|
|
|
- this.track = t;
|
|
|
|
- this.waveForm.setTrack(t);
|
|
|
|
- this.waveFormContainer.style.display = 'block';
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- update(): void {
|
|
|
|
- if (this.track != null) {
|
|
|
|
- let percentage = (this.track.getTime() / this.track.getDuration()) * 100;
|
|
|
|
- this.barElement.style.width = `${percentage}%`;
|
|
|
|
- if (this.track instanceof audio.FileTrack) {
|
|
|
|
- this.waveForm.update(this.markers);
|
|
|
|
- }
|
|
|
|
- if (this.currentMarker) {
|
|
|
|
- this.currentMarker.liElement.className = '';
|
|
|
|
- }
|
|
|
|
- let index = this.markers.findIndex((m) => m.time > this.track!.getTime());
|
|
|
|
- if (index < 0) index = 0;
|
|
|
|
- this.currentMarker = this.markers[index - 1];
|
|
|
|
- if (this.currentMarker) {
|
|
|
|
- this.currentMarker.liElement.className = 'highlight';
|
|
|
|
- }
|
|
|
|
- let text = this.kanjiElement.value.split('\n')[index] || '';
|
|
|
|
- this.displayElement.textContent = text;
|
|
|
|
- }
|
|
|
|
- requestAnimationFrame(() => this.update());
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- scrubberClick(event: MouseEvent): void {
|
|
|
|
- let pos = event.clientX - 10;
|
|
|
|
- console.log(pos);
|
|
|
|
- let percentage = pos / this.markerListElement.clientWidth;
|
|
|
|
- let targetTime = percentage * this.track!.getDuration();
|
|
|
|
- this.play(targetTime);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- markersClick(event: MouseEvent): void {
|
|
|
|
- let pos = event.clientX - 10;
|
|
|
|
- let percentage = pos / this.markerListElement.clientWidth;
|
|
|
|
- let targetTime = percentage * this.track!.getDuration();
|
|
|
|
- this.insertMarker(targetTime);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- insertMarker(time?: number): void {
|
|
|
|
- let marker = new Marker(
|
|
|
|
- this.track!.getDuration(),
|
|
|
|
- (marker: Marker) => this.removeMarker(marker),
|
|
|
|
- (marker: Marker) => this.playMarker(marker)
|
|
|
|
- );
|
|
|
|
- if (time !== undefined) {
|
|
|
|
- marker.time = time;
|
|
|
|
- } else {
|
|
|
|
- marker.time = this.track!.getTime();
|
|
|
|
- }
|
|
|
|
- let insertIndex = this.markers.findIndex((m) => m.time > marker.time);
|
|
|
|
- if (insertIndex >= 0) {
|
|
|
|
- this.markers.splice(insertIndex, 0, marker);
|
|
|
|
- } else {
|
|
|
|
- this.markers.push(marker);
|
|
|
|
- }
|
|
|
|
- this.markerListElement.appendChild(marker.markerElement);
|
|
|
|
- if (insertIndex >= 0) {
|
|
|
|
- this.intervalListElement.insertBefore(
|
|
|
|
- marker.liElement,
|
|
|
|
- this.markers[insertIndex + 1].liElement
|
|
|
|
- );
|
|
|
|
- } else {
|
|
|
|
- this.intervalListElement.appendChild(marker.liElement);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- play(start?: number, duration?: number): void {
|
|
|
|
- this.track!.pause();
|
|
|
|
- this.track!.start(start, duration);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- pause(): void {
|
|
|
|
- this.track!.pause();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- playMarker(marker: Marker): void {
|
|
|
|
- let start = marker.time;
|
|
|
|
- let end = this.track!.getDuration();
|
|
|
|
- let index = this.markers.findIndex((m) => m == marker);
|
|
|
|
- if (index < this.markers.length - 1) {
|
|
|
|
- end = this.markers[index + 1].time;
|
|
|
|
- }
|
|
|
|
- let duration = end - start;
|
|
|
|
- this.play(start, duration);
|
|
|
|
-
|
|
|
|
- this.highlightLine(this.kanjiElement, index + 1);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- removeMarker(marker: Marker): void {
|
|
|
|
- let index = this.markers.findIndex((m) => m == marker);
|
|
|
|
- this.markers.splice(index, 1);
|
|
|
|
- this.markerListElement.removeChild(marker.markerElement);
|
|
|
|
- this.intervalListElement.removeChild(marker.liElement);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- clearMarkers(): void {
|
|
|
|
- this.markers.forEach((m) => {
|
|
|
|
- this.markerListElement.removeChild(m.markerElement);
|
|
|
|
- this.intervalListElement.removeChild(m.liElement);
|
|
|
|
- });
|
|
|
|
- this.markers = [];
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- highlightLine(element: HTMLTextAreaElement, line: number) {
|
|
|
|
- let text = element.value;
|
|
|
|
- let index = 0;
|
|
|
|
- for (let i = 0; i < line; ++i) {
|
|
|
|
- index = text.indexOf('\n', index + 1);
|
|
|
|
- }
|
|
|
|
- let endIndex = text.indexOf('\n', index + 1);
|
|
|
|
- element.focus();
|
|
|
|
- element.setSelectionRange(index, endIndex);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- import(): void {
|
|
|
|
- this.clearMarkers();
|
|
|
|
- let lines: level.Line[] = JSON.parse(this.jsonElement.value);
|
|
|
|
- let kanji = '';
|
|
|
|
- let kana = '';
|
|
|
|
-
|
|
|
|
- lines.forEach((line) => {
|
|
|
|
- kanji += line.kanji + '\n';
|
|
|
|
- kana += line.kana + '\n';
|
|
|
|
- if (line.end != undefined) {
|
|
|
|
- this.insertMarker(line.end);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- this.kanjiElement.value = kanji;
|
|
|
|
- this.kanaElement.value = kana;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- export(): void {
|
|
|
|
- let kanji = this.kanjiElement.value.split('\n');
|
|
|
|
- let kana = this.kanaElement.value.split('\n');
|
|
|
|
- let length = Math.max(kanji.length, kana.length, this.markers.length - 1);
|
|
|
|
-
|
|
|
|
- let lines = [];
|
|
|
|
- let lastStart = 0;
|
|
|
|
- for (let i = 0; i < length; ++i) {
|
|
|
|
- let data: level.Line = {
|
|
|
|
- kanji: kanji[i] || '@',
|
|
|
|
- kana: kana[i] || '@',
|
|
|
|
- };
|
|
|
|
- if (this.markers[i]) {
|
|
|
|
- data.start = lastStart;
|
|
|
|
- data.end = this.markers[i].time;
|
|
|
|
- lastStart = data.end;
|
|
|
|
- }
|
|
|
|
- lines.push(data);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- this.jsonElement.value = JSON.stringify(lines);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- start(): void {
|
|
|
|
- this.loadAudio();
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-class Marker {
|
|
|
|
- markerElement: HTMLElement;
|
|
|
|
- liElement: HTMLElement;
|
|
|
|
- inputElement: HTMLInputElement;
|
|
|
|
-
|
|
|
|
- constructor(
|
|
|
|
- readonly duration: number,
|
|
|
|
- readonly remove: (marker: Marker) => void,
|
|
|
|
- readonly play: (marker: Marker) => void
|
|
|
|
- ) {
|
|
|
|
- this.markerElement = document.createElement('div');
|
|
|
|
- this.markerElement.className = 'marker';
|
|
|
|
-
|
|
|
|
- let fragment = util.loadTemplate(document, 'interval');
|
|
|
|
- this.liElement = util.getElement(fragment, '*');
|
|
|
|
- this.inputElement = util.getElement(fragment, '.interval');
|
|
|
|
- this.inputElement.addEventListener('change', () => {
|
|
|
|
- this.time = parseFloat(this.inputElement.value);
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- fragment
|
|
|
|
- .querySelector('.play-section')!
|
|
|
|
- .addEventListener('click', () => play(this));
|
|
|
|
- fragment
|
|
|
|
- .querySelector('.remove-section')!
|
|
|
|
- .addEventListener('click', () => remove(this));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- get time(): number {
|
|
|
|
- return parseFloat(this.inputElement.value);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- set time(t: number) {
|
|
|
|
- this.inputElement.value = t.toFixed(1);
|
|
|
|
- let percentage = (t * 100) / this.duration;
|
|
|
|
- this.markerElement.style.left = `${percentage}%`;
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-class WaveForm {
|
|
|
|
- ctx: CanvasRenderingContext2D;
|
|
|
|
- overlayCtx: CanvasRenderingContext2D;
|
|
|
|
- track: audio.FileTrack | null = null;
|
|
|
|
- data: Float32Array | null = null;
|
|
|
|
- stride: number = 0;
|
|
|
|
- currentSection: number = -1;
|
|
|
|
-
|
|
|
|
- constructor(
|
|
|
|
- readonly canvas: HTMLCanvasElement,
|
|
|
|
- readonly overlay: HTMLCanvasElement,
|
|
|
|
- readonly setTime: (time: number) => void
|
|
|
|
- ) {
|
|
|
|
- canvas.height = canvas.clientHeight;
|
|
|
|
- canvas.width = canvas.clientWidth;
|
|
|
|
- overlay.height = overlay.clientHeight;
|
|
|
|
- overlay.width = overlay.clientWidth;
|
|
|
|
- this.ctx = canvas.getContext('2d')!;
|
|
|
|
- this.overlayCtx = overlay.getContext('2d')!;
|
|
|
|
-
|
|
|
|
- this.overlayCtx.fillStyle = 'rgba(255, 0, 0, 0.5)';
|
|
|
|
-
|
|
|
|
- this.overlay.addEventListener('click', (event: MouseEvent) => {
|
|
|
|
- let pos = event.clientX - this.overlay.offsetLeft;
|
|
|
|
- let percentage = pos / this.overlay.width;
|
|
|
|
- let time = this.currentSection * 5 + percentage * 5;
|
|
|
|
- this.setTime(time);
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- clear(): void {
|
|
|
|
- this.track = null;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- setTrack(track: audio.FileTrack): void {
|
|
|
|
- this.track = track;
|
|
|
|
- this.stride = Math.floor(
|
|
|
|
- (this.track.buffer.sampleRate / this.canvas.width) * 5
|
|
|
|
- );
|
|
|
|
- this.currentSection = -1;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- timeToX(time: number): number {
|
|
|
|
- return ((time - this.currentSection * 5) / 5) * this.canvas.width;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- update(markers: Marker[]): void {
|
|
|
|
- let section = Math.floor(this.track!.getTime() / 5);
|
|
|
|
-
|
|
|
|
- let height = this.canvas.height;
|
|
|
|
- if (this.currentSection != section) {
|
|
|
|
- this.data = this.track!.buffer.getChannelData(0);
|
|
|
|
- this.ctx.clearRect(0, 0, this.canvas.width, height);
|
|
|
|
- this.ctx.beginPath();
|
|
|
|
- this.ctx.moveTo(0, height / 2);
|
|
|
|
- let offset = section * this.canvas.width * this.stride;
|
|
|
|
- for (let i = 0; i < this.canvas.width; ++i) {
|
|
|
|
- let index = offset + i * this.stride;
|
|
|
|
- let value = height / 2 + (height / 2) * this.data[index];
|
|
|
|
- this.ctx.lineTo(i, value);
|
|
|
|
- }
|
|
|
|
- this.ctx.stroke();
|
|
|
|
- this.currentSection = section;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- let marker = this.timeToX(this.track!.getTime());
|
|
|
|
- this.overlayCtx.clearRect(0, 0, this.canvas.width, height);
|
|
|
|
- this.overlayCtx.fillRect(0, 0, marker, height);
|
|
|
|
- markers.forEach((m) => {
|
|
|
|
- if (m.time > section * 5 && m.time <= section * 5 + 5) {
|
|
|
|
- let x = this.timeToX(m.time);
|
|
|
|
- this.overlayCtx.beginPath();
|
|
|
|
- this.overlayCtx.moveTo(x, 0);
|
|
|
|
- this.overlayCtx.lineTo(x, height);
|
|
|
|
- this.overlayCtx.stroke();
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-let e = new Editor();
|
|
|
|
-e.start();
|
|
|