GoogleTTS API이용해서 만든 음성 클라이언트로 넘겨주고 재생하기

임재현·2021년 2월 19일
4

laggardProject

목록 보기
1/3

팀 프로젝트로 음악퀴즈 사이트를 만들게 되었다.
놀토에서 나오는 간식퀴즈같이 기계음등으로 음정이나 박자 없이 가사를 그대로 말해주면 노래 제목을 맞추는 퀴즈사이트이다.

나는 우선 이 사이트의 구현이 가능한지 알아보기 위해 사전조사 단계로
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;

나의 주, 나의 아버지께 감사드립니다.

profile
임재현입니다.

0개의 댓글