由播客转向个人定制的音频频道(1)平台搭建

项目的背景

最近开始听喜马拉雅播客的内容,但是发现许多不方便的地方。

  • 休息的时候收听喜马拉雅,但是还需要不断地选择喜马拉雅的内容,比较麻烦,而且黑灯操作反而伤眼睛。
  • 喜马拉雅为代表的播客平台都是VOD 形式的,需要选择内容收听,有时候想听科技方面的访谈,但是许多访谈节目并不是连续更新播放的。免不了要去主动查找。
  • 休闲或者开车时,希望收听收音机那样轻松一点地享受电台安排的节目,但是目前的电台广告太多,内容匮乏。
  • 家里的老人更不习惯手机操作。老人目前主要是看短视频。

笔者看来,播客是一个被低估的服务,其实依靠短视频很难接收有效的信息,靠几分钟很难讲清楚一个观点和知识。所以,要完整地了解一些有用的内容,语音比短视频更好。

那么,能否通过AI 推荐技术,讲播客内容主动生成个人定制的音频频道吗?理论上是可能的,也十分有趣。作为一名创客,我想试试。

说干就干!本文介绍实验平台的搭建。

基于ardunio ESP32 的选台器

电子设备中经常使用旋钮来选择参数,最简单的是旋钮是电位器,它是一个滑动电阻,高端家电,汽车中使用的是编码器Encoder。编码器输出的是脉冲信号。本文介绍如何使用Ardunio ESP32-Nano 来设计一个蓝牙旋转编码器。

外观设计

硬件设计

细节

编码器

下面是日本ALPS 公司的中空旋转编码器EC35A。

三个引脚分别是A,C,B。

接线图

最好在编码器脉冲计数处理回路中设置下图所示的滤波器。

与Ardunio 的连接图。

代码

目前的代码使用了蓝牙mouse 仿真,最终的程序也许要改成ble server 。

cpp 复制代码
#include <BleMouse.h>
BleMouse bleMouse;
// Define the pins used for the encoder
const int encoderPinA = 11;
const int encoderPinB = 10;

// Variables to keep the current and last state
volatile int encoderPosCount = 0;
int lastEncoded = 0;
int MAX=60;
int channel=-1;
void setup() {
  Serial.begin(115200);

  // Set encoder pins as input with pull-up resistors
  pinMode(encoderPinA, INPUT_PULLUP); 
  pinMode(encoderPinB, INPUT_PULLUP);

  // Attach interrupts to the encoder pins
 attachInterrupt(digitalPinToInterrupt(encoderPinA), updateEncoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(encoderPinB), updateEncoder, CHANGE);
   bleMouse.begin();
}

void loop() {
   
  static int lastReportedPos = -1; // Store the last reported position
  if (encoderPosCount != lastReportedPos) {
    if((encoderPosCount/2)>channel) {
      channel++; 
    Serial.print("Encoder Position: ");
    Serial.println(channel);
    bleMouse.move(0,0,1);
    };
       if((encoderPosCount/2)<channel) {
      channel--; 
    Serial.print("Encoder Position: ");
    Serial.println(channel);
    bleMouse.move(0,0,-1);
    }
    lastReportedPos = encoderPosCount;
    }
     
  delay(500) ;
  
}

void updateEncoder() {
  
  int MSB = digitalRead(encoderPinA); // MSB = most significant bit
  int LSB = digitalRead(encoderPinB); // LSB = least significant bit
  int encoded = (MSB << 1) | LSB; // Converting the 2 pin value to single number
  int sum  = (lastEncoded << 2) | encoded; // Adding it to the previous encoded value

  if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011){
    
    if (encoderPosCount==MAX) encoderPosCount=0;
    else
      encoderPosCount++;
    } 
  if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000){
   if (encoderPosCount==0) encoderPosCount=MAX;
   else
    encoderPosCount--;
   
    } 

  lastEncoded = encoded; // Store this value for next time
}

ardunio-ESP32-nano 开发板

外观

引脚图

HLS 网络电台的构建

HLS 全称是 HTTP Live Streaming, 是一个由 Apple 公司实现的基于 HTTP 的媒体流传输协议。 借助 HLS,视频和音频内容被分解为一系列块,经过压缩以便快速交付,并通过 HTTP 传输到最终用户的设备。

HLS 由一个m3u8 文件和多个ts 文件构成的。

节目源

网络上有许多网络广播电台的m3u8 的节目源地址,有的可以播放,有的不行。我们下载之后转换成为CSV 格式,然后通过CSV2JSON.js 软件转化为json 文件.下面是一部分

javascript 复制代码
[
    {
        "StationName": "本地音乐台",
        "URL": "media/1.m3u8"
    },
    {
        "StationName": "CGTN Radio",
        "URL": "http://sk.cri.cn/am846.m3u8"
    },
    {
        "StationName": "CRI环球资讯广播",
        "URL": "http://satellitepull.cnr.cn/live/wxhqzx01/playlist.m3u8"
    },
    {
        "StationName": "CRI华语环球广播",
        "URL": "http://sk.cri.cn/hyhq.m3u8"
    },
    {
        "StationName": "CRI南海之声",
        "URL": "http://sk.cri.cn/nhzs.m3u8"
    },
    {
        "StationName": "CRI世界华声",
        "URL": "http://sk.cri.cn/hxfh.m3u8"
    }]

音频分发服务器

构建了一个HLS 音频测试平台,用于测试。

  • 后台nodeJS 编写
  • 前端 使用hlv.js 插件

自制HLS 媒体

