본문 바로가기
안드로이드 자바

[Java][Android] 내 음성을 raw pcm data format 음원에 저장하기.

by teamnova 2023. 5. 12.

 

raw pcm data format 음원이란?

header가 없이 raw data만 저장한 파일입니다.

따라서 pcm 포맷으로 저장된 오디오는 별도로 오디오에 대한 정보( sampling rate, bit size, endian, channels )를 가지고 있지 않으면 제대로 play할 수 없습니다.

 

 

저는 sampling rate, bit size, endian, channels 을 

16kHz Sampling rate, 16 bit short-int, little-endian, mono 로 설정했지만

변수값을 바꾸면 다른 값으로도 변경 가능합니다!

 

 

실행 결과입니다.

 

아래는 생성한 음원입니다.

test.wav
0.06MB

 

전체 코드 및 코드 설명입니다.

메니페스트에 아래의 코드를 추가해주세요.

// 음성 녹음을 위해 필요한 코드
	<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 파일을 저장하기 위해 필요한 코드
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

 

 

MainActivity.java코드 입니다.

AudioRecord class와  RecordingRunnable class를 사용해서 음성을 음원에 저장합니다.

AudioRecord 객체는 음성을 0,1로 이루어진 디지털 정보로 바꿉니다.

RecordingRunnable 객체는 AudioRecord 객체가 음성을 디지털 정보로 바꿔놓은걸 가져와서 파일에 저장하는 일을 합니다.

package com.example.recordexample;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Button;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;

public class MainActivity extends AppCompatActivity {
// raw pcm data (16kHz Sampling rate, 16 bit short-int, little-endian)
// 헤더 : RAW(header-less), 인코딩 : Signed 16-bit PCM

    // 16kHz Sampling rate
    private static final int RECORDER_SAMPLE_RATE = 16000;

    // 오디오 채널 MONO
    private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;

    private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;

	// 마이크에서 음성 받아온다.
    int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;

	// 사용할 버퍼 사이즈
    int BUFFER_SIZE_RECORDING = AudioRecord.getMinBufferSize(RECORDER_SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);

    /**
     * Signals whether a recording is in progress (true) or not (false).
     * Boolean 타입 과 거의 비슷한 듯. 여러 쓰레드에 안전하다는 장점이 있는?
     */
    private final AtomicBoolean recordingInProgress = new AtomicBoolean(false);

    // 음성을 녹음하는 객체. 음성을 디지털 데이터로 변환하는.
    private AudioRecord audioRecord = null;

    // 일반 스레드, 데이터를 계속 받아와서 파일에 저장하는
    private Thread recordingThread = null;

    private Button startButton;
    private Button stopButton;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

		// 권한 요청.
         String[] permissions = {android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.WRITE_EXTERNAL_STORAGE};
        ActivityCompat.requestPermissions(this, permissions, 0);


        startButton = (Button) findViewById(R.id.btnStart);
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            
            // 녹음 시작
                startRecording();

                startButton.setEnabled(false);
                stopButton.setEnabled(true);

            }
        });

        stopButton = (Button) findViewById(R.id.btnStop);
        stopButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
                // 녹음 중지
                stopRecording();

                startButton.setEnabled(true);
                stopButton.setEnabled(false);

            }
        });

        stopButton.setEnabled(false);
    }

    @SuppressLint("MissingPermission")
    private void startRecording() {

        audioRecord = new AudioRecord(AUDIO_SOURCE, RECORDER_SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, BUFFER_SIZE_RECORDING);

        audioRecord.startRecording();

        recordingInProgress.set(true);

        recordingThread = new Thread(new MainActivity.RecordingRunnable(), "Recording Thread");
        recordingThread.start();

    }

    private void stopRecording() {
        if (null == audioRecord) {
            return;
        }

        recordingInProgress.set(false);

        audioRecord.stop();
        audioRecord.release();
        audioRecord = null;
        recordingThread = null;

    }

    private class RecordingRunnable implements Runnable {

        @Override
        public void run() {

           
           // download/STTFile/test.wav 파일에 음성 저장하기.
            final String foldername = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()+"/STTFile" ;
            String name = "test.wav";

            File dir = new File(foldername);
            dir.mkdirs();
            final File file = new File(dir, name);


			// 음성 데이터 잠시 담아둘 버퍼 생성.
            final ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE_RECORDING);

            FileOutputStream outStream = null;
            try {
                outStream = new FileOutputStream(file);
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            }

			// 녹음하는 동안 {} 안의 코드 실행.
            while (recordingInProgress.get()) {

				// audioRecord 객체에서 음성 데이터 가져옴.
                int result = audioRecord.read(buffer, BUFFER_SIZE_RECORDING);
                if (result < 0) {
                    throw new RuntimeException("Reading of audio buffer failed: " +
                            getBufferReadFailureReason(result));
                }

                try {
                
                // 가져온 데이터를 파일에 저장.
                    outStream.write(buffer.array(), 0, BUFFER_SIZE_RECORDING);


                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

            }

            buffer.clear();
            try {
                outStream.flush();
                outStream.close();

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            System.out.println("updateWavHeader start");

        }

        private String getBufferReadFailureReason(int errorCode) {
            switch (errorCode) {
                case AudioRecord.ERROR_INVALID_OPERATION:
                    return "ERROR_INVALID_OPERATION";
                case AudioRecord.ERROR_BAD_VALUE:
                    return "ERROR_BAD_VALUE";
                case AudioRecord.ERROR_DEAD_OBJECT:
                    return "ERROR_DEAD_OBJECT";
                case AudioRecord.ERROR:
                    return "ERROR";
                default:
                    return "Unknown (" + errorCode + ")";
            }
        }
    }
}

 

activity_main.xml 코드입니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnStart"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnStop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop"
        app:layout_constraintTop_toBottomOf="@+id/btnStart"
        tools:layout_editor_absoluteX="0dp" />

</androidx.constraintlayout.widget.ConstraintLayout>