如何用Twilio语音和JavaScript建立一个歌曲识别器电话服务(代码示例)

107 阅读12分钟

我们都有过这样的经历:我们听过一首歌,但就是不记得它的名字,或者它就在我们的舌尖上。在这种时候,Shazam是个好办法;打开应用程序,让它听一些音频,它会立即输出歌曲名称和艺术家。

当Shazam首次推出时,它最初只是英国的一项电话服务,你拨打 "2580 "来识别一首歌曲。一旦你拨打了这个号码,你就会把你的手机放在音频附近,30秒后它就会挂断,同时给你发送一条歌曲名称和艺术家的短信。

在发现他们的 "2580 "服务后,我内心的工程师就出来了。我很想知道如何用Twilio的可编程语音短信来构建这个服务,所以我挑战自己,创建一个克隆的服务--做了一些改进

在本教程中,你将学习如何使用Twilio的可编程语音和短信,用Node.js创建一个电话服务来识别歌曲。将用于识别歌曲的API是API Dojo的Shazam API

概述

在我进入教程之前,让我告诉你这个服务将如何工作。

将用于服务的Twilio号码将把所有来电(通过HTTP请求)路由到一个Node.js应用程序,该应用程序将使用Twilio的标记语言(TwiML)来指示和处理这些电话。TwiML提供了一套简单的动词和名词,用来告诉Twilio如何处理你的电话。

Diagram of how incoming voice calls work with Twilio

第一个动词将被用来记录来电,它是 <Record>它将记录一个来电5秒钟,然后返回一个包含录音的文件的URL。然后这个URL将被传递给一个函数,该函数将尝试从音频文件中识别歌曲。

录音电话或语音信息有各种法律上的考虑,你必须确保在录制任何东西时遵守当地、州和联邦法律。

音频文件将被下载,并为(非官方的)Shazam API正确格式化。来自Twilio的音频文件将是一个WAV文件,API要求它是44100赫兹采样的原始数据,所以将使用一个第三方软件包来正确转换该文件。然后,原始数据将作为一个字节数组的Base64编码字符串被发送到API。

然后Shazam API将尝试从Base64字符串中识别歌曲,如果成功的话,将返回歌曲信息(歌曲名称、艺术家、专辑、封面等等)。该 <Hangup>动词将挂断电话,然后应用程序将发送歌曲信息的短信给呼叫者。

如果歌曲没有被检测到,动词将把电话转回 <Redirect>动词将把电话转回使用<Record> 动词的第一个功能,并试图识别歌曲的下一个5秒钟。这个循环将重复进行,直到歌曲被识别。

现在你已经了解了电话服务将如何工作,你可以开始构建它了

设置你的应用程序

创建你的项目结构

首先,在你的首选目录中建立项目的脚手架。在你的终端或命令提示符中,导航到你喜欢的目录并运行以下命令:

mkdir song-identifier
cd song-identifier

安装依赖项

下一步是启动一个全新的Node.js项目,并安装该项目所需的依赖项:

npm init -y
npm install twilio dotenv express wavefile axios

你将需要:

  • twilio使用Twilio可编程语音和短信API的软件包,以接收电话和发送文本信息
  • dotenv访问环境变量,这是你存储Twilio证书和RapidAPI密钥的地方,需要与这两个API互动。
  • 构建服务器的 express包来构建你的服务器:你将在这里编写代码来捕捉和记录所有来电。
  • 对于Shazam API,你将需要 wavefile包来修改录音的声音数据,使其符合API要求的格式;API要求声音数据为44100Hz。
  • 最后,需要一个 axios包来向Shazam API发送请求。

接下来,用你喜欢的文本编辑器打开你的项目目录,创建两个新文件:index.js 和*.env*。

index.js文件是你编写电话服务代码的地方,.env文件将保存你的Rapid API密钥和Twilio凭证。

安全环境变量

打开*.env*文件,将以下几行放入文件中。

TWILIO_NUMBER=XXXXXXXXXX
TWILIO_ACCOUNT_SID=XXXXXXXXXX
TWILIO_AUTH_TOKEN=XXXXXXXXXX
RAPID_API_KEY=XXXXXXXXXX

你需要把XXXXXXXXXX 占位符替换成它们各自的值。

要获得你的Twilio号码、账户SID和Auth Token,请登录Twilio控制台,它将出现在你的仪表盘上。

Twilio console with red box over account info

在添加你的电话号码时,别忘了使用E.164格式

要获得你的RapidAPI密钥,请登录并前往开发者仪表板。然后,在左边标签上的 "我的应用程序"下拉菜单下,导航到你的默认应用程序(应该是自动为你创建的)。你的RapidAPI密钥应该被显示并列为应用密钥

