18岁生日

2026-09-13 22:47:46

今天是我的十八岁生日。

最近我一直在折腾我 vibecoding 的 xlgui 框架,我想,要不就自己用自己的xlgui写个demo试试,有一个词形容这种行为,叫dogfooding,这两天我一直在写这个,一开始我只想单曲播放,后面把列表和专辑封面,lrc支持加了上去。久违的手写代码,而不是ai agent。

我下了一堆几十mb的flac用着我写的这个听歌。有意思的是,不少flac在windows 11自带的媒体播放器里打不开,反而在我的player下正常。

我懒得说了,直接把代码贴着。

#include <xlgui/xlgui.hpp>
#include <fmt/format.h>
#include <SFML/Audio.hpp>
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <random>
#include <regex>

using namespace xlgui::dsl;

namespace xl=xlgui;

namespace fs=std::filesystem;

std::vector<std::string> sfmlAudioSupportedTypes = {
    ".mp3", ".ogg", ".flac", ".wav"
};

class AppVM
{ public:
    inline static Ref<sf::Time>    totalPlayTime;
    inline static Ref<sf::Time>    currentPlayTime;
    inline static Ref<std::string> songName    = "未播放歌曲";
    inline static Ref<std::string> artistName  = "拖拽音乐文件至此以播放";
    inline static Ref<std::string> albumName   = "";
    inline static Ref<bool>        isPlaying   = false;
    inline static Ref<float>       volume      = 1.0f;
    inline static Ref<std::vector<fs::path>>
                                   playList    = {};
    inline static Ref<std::vector<unsigned char>> coverData;
    inline static Ref<bool>        isSongHaveCover = false;
    inline static Ref<std::string> currentPlayFilePath = "";
    inline static Ref<int>         currentPlayListIdx  = 0;
    inline static std::vector<std::pair<std::chrono::microseconds, std::string>> currentPlayLyrics;
    inline static Ref<int>         currentPlayLyricsIdx = 0;
    inline static Ref<std::string> currentPlayLyricLine = "";

    enum class PlayListRule {
        Loop = 0,
        LoopSong,
        Count,
    };

    inline static Ref<PlayListRule> playListRule = PlayListRule::Loop;

    inline static Ref<bool> isRandom = false;

    inline static sf::Music music;

    inline static std::mt19937& GetRandomDevice() {
        thread_local std::mt19937 rd(std::random_device{}());
        return rd;
    }

    static void ResetCurrentPlayTime()
    {
        currentPlayTime = sf::Time::Zero;
    }

    static void Play(const std::filesystem::path& path, bool scan = true)
    {        
        if (!music.openFromFile(path))
        {
            XL_WARN("play failed, {}", path.string());
            currentPlayFilePath = "";
            songName = "无法播放的文件类型";
            artistName = "";
            totalPlayTime = sf::Time::Zero;
            currentPlayTime = sf::Time::Zero;
            isPlaying = false;
            currentPlayLyricLine = "";
        } else {
            ResetCurrentPlayTime();
            currentPlayFilePath = path.string();
            if (scan) {
                LoadToPlayList(path);
            }
            music.play();
            UpdateSongInfo(path);
            isPlaying = true;
        }
    }

    static void Play()
    {
        if (currentPlayFilePath.Get() != "") {
            music.play();
            isPlaying = true;
        }
    }

    static void Play(int idx)
    {
        if (playList.Get().size()==0) {return;}

        if (idx >= playList.Get().size()) { idx=0; }
        else if (idx < 0) { idx=playList.Get().size()-1; }
        
        Play(playList.Get()[idx], false);
        currentPlayListIdx = idx;
    }

    static void Pause()
    {
        if (isPlaying)
        {
            music.pause();
            isPlaying = false;
        }
    }

    static void TogglePlay()
    {
        if (isPlaying)
        {
            Pause();
        } else {
            Play();
        }
    }

    // lrc
    static void LoadLyrics(const std::filesystem::path& path)
    {
        currentPlayLyricsIdx = 0;
        currentPlayLyrics.clear();
        currentPlayLyricLine = "";
        
        std::ifstream file(path);
        if (!file.is_open()) {
            return;
        }

        std::string buffer;
        while (std::getline(file, buffer))
        {
            static const std::regex pattern(R"(\[(\d{2}):(\d{2})\.(\d{2,3})\](.*))");
            std::smatch sm;
            if (std::regex_match(buffer, sm, pattern))
            {
                auto ms_str = sm[3].str();
                auto ms = stoi(sm[3]);
                if (ms_str.length() == 2) {
                    ms *= 10;
                }
                currentPlayLyrics.push_back({
                    std::chrono::minutes(std::stoi(sm[1])) +
                    std::chrono::seconds(std::stoi(sm[2])) +
                    std::chrono::milliseconds(ms),
                    sm[4]
                });
            }
        }
    }

