TarsosDSP 라이브러리로 하는 pitch detection

TarsosDSP 라이브러리로 하는 pitch detection

TarsosDSP란?

TarsosDSP는 audio processing을 위한 java library다. 보통 audio processing을 위한 프레임워크나 라이브러리는 아래와 같은 두 카테고리로 나뉘는데 이를 real-time으로 지원하는 경우는 매우 드물다.

1) audio analysis & feature extraction

2) audio synthesis capabilities

TarsosDSP는 real-time pitch / onset extraction을 비롯하여 time stretching, pitch shifting, filtering, resampling, effects, synthesis 등을 지원한다. 또한 Android platform에서 개발 가능 하다는 점이 특징이다.

TarsosDSP 사용 방법

https://0110.be/releases/TarsosDSP/TarsosDSP-latest/ 에서 TarsosDSP-latest.jar 파일을 다운로드한 뒤, 안드로이드 스튜디오를 실행하여 아래 사진과 같이 Android -> Project를 선택한다.

앞서 다운 받은 TarsosDSP-latest.jar 파일을 app > libs 폴더에 복사, 붙여넣기 한다.

아래 사진에서 마우스오버된 아이콘을 누르거나 맥 기준 command와 ; 를 입력하면 project structure 창이 띄워진다. Dependencies > app에서 + 버튼을 눌러 Jar Dependency를 클릭하면 Dependency를 추가할 수 있는 창이 뜬다. 이때 libs > TarsosDSP-latest.jar 경로를 Step 1.에 입력해준 뒤 Apply 후 OK를 누른다.

위 과정이 정상적으로 이루어졌을 경우 build.gradle 파일에 아래와 같이 추가된 것을 확인할 수 있다.

permission 추가 방법

AndroidManifest.xml 파일을 열어 태그 내에 아래 3개의 코드를 넣어 오디오 및 스토리지 권한을 추가하면 TarsosDSP를 통해 pitch detection을 할 모든 준비가 끝난다. 이때 Android 6.0 이상인 device를 사용하는 경우, 단순히 AndroidManifest.xml 파일에 권한을 추가하는 코드를 작성하는 것만으로는 제대로 작동이 되지 않는다.

그 이유는 Android 6.0 이상의 device에서는 run-time에 권한을 요청하는 코드가 따로 필요하기 때문이다. 그래서 나는 checkPermission()이라는 함수를 만들어서 onCreate 메소드에서 호출하는 코드를 추가 작성하여 이를 해결했다.

public void checkPermission() { String[] permissions = { Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; int permissionCheck = PackageManager.PERMISSION_GRANTED; for (int i = 0; i < permissions.length; i++) { permissionCheck = ContextCompat.checkSelfPermission(this, permissions[i]); if (permissionCheck == PackageManager.PERMISSION_DENIED) { break; } } if (permissionCheck == PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "권한 있음", Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, "권한 없음", Toast.LENGTH_LONG).show(); if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) { Toast.makeText(this, "권한 설명 필요함.", Toast.LENGTH_LONG).show(); } else { ActivityCompat.requestPermissions(this, permissions, 1); } } }

아래는 간단하게 pitch detection을 시도해볼 수 있는 예제 코드이다. 해당 코드에 권한을 요청하는 함수를 추가하여 사용하면 정상적으로 record와 detection이 수행된다.