RapidAPI developer dashboard showing an application key

一旦你把所有的XXXXXXXXXX 占位符替换成各自的值,下一步就是建立电话服务。

创建电话服务

在本节中,你将在index.js文件中编写电话服务的代码,在这里你将创建两个路由:/record/identify

/record 途径将捕获一个来电,记录一个5秒钟的通话片段,然后将包含录音的文件的URL传递给/identify 途径。该路由将是重新编排的函数。 */identify*路由将是重新格式化音频文件的函数,并通过Shazam API识别它。

打开index.js 文件,将以下代码放入该文件。

require('dotenv').config();
const twilio = require('twilio')(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const WaveFile = require('wavefile').WaveFile;
const VoiceResponse = require('twilio').twiml.VoiceResponse;
const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.urlencoded({
  extended: true
 }));

这段代码将初始化你之前安装的dotenv,twilio,wavefile,express, 和axios 包。

录制电话

在初始化包的下面,复制并粘贴以下代码:

app.post('/record', async (req, res) => {
   const twiml = new VoiceResponse();
   twiml.record({
       action: '/identify',
       maxLength: '5',
   });
   res.type('text/xml');
   res.send(twiml.toString());
});

这段代码实现了/record 路由,每当向你服务器上的端点发出POST请求时,就会调用该路由。每当从你的Twilio号码收到一个电话,就会发出这个请求。

上面这段代码用TwiML的语音响应对象创建了一个叫做twiml 的变量 创建这个变量后,TwiML用来指示Twilio通过<Record> 这个动词来记录电话。maxLength 属性告诉Twilio将电话记录5秒,action 属性告诉Twilio在电话记录后将其重定向到/identify 路径上

然后通过HTTP响应将指令发回给Twilio。

从录音中识别出歌曲

在你刚刚实现的/record 路由下面,复制并粘贴以下代码:

app.post('/identify', async (req, res) => {
   const twiml = new VoiceResponse();
   let response;

   // Fetch recording by URL.
   // Request needs to be polled since recording may be processing
   const delay = ms => new Promise(res => setTimeout(res, ms));
   while(true) {
       await delay(1000);
       response = await axios.get(req.body.RecordingUrl, { responseType: 'arraybuffer' }).catch(err => {});
       if(response) break;
   }

  // Reformat recording for API
   wav = new WaveFile();
   wav.fromBuffer(response.data);
   wav.toSampleRate(44100);
   const wavBuffer = wav.toBuffer()
   const base64String = new Buffer.from(wavBuffer).toString('base64');

   // If track is identified, send sms of track info. Else, record and identify the next 5 seconds of the song
   const track = await fetchTrack(base64String)
   if(track) {
       sendSMS(track, req.body.Caller)
       await twiml.hangup()
   }
   else {
       twiml.redirect('/record')
   }
   res.type('text/xml');
   res.send(twiml.toString());
});

包含录音的文件的URL被传递到发送到/identify 的请求正文中,并存储在req.body.RecordingUrl 变量中。然后,axios 被用来发送HTTP GET请求来抓取该文件。

你可能想知道为什么对RecordingUrl 的请求要通过一个循环轮询。在某些情况下,对URL的立即请求可能会失败,因为录音可能仍处于处理阶段。

<Record> 动词提供了recordingStatusCallback ,它可以发出一个HTTP请求,并在录音可以访问时运行/identify 路线。然而,应用程序将需要决定在尝试识别歌曲后如何处理该电话。电话不能被重定向到这个回调方法。它只会被重定向到action 属性中的URL。

然后文件以44,100赫兹重新取样,然后转换为原始数据,再转换为Base 64字符串。这个字符串(base64String)然后被传入fetchTrack() 函数,以使用Shazam API识别歌曲。

如果歌曲被识别,轨道将被返回,sendSMS() 函数将被用来发送歌曲信息给调用者。如果歌曲没有被识别,<Redirect> 动词将被调用,以重定向调用到/record 来识别歌曲的下一个5秒。

辅助功能

在*/identify*路线的下面,放置fetchTrack() 函数:

async function fetchTrack(base64String) {
    const options = {
        method: 'POST',
        url: 'https://shazam.p.rapidapi.com/songs/v2/detect',
        headers: {
          'content-type': 'text/plain',
          'X-RapidAPI-Key': process.env.RAPID_API_KEY,
          'X-RapidAPI-Host': 'shazam.p.rapidapi.com'
        },
        data: base64String,
      };
      
      const response = await axios.request(options)
      .catch(function (error) {
          console.error(error);
      });
      if(response.data.matches.length) return response.data.track;
      else return null;
}