    static void UpdateSongInfo(const std::filesystem::path& path)
    {
        TagLib::FileRef file(path.c_str(), false);
        if (!file.isNull() && file.tag() && file.file())
        {
            songName   = file.tag()->title().toCString(true);
            artistName = file.tag()->artist().toCString(true);
            TagLib::List<TagLib::VariantMap> pictures = file.file()->complexProperties("PICTURE");
            if (pictures.isEmpty())
            {
                isSongHaveCover = false;
                (*coverData).clear();
            } else {
                // 不耗时,直接同步
                const auto& picMap = pictures.front();

                TagLib::ByteVector imageData = picMap["data"].toByteVector();
                const auto* dataPtr = reinterpret_cast<const unsigned char*>(imageData.data());
                coverData = std::vector<unsigned char>(dataPtr, dataPtr + imageData.size());

                isSongHaveCover = true;
            }
        } else {
            isSongHaveCover = false;
            (*coverData).clear();
            songName   = reinterpret_cast<const char*>(path.stem().u8string().c_str());
            (*artistName).clear();
        }
        // 如果没获取到直接文件名赋给songName(无后缀)
        if (songName.Get() == "") {
            // c++ 20 shabi
            songName   = reinterpret_cast<const char*>(path.stem().u8string().c_str());
        }

        auto lrcpath = path;
        lrcpath.replace_extension(".lrc");
        LoadLyrics(lrcpath);

        // fallback
        if (currentPlayLyrics.empty())
        {
            lrcpath =  path.parent_path() / "lyrics" / path.filename();
            lrcpath.replace_extension(".lrc");
            LoadLyrics(lrcpath);
        }
        if (currentPlayLyrics.empty())
        {
            lrcpath =  path.parent_path() / "lrc" / path.filename();
            lrcpath.replace_extension(".lrc");
            LoadLyrics(lrcpath);
        }
        
        totalPlayTime = music.getDuration();
    }

    static void StepBack(float val)
    {
        if (music.getPlayingOffset() - val * music.getDuration() >= sf::Time::Zero)
            music.setPlayingOffset(music.getPlayingOffset() - val * music.getDuration());
        SyncLyrics();
    }

    static void StepForward(float val)
    {
        music.setPlayingOffset(music.getPlayingOffset() + val * music.getDuration());
        SyncLyrics();
    }

    static void ChangeCurrentPlayTime(float val)
    {
        auto offset = val * music.getDuration();
        currentPlayTime = offset;
        music.setPlayingOffset(currentPlayTime);
        SyncLyrics();
    }

    static void SetVolume(float val)
    {
        if (val < 0.0f) { val = 0.0f; }
        else if (val > 1.0f) { val = 1.0f; }
        volume = val;
        music.setVolume(volume * 100.0f);
    }

    static void SetPlayListRule(PlayListRule rule) { playListRule = rule; }

    static void SetRandom(bool random) { isRandom = random; }

    static void LoadToPlayList(const std::filesystem::path& path)
    {
        fs::path p;
        if (!fs::is_directory(path))
        {
            p = path.parent_path();
        } else {
            p = path;
        }
    
        std::vector<fs::path> playlist;
    
        auto it = fs::directory_iterator(p);
        int idx = 0, curridx = 0;
        for (const auto& e : it)
        {
            if (e.is_regular_file())
            {
                for (auto& t : sfmlAudioSupportedTypes)
                {
                    auto ext = e.path().extension().string();
                    for (char& c : ext)
                    {
                        c = std::tolower(static_cast<unsigned char>(c));
                    }
                    if (ext == t)
                    {
                        playlist.push_back(e.path());
                        if (path == e.path()) { curridx = idx; }
                        idx++;
                    }
                }
            }
        }
    
        playList = playlist;
        currentPlayListIdx = curridx;
    }

    static void NextSong(bool is_force = false)
    {
        if (playListRule == PlayListRule::LoopSong) {
            if (!is_force) {
                Play();
                return;
            }
        }
        else
        {
            if (isRandom) {
                std::uniform_int_distribution<int> dis(0, playList.Get().size()-1);
                Play(dis(GetRandomDevice()));
                return;
            }
        }
        Play(currentPlayListIdx+1);
    }

    static void PrevSong(bool is_force = false)
    {
        if (isRandom) {
            std::uniform_int_distribution<int> dis(0, playList.Get().size()-1);
            Play(dis(GetRandomDevice()));
            return;
        }
        Play(currentPlayListIdx-1);
    }