package com.example.tarsosdsp; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Environment; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteOrder; import be.tarsos.dsp.AudioDispatcher; import be.tarsos.dsp.AudioEvent; import be.tarsos.dsp.AudioProcessor; import be.tarsos.dsp.io.TarsosDSPAudioFormat; import be.tarsos.dsp.io.UniversalAudioInputStream; import be.tarsos.dsp.io.android.AndroidAudioPlayer; import be.tarsos.dsp.io.android.AudioDispatcherFactory; import be.tarsos.dsp.pitch.PitchDetectionHandler; import be.tarsos.dsp.pitch.PitchDetectionResult; import be.tarsos.dsp.pitch.PitchProcessor; import be.tarsos.dsp.writer.WriterProcessor; public class RecordPlayActivity extends AppCompatActivity { AudioDispatcher dispatcher; TarsosDSPAudioFormat tarsosDSPAudioFormat; File file; TextView pitchTextView; Button recordButton; Button playButton; boolean isRecording = false; String filename = "recorded_sound.wav"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); File sdCard = Environment.getExternalStorageDirectory(); file = new File(sdCard, filename); /* filePath = file.getAbsolutePath(); Log.e("MainActivity", "저장 파일 경로 :" + filePath); // 저장 파일 경로 : /storage/emulated/0/recorded.mp4 */ tarsosDSPAudioFormat=new TarsosDSPAudioFormat(TarsosDSPAudioFormat.Encoding.PCM_SIGNED, 22050, 2 * 8, 1, 2 * 1, 22050, ByteOrder.BIG_ENDIAN.equals(ByteOrder.nativeOrder())); pitchTextView = findViewById(R.id.pitchTextView); recordButton = findViewById(R.id.recordButton); playButton = findViewById(R.id.playButton); recordButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(!isRecording) { recordAudio(); isRecording = true; recordButton.setText("중지"); } else { stopRecording(); isRecording = false; recordButton.setText("녹음"); } } }); playButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { playAudio(); } }); } public void playAudio() { try{ releaseDispatcher(); FileInputStream fileInputStream = new FileInputStream(file); dispatcher = new AudioDispatcher(new UniversalAudioInputStream(fileInputStream, tarsosDSPAudioFormat), 1024, 0); AudioProcessor playerProcessor = new AndroidAudioPlayer(tarsosDSPAudioFormat, 2048, 0); dispatcher.addAudioProcessor(playerProcessor); PitchDetectionHandler pitchDetectionHandler = new PitchDetectionHandler() { @Override public void handlePitch(PitchDetectionResult res, AudioEvent e){ final float pitchInHz = res.getPitch(); runOnUiThread(new Runnable() { @Override public void run() { pitchTextView.setText(pitchInHz + ""); } }); } }; AudioProcessor pitchProcessor = new PitchProcessor(PitchProcessor.PitchEstimationAlgorithm.FFT_YIN, 22050, 1024, pitchDetectionHandler); dispatcher.addAudioProcessor(pitchProcessor); Thread audioThread = new Thread(dispatcher, "Audio Thread"); audioThread.start(); }catch(Exception e) { e.printStackTrace(); } } public void recordAudio() { releaseDispatcher(); dispatcher = AudioDispatcherFactory.fromDefaultMicrophone(22050,1024,0); try { RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw"); AudioProcessor recordProcessor = new WriterProcessor(tarsosDSPAudioFormat, randomAccessFile); dispatcher.addAudioProcessor(recordProcessor); PitchDetectionHandler pitchDetectionHandler = new PitchDetectionHandler() { @Override public void handlePitch(PitchDetectionResult res, AudioEvent e){ final float pitchInHz = res.getPitch(); runOnUiThread(new Runnable() { @Override public void run() { pitchTextView.setText(pitchInHz + ""); } }); } }; AudioProcessor pitchProcessor = new PitchProcessor(PitchProcessor.PitchEstimationAlgorithm.FFT_YIN, 22050, 1024, pitchDetectionHandler); dispatcher.addAudioProcessor(pitchProcessor); Thread audioThread = new Thread(dispatcher, "Audio Thread"); audioThread.start(); } catch (IOException e) { e.printStackTrace(); } } public void stopRecording() { releaseDispatcher(); } public void releaseDispatcher() { if(dispatcher != null) { if(!dispatcher.isStopped()) dispatcher.stop(); dispatcher = null; } } @Override protected void onStop() { super.onStop(); releaseDispatcher(); } }

from http://dev-igation.tistory.com/48 by ccl(A) rewrite - 2021-11-25 23:27:22