123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- 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();
|