    inline static void Sync()
    {
        if (isPlaying) {
            currentPlayTime = music.getPlayingOffset();
            if (music.getStatus() == sf::Music::Status::Stopped)
            {
                NextSong();
            }
            SyncLyrics();
        }
    }

    inline static void SyncLyrics()
    {
       if (currentPlayLyrics.empty()) {
            return;
        }

        auto currms = std::chrono::microseconds(currentPlayTime.Get().asMicroseconds());

        // 找到第一个大于当前时间的歌词
        auto it = std::upper_bound(
            currentPlayLyrics.begin(),
            currentPlayLyrics.end(),
            currms,
            [](const std::chrono::microseconds& time, const auto& item) {
                return time < item.first;
            }
        );

        if (it == currentPlayLyrics.begin()) {
            // 歌曲前奏阶段显示artistName
            currentPlayLyricsIdx = 0;
            currentPlayLyricLine = "";
        } else {
            // 找到的歌词是it的前一个位置
            int idx = static_cast<int>(std::distance(currentPlayLyrics.begin(), it) - 1);
            currentPlayLyricsIdx = idx;
            currentPlayLyricLine = currentPlayLyrics[idx].second;
        }
    }
};


std::string timeToStr(const sf::Time& time)
{
    return fmt::format("{:02d}:{:02d}", static_cast<int>(time.asSeconds() / 60)
    , static_cast<int>((int)time.asSeconds() % 60));
}

void applyAppStyles(xl::Application& app)
{
    static bool initStyle = [&app](){
        WidgetRef(app.getRoot())
        .select(".player-area button")
            .padding(4, 4).radius(50_pct).bg(Color::Transparent)
            .fontSize(20)
        .end();

        return true;
    }();

    if (xl::IsSystemLightMode()) {
        WidgetRef(app.getRoot())
        .select(".player-area button").fg(Color::Black)
        .select(".artist-text").fg("#3a3939")
        ;
    } else {
        WidgetRef(app.getRoot())
        .select(".player-area button").fg(Color::White)
        .select(".artist-text").fg("#dbdbdb")
        ;
    }
}