除了网络上的节目源之外,我们也制作了一些测试语音媒体。要使用ffmpeg 工具转换。

使用ffmpeg 将mp3 转换成m3u8 的分段(1.mp3 )

复制代码
ffmpeg -i 1.mp3 -c:v libx264 -c:a aac -strict -2 -f hls -hls_list_size 2 -hls_time 15 1.m3u8

生成的效果是:

将 1.mp3 视频文件每 15 秒生成一个 ts 文件,最后生成一个 m3u8 文件(1.m3u8),m3u8 文件是 ts 的索引文件。和两个ts( 1.m3u8 ,10.ts 和11.ts)

将生成的文件放置在nodeJS/public/media 目录中。

我的代码

NodeJS代码

javascript 复制代码
import express from 'express';
import path from 'path'
import url from 'url'
import   fs   from 'fs';
const router = express.Router();
const app = express();
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json())

router.get('/index', function (req, res) {
    res.sendFile(path.join(__dirname + '/views/index.html'));
});
router.post('/getStations', async function (req, res) {
    Request = req.body;
    //   console.log(Request)
    const Method = Request.Method;
    const Stations=JSON.parse(fs.readFileSync("public/doc/StationTable.json",'utf8'))
    
    console.log(Stations)
    res.send(JSON.stringify({
        Method: "getStations",
        Result: { Status: "OK", Stations: JSON.stringify(Stations) }
    }))
});
app.use('/', router);
app.listen(process.env.port || 3000);
console.log('Running at Port 3000');

前端Index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Radio Player</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/font-awesome.min.css">
    <link rel="stylesheet" href="font/bootstrap-icons.css">
    <script src="js/jquery-3.4.1.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="js/hls.js"></script>
    <script src="js/jquery.mousewheel.min.js"></script>
    <style>
        select {
            font-size: 24px;
            width: 320px;
        }
        audio {
            width: 320px;
        }
    </style>
    <script>
        var hls
        var CurrentChannel=0
        $(document).ready(function () {
            $("#Title").click()
            var audio = document.getElementById('audio');
            hls = new Hls();
            $("#Title").mousewheel(Mousewheel)         
            getStations()

        });
        var Stations = null
       
        function Mousewheel(event){
            console.log(event.deltaX, event.deltaY, event.deltaFactor);
            if (event.deltaY>0) {
                CurrentChannel++;
                if (CurrentChannel==64) CurrentChannel==0
            }
            else  {
                if (CurrentChannel==0) CurrentChannel=64
                else
                CurrentChannel--;
            }
            $("#Selection").get(0).selectedIndex=CurrentChannel
            Selected()
        }
        function getStations() {
            var parameter = {
                Method: "getStations",
            }
            $.ajax({
                url: "/getStations",
                type: 'post',
                contentType: "application/json",
                dataType: "json",
                data: JSON.stringify(parameter),
                success: function (response) {
                    Stations = JSON.parse(response.Result.Stations)
                    console.log(Stations)
                    for (let i = 0; i < Stations.length; i++) {
                        $("#Selection").append("<option>" + Stations[i].StationName + "</option>")
                    }
                    $("#Selection").get(0).selectedIndex=CurrentChannel
                    Selected()
                }
            })
        }
        function Selected() {
            const StationName = $("#Selection").val()
            const Index = $("#Selection").get(0).selectedIndex
            CurrentChannel=Index
            console.log(StationName)
            console.log(Index)
            if (Hls.isSupported()) {

                var audio = document.getElementById('audio');

                hls.loadSource(Stations[Index].URL);
                hls.attachMedia(audio);
                hls.on(Hls.Events.MANIFEST_PARSED, function () {
                    audio.play();
                });
            }
        }
    </script>

</head>

<body>
    <div class="container" >
        <h1 class="text-info" id="Title">网络收音机</h1>
        <div class="form-group">
        <h3 class="text-info">选台</h3>
        <select class="form-select   " aria-label="Default select" id="Selection" onchange="Selected()"></select>
        </div>
        <div>
        <h3 class="text-info">播放器</h3>
        <audio id="audio" controls ></audio>
         
       </div>
    </div>
</body>

</html>

界面有点丑

结束语

下一步,开始研究个人定制的音频频道的构建和尝试。感兴趣的可以共同探讨。

相关推荐
回眸&啤酒鸭9 小时前
【回眸】AI新鲜事(二)使用Copilot制定一个60天的系统学习AI计划细化落实到每天并导入Outlook日程?
ai·outlook
AI_56789 小时前
阿里云OSS成本优化:生命周期规则+分层存储省70%
运维·数据库·人工智能·ai
空白诗9 小时前
高级进阶 React Native 鸿蒙跨平台开发:slider 滑块组件 - 进度条与评分系统
javascript·react native·react.js
晓得迷路了9 小时前
栗子前端技术周刊第 116 期 - 2025 JS 状态调查结果、Babel 7.29.0、Vue Router 5...
前端·javascript·vue.js
How_doyou_do9 小时前
执行上下文、作用域、闭包 patch
javascript
叫我一声阿雷吧10 小时前
深入理解JavaScript作用域和闭包,解决变量访问问题
开发语言·javascript·ecmascript
iDao技术魔方10 小时前
深入Vue 3响应式系统:为什么嵌套对象修改后界面不更新?
javascript·vue.js·ecmascript
历程里程碑10 小时前
普通数组-----除了自身以外数组的乘积
大数据·javascript·python·算法·elasticsearch·搜索引擎·flask
摸鱼的春哥10 小时前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
念念不忘 必有回响10 小时前
viepress:vue组件展示和源码功能
前端·javascript·vue.js