这个函数(在/identify 路由中使用)将向Shazam API的 /songs/v2/detect端点发送一个POST请求。这个请求的主体将包含位于base64String 变量中的音频记录的采样原始数据。如果API返回匹配的歌曲,它将返回该歌曲的信息。

接下来,将最后的代码块附加到index.js文件中:

async function sendSMS(track, caller) {
    twilio.messages
  .create({
     body: `Song detected: ${track.title} - ${track.subtitle}\n\n${track.url}`,
     from: process.env.TWILIO_NUMBER,
     mediaUrl: [track.images.coverart],
     to: caller
   }).then(message => console.log(message.sid));
}

app.listen(3000, () => {
    console.log(`Listening on port 3000`);
});

这个代码块包括sendSMS() 函数(在/identify 路径中使用),并从Shazam API中接收一个歌曲轨道。短信将包含歌曲名称、艺术家、封面和歌曲的Shazam URL,将被发送给调用者。

代码块的最后一点将启动Express服务器并监听3000端口的请求。

部署电话服务

在生产环境中,建议在云服务器上运行你的Node.js应用程序。然而,为了简化本教程的部署,你将在自己的电脑上部署你的应用程序。

ngrok将被用来将你的Express服务器连接到互联网上,它将生成一个公共的URL,将所有的请求直接传输到你的电脑上。这个公共URL将被配置到你的Twilio控制台的Twilio号码上,这样所有的电话将被路由到你的应用程序。

回到你的终端,运行以下命令:

node index.js

这个命令将运行index.js文件,它将在你的电脑的3000端口上启动一个本地Express服务器。

在终端上写一个新标签,导航到你的项目目录,并运行以下命令:

ngrok http 3000

然后,你的终端将看起来像下面这样:

Terminal response after running ngrok command.

你会看到ngrok已经生成了两个转发URL到你的3000端口的本地服务器(在某些情况下,可能只显示一个URL)。复制其中一个URL--建议使用https URL,因为它是加密的--因为其中一个需要插入到你的Twilio号码的信息设置中。

导航到你的Twilio控制台的**活动号码** 部分 。你可以从控制台的左边标签点击电话号码>管理>活动号码

现在,点击你想用于电话服务的Twilio号码,向下滚动到语音和传真部分。在A CALL COMES IN下面,在第一个下拉框中选择Webhook,然后在下一个文本框中,输入你的转发URL,后面是"/record"(见下面的URL应该是什么样子)。

Twilio phone number settings in Twilio console with ngrok forwarding URL within the webhook textbox

一旦你配置好了你的Twilio号码,以参考你的Express服务器,点击蓝色的保存按钮。

一旦保存完毕,你的歌曲识别器电话服务就可以使用了!开始播放一首歌曲,然后拨打你的Twilio号码,并把你的手机放在扬声器附近。一旦电话挂断,你会得到一个短信回复,看起来像这样。

Phone screenshot of messages response from Twilio number showing a song cover art, title, artist name and shazam link.

进一步改进

这项电话服务不仅复制了Shazam的 "2580 "服务,还进行了一些升级。该电话以5秒为单位记录音频,一旦从其中一个记录中检测到歌曲,就会挂断,而不是只记录30秒就挂断。这项服务还输出歌曲的封面和Shazam链接,而不仅仅是歌名和名称。

尽管这项电话服务是一个伟大的开始,但仍有改进的余地。音频传输的电话标准被固定为8位PCM,采样率为8000hz。与使用语音备忘录应用程序的语音记录相比,这种音频的质量非常差,所以在很多情况下,歌曲的传输效果很差,无法检测。

为了提高音频质量,可以使用WhatsApp Business API与Twilio将电话服务转换为WhatsApp服务,因为语音备忘录可以通过WhatsApp录制。然后,这个语音备忘录可以发送到你在WhatsApp上的Twilio号码,在那里它可以被你的Node.js应用程序读取。

另一个改进是,当录音还没有准备好时,删除对录音URL的轮询请求。该请求实际上会因为其他原因而失败,并无限循环,这并不好。

为了解决这个问题,所有的调用都可以用录音的URL和它的状态进行缓存。/record 路由可以被修改,这样一旦它完成了录音,它就会通过将action 设置为空白来继续路由到自己。该 recordingStatusCallback可以用来更新缓存中的状态,所以一旦录音被处理,action ,然后可以改变为*/identify* 路由。

总结

祝贺你!你刚刚通过Twilio建立了一个类似Shazam的电话服务!🎉

尽管下载和使用Shazam应用程序要容易得多,但如果有Twilio在身边,那就真的没有什么乐趣了。我希望你能从这个教程中得到一些乐趣,并在此过程中学到一些东西