import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, OnChanges, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { AudioDelegate } from './audio-delegate'; import { fromEvent, Subscription } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; import { DragElement } from './drag-element'; import { NzMessageService } from 'ng-zorro-antd'; // interface Blob { // arrayBuffer: () => Promise<ArrayBuffer>; // } const MODE = { TEXT: 1, IMAGE: 2 }; function arrayBufferToString( buffer, encoding ) { return new Promise((resolve, reject) => { const blob = new Blob([buffer], { type: 'text/plain' }); const reader = new FileReader(); reader.onload = (evt) => { resolve(evt.target.result); }; reader.readAsText(blob, encoding); }); } @Component({ selector: 'app-lrc-editor', templateUrl: './lrc-editor.component.html', styleUrls: ['./lrc-editor.component.scss'] }) export class LrcEditorComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit { @ViewChild('waveEl', {static: true }) canvasElRef: ElementRef; @ViewChild('timeLineEl', {static: true }) timeLineEl: ElementRef; @ViewChild('uploadBtn', {static: true }) uploadBtn: ElementRef; lrcFileEncoding = 'GB18030'; @Output() editFinished = new EventEmitter(); // MODE = MODE; @Input() LRCData: any = { audio_url: null, fontSize: 24, lineHeight: 32, // mode: MODE.TEXT, lyrics: [] }; timeRangeModeConfObj = {}; timeRangeObj = { min: 0, max: 100 }; isScaleTimeLine = false; timeRangeSelector = [0, 100]; currentAudioTime = '00:00:00.000'; currentAudioDuration = '00:00:00.000'; selectHighlightTimePointIndex = -1; currentStep = 0; // item = { // audio_url: null, // mode: MODE.TEXT, // lyrics: [] // }; audioObj = new AudioDelegate(); isLoadingAudioBuffer = false; private waveWidth: number; private waveHeight: number; private ctx: CanvasRenderingContext2D; private adContext = new AudioContext(); private timeLineTimer: number; private timeLine: any; timePointData = []; httpErrorTryCount = 0; httpErrorTryMax = 3; playbackRate = 1; // private audioBuffer: AudioBuffer; private audioArrayBuffer: ArrayBuffer; timePointDataFull: any; private currentTimeRangDuration: number; private timeRangeEditLines: number[]; private audioPlayBarPositionSub: Subscription; private audioDataLoadedSub: Subscription; private audioPlayTimestampSub: Subscription; private audioPlayEndSub: Subscription; private dragMoveBarSub: Subscription; private arrowKeyUpSub: Subscription; private loadAudioSub: Subscription; constructor(private client: HttpClient, private nzMessageService: NzMessageService) { } ngOnInit() { window['lrc'] = this; // console.log('this.LRCData 1', this.LRCData); if (!this.LRCData) { this.LRCData = {}; } // console.log('this.LRCData 1', this.LRCData); this.LRCData = Object.assign( { audio_url: null, fontSize: 24, lineHeight: 32, // mode: MODE.TEXT, lyrics: [] }, this.LRCData); // window.player = this; if (this.audioPlayBarPositionSub) { this.audioPlayBarPositionSub.unsubscribe(); } this.audioPlayBarPositionSub = this.audioObj.audioPlayBarPosition.subscribe((evt) => { this.calcPlayBarPositionByTime(evt.time); }); if (this.audioDataLoadedSub) { this.audioDataLoadedSub.unsubscribe(); } this.audioDataLoadedSub = this.audioObj.audioDataLoaded.subscribe((ab) => { console.log('this.audioObj.audioDataLoaded'); this.timeRangeObj.max = this.audioObj.duration; // this.timeRangeSelector[1] = this.audioObj.duration; this.timeRangeSelector = [0, this.audioObj.duration]; this.currentAudioDuration = this.audioObj.durationFormatted; if (this.LRCData && this.LRCData.lyrics) { this.timePointData = []; this.LRCData.lyrics.forEach(timeline => { this.setTimestampPoint(timeline); }); } this.drawArrayBuffer(ab); }); if (this.audioPlayTimestampSub) { this.audioPlayTimestampSub.unsubscribe(); } this.audioPlayTimestampSub = this.audioObj.audioPlayTimestamp.subscribe((evt) => { this.currentAudioTime = evt.timeFormat; let time = evt.time; if (this.isScaleTimeLine) { if ( evt.time >= this.timeRangeSelector[0]) { time = evt.time - this.timeRangeSelector[0]; } if (evt.time >= this.timeRangeSelector[1]) { this.audioObj.pause(); this.audioObj.currentTime = evt.time; // - 5 * 60 / 1000; this.calcPlayBarPositionByTime(this.timeRangeSelector[0]); } } this.calcPlayBarPositionByTime(time); }); if (this.audioPlayEndSub) { this.audioPlayEndSub.unsubscribe(); } this.audioPlayEndSub = this.audioObj.audioPlayEnd.subscribe(() => { this.calcPlayBarPositionByTime(0); this.selectHighlightTimePointIndex = -1; }); this.uploadBtn.nativeElement.addEventListener('change', () => { const lrcFile = this.uploadBtn.nativeElement.files[0]; if (!lrcFile) { this.nzMessageService.error('请正确选择文件'); return; } lrcFile.arrayBuffer().then(ab => { arrayBufferToString(ab, this.lrcFileEncoding).then((s: string) => { this.timePointData.length = 0; s.split('[').forEach(l => { const p = l.indexOf(']'); const t = l.substr(0, p); const data = l.substr(p + 1).trim(); if (!isNaN(Number(l[0]))) { const time = this.audioObj.convertTagToTime(t); this.timePointData.push({ time, timeFormatted: t, data, position: `translateX(${ this.getXPositionByTime(time)}px)`, }); } console.log( t, data); }); }); }); }); if (this.LRCData.audio_url) { this.goNextStep2(); } } drawArrayBuffer(ab) { this.adContext.close(); this.adContext = new AudioContext(); this.adContext.decodeAudioData(ab).then(adb => { this.draw(adb); this.isLoadingAudioBuffer = false; (document.activeElement as HTMLButtonElement).blur(); this.canvasElRef.nativeElement.focus(); }); } ngOnChanges(value) { if (value.LRCData && !value.LRCData.firstChange) { // console.log(1111111111) this.ngOnInit(); // this.onAudioUploaded({url: this.LRCData.audio_url}); } // console.log('ngOnChanges', JSON.stringify(value)); // this.ngOnInit(); } ngOnDestroy(): void { this.audioPlayBarPositionSub.unsubscribe(); this. audioDataLoadedSub.unsubscribe(); this. audioPlayTimestampSub.unsubscribe(); this. audioPlayEndSub.unsubscribe(); this. dragMoveBarSub.unsubscribe(); this. arrowKeyUpSub.unsubscribe(); this. loadAudioSub.unsubscribe(); } ngAfterViewInit() { this.timeLine = this.timeLineEl.nativeElement; // this.timeLineBar = this.timeLineBarEl.nativeElement; this.waveWidth = this.canvasElRef.nativeElement.offsetWidth; this.waveHeight = this.canvasElRef.nativeElement.offsetHeight; this.ctx = this.canvasElRef.nativeElement.getContext('2d'); const dpr = window.devicePixelRatio || 1; this.canvasElRef.nativeElement.width = this.waveWidth * dpr; this.canvasElRef.nativeElement.height = this.waveHeight * dpr; this.ctx.scale(dpr, dpr); const cb = (e) => { if (e.code === 'ArrowUp' || e.code === 'ArrowDown' || e.code === 'ArrowLeft' || e.code === 'ArrowRight') { e.preventDefault(); e.stopPropagation(); // set time point } }; document.removeEventListener('keydown', cb ); document.addEventListener('keydown', cb ); if (this.arrowKeyUpSub) { this.arrowKeyUpSub.unsubscribe(); } this.arrowKeyUpSub = fromEvent<KeyboardEvent>(document, 'keyup') .pipe(throttleTime(100) ) .subscribe((e) => { e.preventDefault(); e.stopPropagation(); if (e.code === 'ArrowUp') { this.togglePlayAudio(); this.canvasElRef.nativeElement.focus(); } if (e.code === 'ArrowDown') { this.setTimestampPoint(); this.canvasElRef.nativeElement.focus(); // set time point } if (e.code === 'ArrowLeft') { if (this.selectHighlightTimePointIndex >= 0) { this.updateTimePointData(this.selectHighlightTimePointIndex, -0.05); } else { this.audioObj.currentTime -= 1; } this.canvasElRef.nativeElement.focus(); } if (e.code === 'ArrowRight') { if (this.selectHighlightTimePointIndex >= 0) { this.updateTimePointData(this.selectHighlightTimePointIndex, 0.05); } else { this.audioObj.currentTime += 1; } this.canvasElRef.nativeElement.focus(); } }); this.ctx.fillStyle = '#0F0'; if (this.dragMoveBarSub) { this.dragMoveBarSub.unsubscribe(); } this.dragMoveBarSub = new DragElement(this.timeLine, this.waveWidth).onMove.subscribe(evt => { this.selectHighlightTimePointIndex = -1; const percent = evt.position / this.waveWidth; let baseDur = this.audioObj.duration; let startPoint = 0; let currentTime = percent * this.audioObj.duration; if (this.isScaleTimeLine) { baseDur = this.currentTimeRangDuration; startPoint = this.timeRangeObj.min; currentTime = this.timeRangeObj.min + percent * baseDur; } this.audioObj.currentTime = currentTime; console.log('dd', baseDur, this.audioObj.currentTime ); this.currentAudioTime = this.audioObj.currentTimeFormatted(); this.calcPlayBarPositionByTime(percent * baseDur); }); } get isPlaying() { return this.audioObj.isPlaying; } updateTimePointData(index, time) { const p = this.timePointData[index]; if (p) { const newTime = p.time + time; let posTime = newTime; if (this.isScaleTimeLine) { posTime = posTime - this.timeRangeObj.min; } this.audioObj.currentTime = newTime; p.time = newTime; p.position = this.calcPlayBarPositionByTime(posTime, true); p.timeFormatted = this.audioObj.currentTimeFormatted(newTime); if (p.time > this.audioObj.duration) { p.warn = true; } } } getXPositionByTime(time) { let base = this.audioObj.duration; if (this.currentTimeRangDuration) { time = time - this.timeRangeObj.min; base = this.currentTimeRangDuration; } return ((time / base) * this.waveWidth) ; } calcPlayBarPositionByTime(time, returnVal = false) { const pos = `translateX(${this.getXPositionByTime(time) }px)`; if (returnVal) { return pos; } this.timeLine.style.transform = pos; } togglePlayAudio(evt?) { if (evt) { evt.target.blur(); } console.log('togglePlayAudio', evt); if (this.isPlaying) { this.audioObj.pause(); clearInterval(this.timeLineTimer); return; } if (this.isScaleTimeLine && (this.audioObj.currentTime < this.timeRangeSelector[0] || this.audioObj.currentTime >= this.timeRangeSelector[1] )) { this.audioObj.currentTime = this.timeRangeSelector[0]; } if (this.selectHighlightTimePointIndex >= 0) { const p = this.timePointData[this.selectHighlightTimePointIndex]; this.audioObj.currentTime = p.time; } this.audioObj.play().then(() => { // this.timeLineTimer = window.setInterval(() => { // if (canSetPoint) { // console.log(audio.currentTime,audioImages[audio.currentTime] ) // audioImages.push([audio.currentTime, pointAudio]); // canSetPoint = false; // } // this.timeLine.style.width = `${Math.ceil((this.audioObj.currentTime / this.audioObj.duration) * 100) }%`; // }, 32); }); } setTimestampPoint(timelineData = null, select = false) { if (!this.audioObj.currentSrc) { return; } // console.log('setTimestampPoint'); let pointTime = this.audioObj.currentTime; let data = null; let newLine = false; if (timelineData) { pointTime = timelineData.time; data = timelineData.data; newLine = timelineData.newLine; } let warn = false; if (pointTime > this.audioObj.duration || pointTime < 0) { warn = true; } const tmpAllTime = this.timePointData.map(d => { return {...d}; }); tmpAllTime.forEach(p => { p.timeFormatted = this.audioObj.convertTimeToTag(p.time, true); const delta = Math.abs(p.time - pointTime ) * 1000; if (delta < 150) { p.warn = true; } }); // if (this.isScaleTimeLine) { // pointTime = pointTime - this.timeRangeObj.min; // } tmpAllTime.push({ time: pointTime, // this.audioObj.currentTime, timeFormatted: this.audioObj.currentTimeFormatted(pointTime), data, position: `translateX(${ this.getXPositionByTime(pointTime)}px)`, warn, newLine }); tmpAllTime.sort((a, b) => { return a.time - b.time; }); this.timePointData.length = 0; tmpAllTime.forEach(item => { this.timePointData.push(item); }); if (select) { this.selectHighlightTimePointIndex = this.timePointData.length - 1; } } insertTimePoint(index) { const st = this.timePointData[index].time; let et = st + 5; const next = this.timePointData[index + 1]; if (next) { et = next.time; } const time = st + (et - st) / 2; // if (this.isScaleTimeLine) { // time = time + this.timeRangeObj.min; // } this.audioObj.currentTime = time; this.setTimestampPoint({ time, data: '' }); // this.timePointData.splice(index + 1, 0, { // time, // timeFormatted: this.audioObj.currentTimeFormatted(time), // data: '', // position: `translateX(${ this.getXPositionByTime(time)}px)`, // } ); this.selectHighlightTimePointIndex = index + 1; } removeTimePoint(index) { this.timePointData.splice(index, 1); this.selectHighlightTimePointIndex = -1; } selectTimePoint(index) { this.selectHighlightTimePointIndex = index; this.audioObj.currentTime = this.timePointData[index].time; } onAudioUploaded(evt) { console.log(evt); this.LRCData.audio_url = evt.url; this.LRCData.lyrics.length = 0; this.timePointData.length = 0; this.goNextStep2(); } loadingAudioBuffer() { this.isLoadingAudioBuffer = true; if (this.loadAudioSub) { this.loadAudioSub.unsubscribe(); } this.loadAudioSub = this.client.get(this.LRCData.audio_url, {responseType: 'arraybuffer'}) .subscribe( (ab) => { // const blob = new Blob([ab], { type: 'audio/wav' }); // this.audioObj.src = URL.createObjectURL(blob); this.audioArrayBuffer = ab.slice(0); this.audioObj.setSource(ab); this.audioObj.load(); }, ( error ) => { console.log(error); this.isLoadingAudioBuffer = false; if (this.httpErrorTryCount >= this.httpErrorTryMax) { this.nzMessageService.error('请检查网络,刷新重试'); return; } this.httpErrorTryCount += 1; this.goNextStep2(); }); } goNextStep2() { this.currentStep = 1; this.loadingAudioBuffer(); } filterAudioSamples( adb: AudioBuffer, samples) { const data = []; const length = adb.length; const blockSize = Math.floor(length / samples); const rawData = adb.getChannelData(0); for (let i = 0; i < samples; i++) { const blockStart = i * blockSize; let sum = 0; for (let j = 0; j < blockSize; j++) { sum = sum + Math.abs(rawData[blockStart + j]); } data.push(sum / blockSize); } return data; } normalizeData(data) { const base = Math.pow(Math.max(...data), -1); return data.map(n => n * base); } draw(adb) { const waveData = this.filterAudioSamples(adb, this.waveWidth); const normalizeDataForAllAudioBuffer = this.normalizeData(waveData); const width = this.waveWidth / normalizeDataForAllAudioBuffer.length; normalizeDataForAllAudioBuffer.forEach((d, i) => { const x = width * i; const height = d * this.waveHeight / 2; this.ctx.fillRect(x, this.waveHeight / 2 - height, 1, height * 2); }); } saveUserData() { const result = []; this.timePointData.forEach(p => { const t = {...p}; delete t.position; delete t.warn; delete t.timeFormatted; t.data = t.data; result.push(t); }); this.LRCData.lyrics = result; this.editFinished.emit(this.LRCData); // console.log(this.timePointData); } changePlaybackRate(val) { this.audioObj.playbackRate = val; } onImageUploadSuccess(evt) { } uploadLRC() { this.uploadBtn.nativeElement.click(); } changeLrcFileEncoding(enc) { this.lrcFileEncoding = enc; } formatter = (value: number): string => { return this.audioObj.convertTimeToTag(value, false); } timeRangeAfterChange(value) { this.timeRangeSelector = value; } scaleTimeLine() { this.audioObj.pause(); this.selectHighlightTimePointIndex = -1; this.isScaleTimeLine = true; if (!this.timePointDataFull) { this.timePointDataFull = this.timePointData.map(line => { return {...line}; }); } // this.timeRangeModeConfObj.startTime = this.timeRangeSelector[0]; // this.timeRangeModeConfObj.endTime = this.timeRangeSelector[0]; const start = this.timeRangeSelector[0]; const end = this.timeRangeSelector[1]; const sp = start / this.audioObj.duration; const ep = end / this.audioObj.duration; const sl = sp * this.audioArrayBuffer.byteLength; const el = ep * this.audioArrayBuffer.byteLength; this.currentTimeRangDuration = end - start; this.timeRangeObj.max = end; this.timeRangeObj.min = start; this.calcPlayBarPositionByTime(start); // this.timeRangeSelector[0] = start; // this.timeRangeSelector[1] = end; this.timeRangeSelector = [start, end]; this.timeRangeEditLines = []; this.timePointData = this.timePointData.filter( (line, idx) => { if (line.time > start && line.time < end) { this.timeRangeEditLines.push(idx); return true; } return false; }).map( line => { line.position = this.calcPlayBarPositionByTime( line.time , true); return line; }); const ab = this.audioArrayBuffer.slice(sl, el); // const ab = this.audioObj.getBufferClip(s, e); this.drawArrayBuffer(ab); } restoreTimeLine() { this.audioObj.pause(); this.timeRangeObj.max = this.audioObj.duration; this.timeRangeObj.min = 0; this.timeRangeSelector = [0, this.audioObj.duration]; // this.timeRangeSelector[1] = this.audioObj.duration; this.selectHighlightTimePointIndex = -1; this.isScaleTimeLine = false; this.currentTimeRangDuration = this.audioObj.duration; const head = this.timePointDataFull.slice(0, Math.min(...this.timeRangeEditLines) ); const tail = this.timePointDataFull.slice( Math.max(...this.timeRangeEditLines) + 1 ); // this.timePointData = [...head, ...this.timePointData, ...tail]; this.timePointData = [...head, ...this.timePointData, ...tail].map(line => { return {...line}; }).sort((a, b) => { return a.time - b.time; }); // this.timePointData = this.timePointDataFull.map(line => { // return {...line}; // }); this.timePointDataFull = null; const ab = this.audioArrayBuffer.slice(0); // const ab = this.audioObj.getBufferClip(s, e); this.drawArrayBuffer(ab); } }