int main(int argc, char** argv)
{
    xl::Application app("xl Music Player");
    app.Backdrop(WindowBackdropType::Acrylic, IsSystemDarkMode());
    app.backdropTint(Color::Transparent);

    app.AddChildren({
        Spacer(),
        Flex({
        VStack({
            HStack({
                    ImageBox().id("cover-image").radius(16).width(320).height(320)
                    .shadow(
                        0, 2, 4, Color("#0000008f")
                    )
                              .src(AppVM::coverData)
                }).hcenter().visible(AppVM::isSongHaveCover).marginBottom(16)
                ,
            VStack({
                Text(AppVM::songName).fontSize(26),
                Text([](){
                    if (*AppVM::currentPlayLyricLine != "") {
                        if (*AppVM::artistName == "") {
                            return *AppVM::currentPlayLyricLine;
                        }
                        return fmt::format("{}: {}",
                        *AppVM::artistName,
                        *AppVM::currentPlayLyricLine);
                    }
                    return AppVM::artistName.Get();
                }).fontSize(18).className("artist-text"),
                // Text(AppVM::currentPlayLyricLine).fontSize(16)
            }),
            VStack({
                HStack({
                    Text([](){ return timeToStr(AppVM::currentPlayTime); }),
                    Slider([](){
                        return AppVM::currentPlayTime.Get().asSeconds() 
                          / AppVM::totalPlayTime.Get().asSeconds();
                    }).width(40_vw).minWidth(320_dp)
                        .onValueChanged([](float val){
                            AppVM::ChangeCurrentPlayTime(val);
                    }),
                    Text([](){ return timeToStr(AppVM::totalPlayTime); }),
                }),
                HStack({
                    Button("🔀").bg([](){ return AppVM::isRandom ? Color("#2563eb") : Color::Transparent;})
                    .onclick([](){ AppVM::isRandom = !AppVM::isRandom; }),
                    Button([](){
                        if (AppVM::playListRule == AppVM::PlayListRule::Loop)
                        { return "🔁"; }
                        return "🔂";
                    }).onclick([](){
                        int current = static_cast<int>(AppVM::playListRule.Get());
                        int total   = static_cast<int>(AppVM::PlayListRule::Count);
                        AppVM::playListRule = static_cast<AppVM::PlayListRule>((current + 1) % total);
                    }),
                    Button("⏮").onclick([](){
                        AppVM::PrevSong(true);
                    }),
                    Button([](){
                        return AppVM::isPlaying ?
                        "⏸" : "▶";
                    }).onclick([](){
                        if (AppVM::isPlaying)
                        { AppVM::Pause(); } else { AppVM::Play(); }
                    }),
                    Button("⏭").onclick([](){
                        AppVM::NextSong(true);
                    }),
                    HStack({
                        Button([](){
                            if (AppVM::volume <= 0) return "🔇";
                            else if (AppVM:: volume <= 0.3f) return "🔈";
                            else if (AppVM:: volume <= 0.6f) return "🔉";
                            else if (AppVM:: volume <= 1.0f) return "🔊";
                            return "🔊";
                        }).onclick([]()
                             { static float before = 0.5f;
                               if (AppVM::volume <= 0) {
                                AppVM::SetVolume(before > 0.0f ? before : 0.5f);
                               } else {
                                before = AppVM::volume.Get();
                                AppVM::SetVolume(0.0f);
                               } }),
                        Flex({
                                Slider(AppVM::volume).onValueChanged([](float val){
                                    AppVM::SetVolume(val);
                                }).width(100_pct).marginRight(8)
                            }).bg("#c2c2c265").radius(12)
                    }),
                    Button("◨").onclick([&app](){
                        static bool isOpen = false;
                        isOpen = !isOpen;
                        WidgetRef(app.GetChild("#playlist-view")).visible(isOpen);
                        WidgetRef(app.GetChild("#main")).justifyContent(isOpen ? JustifyContent::FlexEnd : JustifyContent::Center);
                    }),
                }).hcenter().className("player-area"),
            }).hcenter(),
        }).center().textAlign(TextAlign::Center),
        HStack({
        List([](){
                    std::vector<std::string> r;
                    r.reserve(AppVM::playList.Get().size());
                    for (auto& i : AppVM::playList.Get())
                    {
                        r.push_back(reinterpret_cast<const char*>(i.stem().u8string().c_str()));
                    }
                    return r;
            })
              .radius(12).bg("#c2c2c23b")
              .selected(AppVM::currentPlayListIdx)
              .onselect([](int idx){
                AppVM::Play(idx);
            }).width(30_vw).height(100_vh)
        }).visible(false).id("playlist-view").paddingLeft(10_pct)
        })
        .width(100_vw).height(100_vh).alignItems(Alignment::Center).justifyContent(JustifyContent::Center).id("main"),
        Spacer(),
    });

    applyAppStyles(app);

    app.OnSystemThemeChanged([&app](){
        applyAppStyles(app);
    });

    app.OnDropFile([](const std::string& path) {
        AppVM::Play(std::filesystem::u8path(path));
    });

    app.SetInterval([]() mutable {
        AppVM::Sync();
    }, 1000);

    app.onKeyDown([&app](xl::Key k){
        if (k == xl::Key::Space)
        {
            AppVM::TogglePlay();
        }
        if (k == xl::Key::Left || k == xl::Key::A)
        {
            AppVM::StepBack(0.01);
            AppVM::Sync();
        }
        if (k == xl::Key::Right || k == xl::Key::D)
        {
            AppVM::StepForward(0.01);
            AppVM::Sync();
        }
        if (k == xl::Key::Up)
        {
            AppVM::SetVolume(AppVM::volume+0.05f);
        }
        if (k == xl::Key::Down)
        {
            AppVM::SetVolume(AppVM::volume-0.05f);
        }
    });

    // 大鸡巴猛火爆操微软
    #ifndef _WIN32
    if (argc > 1) {
        AppVM::Play(std::filesystem::u8path(argv[1]));
    }
    #endif

    app.Run();
}

$$\downarrow$$

播放中 Usage (music_player.exe)

我懒得写什么了,我想睡觉。

我还想了下 xlgui 的哲学:

$$ \text{State} \leftrightarrow \text{Interface} \leftrightarrow \text{Actor} $$ $$ \quad\downarrow\quad $$ $$ \text{State} \leftrightarrow \text{UI} \leftrightarrow \text{Human} $$ $$ \quad\downarrow\quad $$ $$ \text{State} \leftrightarrow \text{xlgui} \leftrightarrow \text{Human} $$

在我看来,这个世界可以被抽象为state,一个随行动不断变化的state,比如说你拿起一个桌上的水杯,你的感官(向你)、四肢(向外)充当了interface,你发出神经传导,让你的手去拿水杯,水杯的位置被改变(state changed),同理你的感官也捕捉到了水杯位置的变化。

同理,ui也是一种interface,它的作用就是负责人与state的交互,作为一种桥梁,这有点像操作系统的比喻,说自己是硬件和软件的桥梁。在我看来,好的state<->ui(interface)设计,即是就算ui变成另一个interface,无需改变state逻辑,也能够与interface相连接,interface作为桥梁与人或其它actor相连。

我懒得写了。最后祝我这未来十年里,睡一万个18-20岁的漂亮日本女生,买辆Supra,买辆保时捷,存款有个千万。

Copyright © 2026 后术. All rights reserved.