微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

[Angular实战网易云]——15、歌词渲染

w# 歌词

歌词初始化

播放面板左侧展示歌单歌曲,右侧展示当前歌曲歌词,并且实现和左侧相通的滚动条。因此要根据ID获取当前歌曲歌词。

歌词API

  • song.service.ts
   // 根据ID获取歌曲歌词
    getLyric (id: number): Observable<Lyric> {
        const params = new HttpParams().set('id', id.toString());
        return this.http.get(this.uri + 'lyric', { params })
            .pipe(map((res: {
                [key: string]: { lyric: string; }
            }) => {
                return {
                    lyric: res.lrc.lyric,
                    tlyric: res.tlyric.lyric,
                }
            }))
    }
  • common.types.ts
// 歌词
export type Lyric = {
    lyric: string, // 原文
    tlyric: string // 译文
}

根据api获取当前歌曲需要的部分,并且在common.types.ts中声明Lyric类型,将返回值res处理成需要的键值对格式。

处理歌词

单语歌词

当监听当前歌曲值发生变化时,不仅要重新获取当前歌曲索引还有获取当前歌曲歌词。

  • wy-player-panel.component.ts
    currentLyric: BaseLyricLine[]; // 歌词

  // 数据监听
    ngOnChanges (changes: SimpleChanges): void {
        // 监听当前歌曲变化
        if (changes['currentSong']) {
            if (this.currentSong) {
                this.currentIndex = findindex(this.songList, this.currentSong);
                this.updateLyric();
                if (this.show) {
                    this.scrollToCurrent();
                }
            } 
        }
    }

    // 根据ID获取歌词
    private updateLyric () {
        this.songServe.getLyric(this.currentSong.id).subscribe((res) => {
            const lyric = new WyLyric(res);
            this.currentLyric = lyric.lines;
        });
    }
  • wy-lyric.ts
import { Lyric } from "src/app/services/data-types/common.types";
// 歌词基类
export interface BaseLyricLine {
    txt: string;
    txtCn: string;
}
// 歌词派生类
interface LyricLine extends BaseLyricLine {
    time: number
}

const timeExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;  //正值-匹配 [00:34.910] 类型

export class WyLyric {

    private lrc: Lyric; // 暂存传来的歌词
    lines: LyricLine[] = [];

    constructor(lrc: Lyric) {
        this.lrc = lrc;
        this.init();
    }

    private init () {
        // 判断是否是外语歌曲
        if (this.lrc.tlyric) {
            this.generTLyric();
        } else {
            this.generLyric();
        }
    }


    // 解析单语歌词
    private generLyric () {
        const lines = this.lrc.lyric.split('\n');
        lines.forEach(line => this.makeLine(line))
    }

    // 解析双语歌词
    private generTLyric () {

    }

    // 处理单行歌词
    private makeLine (line: string) {
        const result = timeExp.exec(line);
        if (result) {
            const txt = line.replace(timeExp, '').trim();
            const txtCn = '';
            if (txt) {
                const thirdResult = result[3] || '00';
                const len = thirdResult.length;
                const _thirdResult = len > 2 ? parseInt(thirdResult) : parseInt(thirdResult) * 10;
                const time = Number(result[1]) * 60 * 1000 + Number(result[2]) * 60 + _thirdResult;
                this.lines.push({ txt, txtCn, time })
            }
        }
    }

}

当实例化歌词类的时候先暂存传来的歌词,然后判断当前歌词是否是双语,当仅为单语时会将歌词中string类型的原文转为数组,因为每一行歌词前都会有时间提示,所以要通过makeLine方法单独处理每一行。


在处理单行歌词时使用了正则表达式来匹配字符串格式的参数。

RegExpObject.exec(string)
返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。

参数描述
string必需。要检索的字符串。

如果 exec() 找到了匹配的文本,则返回一个结果数组。否则,返回 null。此数组的第 0 个元素是与正则表达式相匹配的文本,第 1 个元素是与 RegExpObject 的第 1 个子表达式相匹配的文本(如果有的话),第 2 个元素是与 RegExpObject 的第 2 个子表达式相匹配的文本(如果有的话),以此类推。除了数组元素和 length 属性之外,exec() 方法还返回两个属性。index 属性声明的是匹配文本的第一个字符的位置。input 属性则存放的是被检索的字符串 string。我们可以看得出,在调用非全局的 RegExp 对象的 exec() 方法时,返回的数组与调用方法 String.match() 返回的数组是相同的。

在单行歌词中匹配"[分钟:秒.毫秒]"类型的字符串


如果能匹配到,就在当前单行歌词中将匹配到的字符串置空并去空格,这样就能拿到实际渲染的歌词了。而在时间上是将分钟,秒转换为毫秒并存下来。

  • wy-player-panel.component.html
