项目的背景
最近开始听喜马拉雅播客的内容,但是发现许多不方便的地方。
- 休息的时候收听喜马拉雅,但是还需要不断地选择喜马拉雅的内容,比较麻烦,而且黑灯操作反而伤眼睛。
- 喜马拉雅为代表的播客平台都是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>
界面有点丑
结束语
下一步,开始研究个人定制的音频频道的构建和尝试。感兴趣的可以共同探讨。