anzhiyu中使用alphatab实现播放乐谱

anzhiyu中使用alphatab实现播放乐谱

要在 Hexo 中使用 anzhiyu 主题并实现通过 AlphaTab 播放乐谱,可以按照以下步骤操作。以下是详细的实现方法:

1. 确保 Hexo 和 anzhiyu 主题已正确安装

在开始之前,请确认你已经成功安装了 Hexo 和 anzhiyu 主题。如果尚未安装,可以参考以下命令:

# 安装 Hexo
npm install -g hexo-cli

# 初始化 Hexo 项目
hexo init my-blog
cd my-blog

# 安装 anzhiyu 主题
git clone https://github.com/anzhiyu/hexo-theme-anzhiyu.git themes/anzhiyu

然后在 _config.yml 文件中设置主题为 anzhiyu

theme: anzhiyu

运行以下命令启动本地服务器,确保一切正常:

hexo server

2. 引入 AlphaTab

AlphaTab 是一个开源的乐谱渲染和播放库。你需要将其集成到你的 Hexo 博客中。

root/一律代表你的博客根目录

(1) 下载 AlphaTab 资源

访问 AlphaTab 官方网站 或其 GitHub 仓库,下载最新版本的 AlphaTab 文件。你需要以下文件:

  • alphatab.js(核心功能)
  • alphatab.css(创建样式文件)root/source/config/``css
  • 如果需要音频支持,可能还需要 soundfont 文件。
  • root一律代表你的博客根目录

将这些文件放到你的 Hexo 项目的 root/source/config/jsroot/source/config/``css 目录下。

例如:

root/source/config/js/alphatab.js
root/source/config/alphatab.css

body[data-type="alphatab"] #page .page-title {
    display: none;
}
.at-wrap {
    /*width: 80vw;*/
    height: 80vh;
    margin: 0 auto;
    border: 1px solid rgba(0, 0, 0, 0.12);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
}

.at-content {
    position: relative;
    overflow: hidden;
    flex: 1 1 auto;
}

/** Sidebar **/
.at-sidebar {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    max-width: 80px;
    width: auto;
    display: flex;
    align-content: stretch;
    z-index: 1002;
    overflow: hidden;
    border-right: 1px solid rgba(0, 0, 0, 0.12);
    background: #FFFFFF;
}

.at-sidebar:hover {
    max-width: 400px;
    transition: max-width 0.2s;
    overflow-y: auto;
}

.at-viewport {
    overflow-y: auto;
    position: absolute;
    top: 0;
    left: 70px;
    right: 0;
    bottom: 0;
    padding-right: 20px;
}

.at-footer {
    flex: 0 0 auto;
    background: #436d9d;
    color: #fff;
}

/** Overlay **/

.at-overlay {
    /** Fill Parent */
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 3;

    /* Blurry dark shade */
    backdrop-filter: blur(3px);
    background: rgba(0, 0, 0, 0.5);

    /** center content */
    display: flex;
    justify-content: center;
    align-items: flex-start;
}

.at-overlay-content {
    /* white box with drop-shadow */
    margin-top: 20px;
    background: #fff;
    box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
    padding: 10px;
}

/** Track selector **/
.at-track {
    display: flex;
    position: relative;
    padding: 5px;
    transition: background 0.2s;
    cursor: pointer;
}

.at-track:hover {
    background: rgba(0, 0, 0, 0.1);
}