<!-- 歌词列表 -->
       <app-wy-scroll class=" list-lyric">
           <ul>
               <li *ngFor="let line of currentLyric">
                   {{line.txt}} 
                   <br />
                   {{line?.txtCn}}   
                </li>
           </ul>
       </app-wy-scroll>


双语歌词

在拿到国外歌曲的歌词时,会有翻译的文本,这时候就要将原文和译文一句句的对应起来。在WyLyric类中通过对传送进来的歌词类判断,来决定歌词处理的通道是走单语还是双语。

  • wy-lyriv.ts
 // 解析双语歌词
    private generTLyric () {
    	// 获取原文数组
        const lines = this.lrc.lyric.split('\n');
        // 获取译文数组
        const tLines = this.lrc.tlyric.split('\n').filter(item => timeExp.exec(item) != null);
        // 判断原文与译文的长度关系
        const moreLine = lines.length - tLines.length;
        let tempArr = [];
        if (moreLine >= 0) {
            tempArr = [lines, tLines];
        } else {
            tempArr = [tLines, lines];
        }
		// 获取短的歌词数组中的第一个时间部分
        const first = timeExp.exec(tempArr[1][0])[0];
		// 获取长的歌词数组需要跳过的索引
        const skipIndex = tempArr[0].findindex(item => {
            const exec = timeExp.exec(item);
            if (exec) {
                return exec[0] === first;
            }
        });
		
        const _skip = skipIndex === -1 ? 0 : skipIndex;
        const skipItems = tempArr[0].slice(0, _skip);
        if (skipItems.length) {
            skipItems.forEach(line => this.makeLine(line));
        }
        let zipLines$;
        if (moreLine > 0) {
            zipLines$ = zip(from(lines).pipe(skip(_skip)), from(tLines));
        } else {
            zipLines$ = zip(from(lines), from(tLines).pipe(skip(_skip)));
        }
        zipLines$.subscribe(([line, tLine]) => this.makeLine(line, tLine))
    }
    // 处理单行歌词
    private makeLine (line: string, tLine = '') {
        const result = timeExp.exec(line);
        if (result) {
            const txt = line.replace(timeExp, '').trim();
            const txtCn = tLine ? tLine.replace(timeExp, '').trim() : '';
            if (txt) {
                const thirdResult = result[3] || '00';
                const len = thirdResult.length;
                const _thirdResult = len > 2 ? parseInt(thirdResult) : parseInt(thirdResult) * 10;
                const time = Number(result[1]) * 60 * 1000 + Number(result[2]) * 60 + _thirdResult;
                this.lines.push({ txt, txtCn, time })
            }
        }
    }

在通过获取双语歌词回调中,声明局部变量lines和tLines来保存传递过来的歌词的原文和译文部分,api数据中观察到原文和译文可能不对应,因此译文部分要经过一个空值筛选。因为二者不一 一对应,但是在渲染的时候要达到一句原文一句译文,所以需要判断。为了方便取值,声明一个装有原文译文数组的数组,当原文的长度大于等于译文,将原文放在索引0,反之放在索引1。

原文和译文长度可能不同,原文中可能含有一些版权声明和作词作曲介绍,这时候译文可能没有翻译,所以长度不同,这时的逻辑就是一是要全部显示原文歌词,但是还要将原文译文相对应。

此时用正则获取短的歌词数组中第一条数据的时间部分,然后用findindex在长的歌词数组中寻找该时间部分等于短的歌词数组中的一条的时间部分,拿到索引。

findindex()

  • 返回传入一个测试条件(函数)符合条件的数组第一个元素位置。
  • 为数组中的每个元素都调用一次函数执行:当数组中的元素在测试条件时返回 true 时, 返回符合条件的元素的索引位置,之后的值不会再调用执行函数如果没有符合条件的元素返回 -1
    注意: findindex() 对于空数组,函数是不会执行的。其并没有改变数组的原始值。

所以此时要将返回-1的时候转为0,即原文和译文长度相等,此时不用截取任何数据。截取数据不为空时,在将截取的数据单独提前push到渲染的歌词中。之后便是要处理一一对应的歌词了。

此时判断原文和译文的长度关系,如果原文长,就将原文跳过获取的索引,然后通过zip()来得到一个 [ 原文, 译文 ]observable类型的数组。译文长就将译文跳过获取的索引。然后在将得到的数据订阅处理单行歌词的回调。此时要将之前写死的单条译文写一个判断

总结

其中使用了正则表达式,很显然是一块短板,而且在使用zip()时提示方法不再支持,而是建议使用map来操作,目前为了理解没有修改,后续来调整这一方面!

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