팀 프로젝트로 음악퀴즈 사이트를 만들게 되었다.
놀토에서 나오는 간식퀴즈같이 기계음등으로 음정이나 박자 없이 가사를 그대로 말해주면 노래 제목을 맞추는 퀴즈사이트이다.
나는 우선 이 사이트의 구현이 가능한지 알아보기 위해 사전조사 단계로
1)텍스트를 기계음으로 바꿔주는 api,
2)브라우저에서 재생
을 알아보기로 했다.
먼저 1번의 경우 Google의 TTS API를 사용하기로 했다.
이 글에서는 2)브라우저에서 재생을 한 과정을 써보겠다.
구글 Cloud Text-to-Speech사이트에 들어가서 (가입이나 이런 절차는 생략) 빠른시작:클라이언트 라이브러리 사용에 들어가면 기본적인 사용방법이 나온다. api key를 발급받고, 클라이언트 라이브러리 설치 코드 얘시가 나와있다. 나는 Node.js를 이용할 거기 때문에 Node.js환경의 예시를 보았다.
npm install --save @google-cloud/text-to-speech //라이브러리 설치
// Imports the Google Cloud client library
const textToSpeech = require('@google-cloud/text-to-speech');
// Import other required libraries
const fs = require('fs');
const util = require('util');
// Creates a client
const client = new textToSpeech.TextToSpeechClient();
async function quickStart() {
// The text to synthesize
const text = 'hello, world!';
// Construct the request
const request = {
input: {text: text},
// Select the language and SSML voice gender (optional)
voice: {languageCode: 'en-US', ssmlGender: 'NEUTRAL'},
// select the type of audio encoding
audioConfig: {audioEncoding: 'MP3'},
};
// Performs the text-to-speech request
const [response] = await client.synthesizeSpeech(request);
// Write the binary audio content to a local file
const writeFile = util.promisify(fs.writeFile);
await writeFile('output.mp3', response.audioContent, 'binary');
console.log('Audio content written to file: output.mp3');
}
quickStart();
이런 예시가 나와있다. test.js
란 파일을 만들어 그대로 입력해주고 node test.js 를 통해 실행시켜 보았다. 그러자 output.mp3라는 오디오 파일이 만들어졌다.(이 파일이 있는 위치에)
원래는 ouput.mp3만 생기는데 내가 실험하느라 ouput.wav도 있는거다.
이 파일을 재생하면 api에 요청한 텍스트인 hello world가 기계음으로 흘러 나온다.
이제 이 파일을 클라이언트에서 재생할 수 있도록 만들어야 한다.
참조 사이트 이 사이트에 정말로 정리가 잘되있고 훌륭한 앱을 만들어 놓아서 많이 참고했다.
먼저 서버에 있는 오디오 파일을 클라이언트로 보내 클라이언트에서 재생할 수 있게 만드는 것부터 시작했다.
연습용으로 soundTest라는 디렉토리를 하나 만들었다. 그리고 그안에 server와 client로 구분해 줬다.
먼저 간단하게 서버를 만들어 주었다.
const express = require('express');
const app = express();
const path = require('path');
const cors = require('cors');
const textToSpeech = require('@google-cloud/text-to-speech');
require('dotenv').config();
const client = new textToSpeech.TextToSpeechClient();
const PORT = process.env.PORT || 5000;
const HOST = process.env.HOST || 'localhost';
app.use(cors({
origin : 'http://localhost:3000'
}));
app.use((req,res,next) => {
console.log(`Request Occur! ${req.method} ${req.url}`);
next();
})
app.get('/exam1', async (req, res) => {
let filePath = path.join(__dirname,'/private/output.wav');
res.sendFile(filePath);
});
app.listen(PORT, HOST, ()=> {
console.log(`App listening on ${PORT}`);
})
클라이언트에서 /exam1로 접속하면 서버에 있는 파일을 보내주는 형태다.
클라이언트는 위 참조사이트를 많이 참고했다.
클라이언트는 리액트를 이용했다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const Example1Page = () => {
const [audioSource, setAudioSource] = useState('');
useEffect(() => {
requestAudioFile();
},[])
const requestAudioFile = async () => {
console.log("request Audio");
const response = await axios.get('http://localhost:5000/exam1',{
responseType : 'arraybuffer'
})
console.log("response : ",response);
const audioContext = getAudioContext();
const audioBuffer = await audioContext.decodeAudioData(response.data);
//create audio source
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();//자동으로 오디오 시작되게
console.log("source : ", source);
setAudioSource(source);
}
const getAudioContext = () => {
AudioContext = window.AudioContext; /* || window.webkitAudioContext */
const audioContent = new AudioContext();
return audioContent;
}
return(
<div>
Example1
<audio controls>
<source type = "audio/mpeg" src={audioSource}/>
</audio>
</div>
)
}
export default Example1Page;
해당 페이지에 들어가면 react hooks로 페이지가 렌더되면 서버에 axios로 요청을 보내고 서버에서 받아온 데이터를 바탕으로 오디오소스를 만들어 오디오 소스가 만들어지면 자동으로 음성이 나오게 된다.
여기까지도 충분히 좋다. 그런데 여기서 더 발전시켜서 서버에 mp3나 wav파일 같은 오디오 파일을 저장시키지 않고 바로 클라이언트로 연결시켜줘서 클라이언트에서 오디오를 재생할 수 있게 만들고 싶었다. 노래개수가 보통이 아닌데 그 노래들을 목소리로 전환시킨 파일을 전부 db에 저장하기에는 용량이 너무 컸기 때문이다.
우선 구글API에서 제공해 주는 부분을 살펴보았다.
// Performs the text-to-speech request
const [response] = await client.synthesizeSpeech(request);
// Write the binary audio content to a local file
const writeFile = util.promisify(fs.writeFile);
await writeFile('output.mp3', response.audioContent, 'binary');
console.log('Audio content written to file: output.mp3');
이부분에서 response를 콘솔로 찍어보니 Buffer데이터를 받아오는 것을 확인하였다. 받아온 버퍼데이터를 바탕으로 output.mp3파일을 만들고 있었던 것이다.
또, 클라이언트에서
const response = await axios.get('http://localhost:5000/exam1',{
responseType : 'arraybuffer'
})
console.log("response : ",response);
이렇게 리스폰스타입을 arrayBuffer
타입으로 버퍼데이터를 받아주고 있다. 그렇다면 서버에서 api에서 받아온 버퍼데이터를 파일을 만들지 말고 바로 클라이언트로 보내주면 될 거라고 생각했다.
그래서
const express = require('express');
const app = express();
const path = require('path');
const cors = require('cors');
const textToSpeech = require('@google-cloud/text-to-speech');
require('dotenv').config();
const client = new textToSpeech.TextToSpeechClient();
const PORT = process.env.PORT || 5000;
const HOST = process.env.HOST || 'localhost';
app.use(cors({
origin : 'http://localhost:3000'
}));
app.use((req,res,next) => {
console.log(`Request Occur! ${req.method} ${req.url}`);
next();
})
app.get('/exam1', async (req, res) => {
// let filePath = path.join(__dirname,'/private/output.wav');
// res.sendFile(filePath);
let bufferData = await quickStart();
console.log();
res.header({
// 'Content-Type' : 'audio/wav',
// 'Content-length' : bufferData.length
})
res.send(bufferData);
// res.end(bufferData);
// res.send({ //이방식은 안된다. 아마도 클라이언트에서 response : ArrayBuffer로 받아서, 순수 버퍼데이터만 보내주어야 하는 듯 하다. 따라서 서버에서 어레이 버퍼로 변환해서 보내주어도 안된다.(toArrayBuffer쓰면 안되고 걍 버퍼데이터 자체를 보내주면 된다.)
// bufferData
// })
})
async function quickStart() {
// The text to synthesize
const text = '울려 퍼지는 음악에 맞춰 Everyone put your hands up and get your drinks up 온 세상이 함께 미쳐 Everyone put your';
// Construct the request
const request = {
input: {text: text},
// Select the language and SSML voice gender (optional)
voice: {languageCode: 'ko-KR', ssmlGender: 'NEUTRAL'},
// select the type of audio encoding
audioConfig: {audioEncoding: 'LINEAR16', speakingRate : 1},
};
// Performs the text-to-speech request
const [response] = await client.synthesizeSpeech(request);
console.log("$$$ : ", response);
return response.audioContent;
}
// function toArrayBuffer(buf) {
// var ab = new ArrayBuffer(buf.length);
// var view = new Uint8Array(ab);
// for (var i = 0; i < buf.length; ++i) {
// view[i] = buf[i];
// }
// return ab;
// }
app.listen(PORT, HOST, ()=> {
console.log(`App listening on ${PORT}`);
})
이렇게 코드를 만들어 주었다. 이 과정에서 사실 시행착오가 많았다. Client의
const audioBuffer = await audioContext.decodeAudioData(response.data);
이부분에서 ArrayBuffer데이터만 받는다는 오류가 떠서 위에 주석처리 해놓은데서 보이다시피
function toArrayBuffer(buf) {
var ab = new ArrayBuffer(buf.length);
var view = new Uint8Array(ab);
for (var i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return ab;
}
처럼 ArrayBuffer형식으로 바꿔서 res.send({ArrayBuffer})이런 형식으로 보내보기도 하고, 클라이언트에 위의 코드(toArrayBuffer함수)를 넣어서 클라이언트에서 버퍼데이터를 어레이버퍼로 바꿔보기도 했다. 이런 저런 방법을 시도해도 안되서 거의 놓아버리는 와중에 주님께서 영감을 주셨다. 진짜 그냥 놓아버리는 심정으로 서버에서
app.get('/exam1', async (req, res) => {
let bufferData = await quickStart();
res.header({
// 'Content-Type' : 'audio/wav',
// 'Content-length' : bufferData.length
})
res.send(bufferData);
// res.end(bufferData);
// res.send({ //이방식은 안된다. 아마도 클라이언트에서 response : ArrayBuffer로 받아서, 순수 버퍼데이터만 보내주어야 하는 듯 하다. 따라서 서버에서 어레이 버퍼로 변환해서 보내주어도 안된다.(toArrayBuffer쓰면 안되고 걍 버퍼데이터 자체를 보내주면 된다.)
// bufferData
// })
})
이렇게 res.send를 보낼 때 아무것도 없이 api에서 받아온 데이터 그대로 넣어주었다. 그리고 client도
const requestAudioFile = async () => {
console.log("request Audio");
const response = await axios.get('http://localhost:5000/exam1',{
responseType : 'arraybuffer'
})
console.log("response : ",response);
// let arr = toArrayBuffer(response.data);
// makeAudio(arr);
const audioContext = getAudioContext();
// makeAudio(response)
const audioBuffer = await audioContext.decodeAudioData(response.data);
//create audio source
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
console.log("source : ", source);
setAudioSource(source);
}
const getAudioContext = () => {
AudioContext = window.AudioContext; /* || window.webkitAudioContext */
const audioContent = new AudioContext();
return audioContent;
}
이렇게 받아온 response 데이터를 넣어주는 방식으로 바꿔주었다.
나중에 살펴보니
const response = await axios.get('http://localhost:5000/exam1',{
responseType : 'arraybuffer'
})
이부분에서 이미 arraybuffer형식으로 리스폰스를 받아오고 있었고, 그래서 서버에서 데이터를 res.send()나 res.end()로 넘겨줄때 res.send({message : 'ok',data : buffer})이렇게 넘겨줘도 안되고(불순물이 껴서 위의 axios - responseType : 'arraybuffer' 로 받아올 때 arratbuffer형식이 다르게 된다.), arraybuffer형식으로 받아주기 때문에 buffer를 서버에서 arraybuffer로 바꾸거나, 클라이언트에서 데이터를 받고 그 데이터를 arraybuffer로 또 바꿔주면 이중으로 arraybuffer로 바꿔준 거가 되기 때문에 audioContext.decodeAudioContext에서 오류가 났던 것이다. 그래서 서버에서는 res.send(bufferData)로 순수 오디오 버퍼 데이터만 넘겨주면 되고, 클라이언트에서는
const response = await axios.get('http://localhost:5000/exam1',{
responseType : 'arraybuffer'
})
이렇게 받아주면 arrayBuffer타입으로 받아주게 되고 그 데이터를 audioContext.decodeAudioContext에 넘겨주기만 하면 됬던 것이다.
이건 정말로 내가 했다고는 생각할 수 없다.
일단 최종코드(여기까지)
Server
const express = require('express');
const app = express();
const path = require('path');
const cors = require('cors');
const textToSpeech = require('@google-cloud/text-to-speech');
require('dotenv').config();
const client = new textToSpeech.TextToSpeechClient();
const PORT = process.env.PORT || 5000;
const HOST = process.env.HOST || 'localhost';
app.use(cors({
origin : 'http://localhost:3000'
}));
app.use((req,res,next) => {
console.log(`Request Occur! ${req.method} ${req.url}`);
next();
})
app.get('/exam1', async (req, res) => {
// let filePath = path.join(__dirname,'/private/output.wav');
// res.sendFile(filePath);
let bufferData = await quickStart();
console.log();
res.header({
// 'Content-Type' : 'audio/wav',
// 'Content-length' : bufferData.length
})
res.send(bufferData);
// res.end(bufferData);
// res.send({ //이방식은 안된다. 아마도 클라이언트에서 response : ArrayBuffer로 받아서, 순수 버퍼데이터만 보내주어야 하는 듯 하다. 따라서 서버에서 어레이 버퍼로 변환해서 보내주어도 안된다.(toArrayBuffer쓰면 안되고 걍 버퍼데이터 자체를 보내주면 된다.)
// bufferData
// })
})
async function quickStart() {
// The text to synthesize
const text = '울려 퍼지는 음악에 맞춰 Everyone put your hands up and get your drinks up 온 세상이 함께 미쳐 Everyone put your';
// Construct the request
const request = {
input: {text: text},
// Select the language and SSML voice gender (optional)
voice: {languageCode: 'ko-KR', ssmlGender: 'NEUTRAL'},
// select the type of audio encoding
audioConfig: {audioEncoding: 'LINEAR16', speakingRate : 1},
};
// Performs the text-to-speech request
const [response] = await client.synthesizeSpeech(request);
console.log("$$$ : ", response);
return response.audioContent;
}
// function toArrayBuffer(buf) {
// var ab = new ArrayBuffer(buf.length);
// var view = new Uint8Array(ab);
// for (var i = 0; i < buf.length; ++i) {
// view[i] = buf[i];
// }
// return ab;
// }
app.listen(PORT, HOST, ()=> {
console.log(`App listening on ${PORT}`);
})
Client
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const Example1Page = () => {
const [audioSource, setAudioSource] = useState('');
useEffect(() => {
requestAudioFile();
},[])
const requestAudioFile = async () => {
console.log("request Audio");
const response = await axios.get('http://localhost:5000/exam1',{
responseType : 'arraybuffer'
})
console.log("response : ",response);
// let arr = toArrayBuffer(response.data);
// makeAudio(arr);
const audioContext = getAudioContext();
// makeAudio(response)
const audioBuffer = await audioContext.decodeAudioData(response.data);
//create audio source
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
console.log("source : ", source);
setAudioSource(source);
}
const getAudioContext = () => {
AudioContext = window.AudioContext; /* || window.webkitAudioContext */
const audioContent = new AudioContext();
return audioContent;
}
return(
<div>
Example1
<audio controls>
<source type = "audio/mpeg" /*src={audioSource}*//>
</audio>
</div>
)
}
export default Example1Page;