.at-track > .at-track-icon,
.at-track > .at-track-details {
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.at-track > .at-track-icon {
    flex-shrink: 0;
    font-size: 32px;
    opacity: 0.5;
    transition: opacity 0.2s;
    width: 64px;
    height: 64px;
    margin-right: 5px;
    align-items: center;
}

.at-track-name {
    font-weight: bold;
    margin-bottom: 5px;
}

.at-track:hover > .at-track-icon {
    opacity: 0.8;
}

.at-track.active {
    background: rgba(0, 0, 0, 0.03);
}

.at-track.active > .at-track-icon {
    color: #4972a1;
    opacity: 1;
}

.at-track > .at-track-name {
    font-weight: 500;
}

/** Footer **/
.at-controls {
    flex: 0 0 auto;
    display: flex;
    justify-content: space-between;
    background: #436d9d;
    color: #fff;
}

.at-controls > div {
    display: flex;
    justify-content: flex-start;
    align-content: center;
    align-items: center;
}

.at-controls > div > * {
    display: flex;
    text-align: center;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 4px;
    margin: 0 3px;
}

.at-controls .btn {
    color: #fff;
    border-radius: 0;
    height: 40px;
    width: 40px;
    height: 40px;
    font-size: 16px;
}
.at-controls .btn.disabled {
    cursor: progress;
    opacity: 0.5;
}

.at-controls a.active {
    background: #5588c7;
    text-decoration: none;
}

.at-controls .btn i {
    vertical-align: top;
}

.at-controls select {
    -moz-appearance: none;
    -webkit-appearance: none;
    appearance: none;
    border: none;
    width: 100%;
    height: 40px;
    background: #436d9d;
    padding: 4px 10px;
    color: #fff;
    font-size: 16px;
    text-align-last: center;
    text-align: center;
    -ms-text-align-last: center;
    -moz-text-align-last: center;
    cursor: pointer;
}

.at-song-title {
    font-weight: bold;
}

.at-cursor-bar {
    /* Defines the color of the bar background when a bar is played */
    background: rgba(255, 242, 0, 0.25);
}

.at-selection div {
    /* Defines the color of the selection background */
    background: rgba(64, 64, 255, 0.1);
}

.at-cursor-beat {
    /* Defines the beat cursor */
    background: rgba(64, 64, 255, 0.75);
    width: 3px;
}

.at-highlight * {
    /* Defines the color of the music symbols when they are being played (svg) */
    fill: #0078ff;
    stroke: #0078ff;
}
.at-score-selector {
    padding: 10px;
    border-bottom: 1px solid #ddd;
}
.at-score-selector select {
    width: 100%;
    padding: 8px;
    border-radius: 4px;
    border: 1px solid #ccc;
}

(2) 在主题中加载 AlphaTab.css

编辑 root/_config.anzhiyu.yml 文件,在 inject 中添加以下代码以加载 AlphaTab 的 CSS 文件:

<inject:
  head:
    - <link rel="stylesheet" href="https://cdn.ljsama.cn/config/alphaTabFont/iconfont.css" media="defer" onload="this.media='all'">

(3) 创建乐谱页面

root/themes/anzhiyu/layout/includes/page中创建alphaTab.pug并写入下代码

".at-wrap
    .at-overlay
        .at-overlay-content 乐谱加载中
    .at-content
        .at-sidebar
            .at-sidebar-content
                .at-track-list
        .at-viewport
            .at-main
    .at-controls
        .at-controls-left
            a.btn.at-player-stop.disabled
                i.iconfont.icon-zanting
            a.btn.at-player-play-pause.disabled
                i.iconfont.icon-bofang
            span.at-player-progress 0%
            .at-song-info
                span.at-song-title
                | -
                span.at-song-artist
            .at-song-position 00:00 / 00:00
        .at-controls-right
            a.btn.toggle.at-count-in
                i.iconfont.icon-shalou
            a.btn.at-metronome
                i.iconfont.icon-jishiben
            a.btn.at-loop
                i.iconfont.icon-xunhuan
            a.btn.at-print
                i.iconfont.icon-dayinji
            .at-zoom
                i.iconfont.icon-wenjianjia
                select
                    option(value="25") 25%
                    option(value="50") 50%
                    option(value="75") 75%
                    option(value="90") 90%
                    option(value="100" selected) 100%
                    option(value="110") 110%
                    option(value="125") 125%
                    option(value="150") 150%
                    option(value="200") 200%
            .at-layout
                select
                    option(value="horizontal") 水平
                    option(value="page" selected) 垂直

template#at-track-template
    .at-track
        .at-track-icon
            i.iconfont.icon-guitar-du
        .at-track-details
            .at-track-name
//alphaTab.js的位置就是上一步保存的位置
script(src="https://cdn.ljsama.cn/config/js/alphaTab.js")
script(type="text/javascript").
    // load elements
    const wrapper = document.querySelector(".at-wrap");
    const main = wrapper.querySelector(".at-main");

    // initialize alphatab
    const settings = {
        file: "https://www.alphatab.net/files/canon.gp" //你的乐谱地址
        player: {
            enablePlayer: true,
            soundFont: "https://cdn.ljsama.cn/config/alphaTabFont/sonivox.sf2", //播放乐谱的音频文件
            scrollElement: wrapper.querySelector('.at-viewport')
        },
    };
    console.log(settings)
    // 添加全局加载方法

    const api = new alphaTab.AlphaTabApi(main, settings);
    window.loadScore = (filePath) => {
        api.score = filePath;
        api.render();
    };
    // overlay logic
    const overlay = wrapper.querySelector(".at-overlay");
    api.renderStarted.on(() => {
        overlay.style.display = "flex";
    });
    api.renderFinished.on(() => {
        overlay.style.display = "none";
    });

    // track selector
    function createTrackItem(track) {
        const trackItem = document
            .querySelector("#at-track-template")
            .content.cloneNode(true).firstElementChild;
        trackItem.querySelector(".at-track-name").innerText = track.name;
        trackItem.track = track;
        trackItem.onclick = (e) => {
            e.stopPropagation();
            api.renderTracks([track]);
        };
        return trackItem;
    }
    const trackList = wrapper.querySelector(".at-track-list");
    api.scoreLoaded.on((score) => {
        // clear items
        trackList.innerHTML = "";
        // generate a track item for all tracks of the score
        score.tracks.forEach((track) => {
            trackList.appendChild(createTrackItem(track));
        });
    });
    api.renderStarted.on(() => {
        // collect tracks being rendered
        const tracks = new Map();
        api.tracks.forEach((t) => {
            tracks.set(t.index, t);
        });
        // mark the item as active or not
        const trackItems = trackList.querySelectorAll(".at-track");
        trackItems.forEach((trackItem) => {
            if (tracks.has(trackItem.track.index)) {
                trackItem.classList.add("active");
            } else {
                trackItem.classList.remove("active");
            }
        });
    });

    /** Controls **/
    api.scoreLoaded.on((score) => {
        wrapper.querySelector(".at-song-title").innerText = score.title;
        wrapper.querySelector(".at-song-artist").innerText = score.artist;
    });

    const countIn = wrapper.querySelector('.at-controls .at-count-in');
    countIn.onclick = () => {
        countIn.classList.toggle('active');
        if (countIn.classList.contains('active')) {
            api.countInVolume = 1;
        } else {
            api.countInVolume = 0;
        }
    };

    const metronome = wrapper.querySelector(".at-controls .at-metronome");
    metronome.onclick = () => {
        metronome.classList.toggle("active");
        if (metronome.classList.contains("active")) {
            api.metronomeVolume = 1;
        } else {
            api.metronomeVolume = 0;
        }
    };

    const loop = wrapper.querySelector(".at-controls .at-loop");
    loop.onclick = () => {
        loop.classList.toggle("active");
        api.isLooping = loop.classList.contains("active");
    };

    wrapper.querySelector(".at-controls .at-print").onclick = () => {
        api.print();
    };

    const zoom = wrapper.querySelector(".at-controls .at-zoom select");
    zoom.onchange = () => {
        const zoomLevel = parseInt(zoom.value) / 100;
        api.settings.display.scale = zoomLevel;
        api.updateSettings();
        api.render();
    };

    const layout = wrapper.querySelector(".at-controls .at-layout select");
    layout.onchange = () => {
        switch (layout.value) {
            case "horizontal":
                api.settings.display.layoutMode = alphaTab.LayoutMode.Horizontal;
                break;
            case "page":
                api.settings.display.layoutMode = alphaTab.LayoutMode.Page;
                break;
        }
        api.updateSettings();
        api.render();
    };

    // player loading indicator
    const playerIndicator = wrapper.querySelector(
        ".at-controls .at-player-progress"
    );
    api.soundFontLoad.on((e) => {
        const percentage = Math.floor((e.loaded / e.total) * 100);
        playerIndicator.innerText = percentage + "%";
    });
    api.playerReady.on(() => {
        playerIndicator.style.display = "none";
    });

    // main player controls
    const playPause = wrapper.querySelector(
        ".at-controls .at-player-play-pause"
    );
    const stop = wrapper.querySelector(".at-controls .at-player-stop");
    playPause.onclick = (e) => {
        if (e.target.classList.contains("disabled")) {
            return;
        }
        api.playPause();
    };
    stop.onclick = (e) => {
        if (e.target.classList.contains("disabled")) {
            return;
        }
        api.stop();
    };
    api.playerReady.on(() => {
        playPause.classList.remove("disabled");
        stop.classList.remove("disabled");
    });
    api.playerStateChanged.on((e) => {
        const icon = playPause.querySelector("i.iconfont");
        if (e.state === alphaTab.synth.PlayerState.Playing) {
            icon.classList.remove("icon-bofang");
            icon.classList.add("icon-24gl-pause2");
        } else {
            icon.classList.remove("icon-24gl-pause2");
            icon.classList.add("icon-bofang");
        }
    });

    // song position
    function formatDuration(milliseconds) {
        let seconds = milliseconds / 1000;
        const minutes = (seconds / 60) | 0;
        seconds = (seconds - minutes * 60) | 0;
        return (
            String(minutes).padStart(2, "0") +
            ":" +
            String(seconds).padStart(2, "0")
        );
    }

    const songPosition = wrapper.querySelector(".at-song-position");
    let previousTime = -1;
    api.playerPositionChanged.on((e) => {
        // reduce number of UI updates to second changes.
        const currentSeconds = (e.currentTime / 1000) | 0;
        if (currentSeconds == previousTime) {
            return;
        }

        songPosition.innerText =
            formatDuration(e.currentTime) + " / " + formatDuration(e.endTime);
    });

(4)在page.pug中引入alphaTab.pug

extends includes/layout.pug

block content
  #page
    if top_img === false && !page.top_single
      h1.page-title= page.title
    case page.type
       ...其他代码
      when 'alphatab'
        include includes/page/alphatab.pug

(5)一键三连就可以看到效果啦

hexo clean && hexo generate && hexo server -p 5000

效果图如下

https://ljsama.cn/alphatab/?score=https%3A%2F%2Fcdn.ljsama.cn%2Fmusicgp%2F%25E5%258D%2583%25E6%259C%25AC%25E6%25A8%25B1.gpx