teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

5

パーミッション関係および Pause/Resume 時の動作等のコード修正

2021/10/05 14:21

投稿

jimbe
jimbe

スコア13355

answer CHANGED
@@ -11,27 +11,30 @@
11
11
  ```java
12
12
  package com.teratail.q362100;
13
13
 
14
- import androidx.annotation.NonNull;
14
+ import androidx.activity.result.ActivityResultLauncher;
15
+ import androidx.activity.result.contract.ActivityResultContracts;
15
- import androidx.appcompat.app.AppCompatActivity;
16
+ import androidx.appcompat.app.*;
16
17
  import androidx.core.content.ContextCompat;
18
+ import androidx.fragment.app.DialogFragment;
17
19
 
18
20
  import android.Manifest;
21
+ import android.app.Dialog;
19
22
  import android.content.pm.PackageManager;
20
23
  import android.media.*;
21
24
  import android.os.*;
25
+ import android.view.Gravity;
22
- import android.widget.Button;
26
+ import android.widget.*;
23
27
 
24
28
  public class MainActivity extends AppCompatActivity {
25
29
  @SuppressWarnings("UnusedDeclaration")
26
30
  private static final String TAG = "MainActivity";
27
31
 
28
- private static final int PERMISSIONS_REQUEST_RECORD_AUDIO = 99;
29
-
30
32
  private static final int AUDIO_SAMPLE_FREQ = 44100;//サンプリング周波数
31
33
  private static final int FRAME_RATE = 10;
32
34
  private static final String START_TEXT = "START";
33
35
  private static final String STOP_TEXT = "STOP";
34
36
 
37
+ private SurfaceDrawer surfaceDrawer;
35
38
  private Button button;
36
39
  private HandlerThread listenerThread;
37
40
  private AudioRecord recorder;
@@ -41,8 +44,71 @@
41
44
  setContentView(R.layout.activity_main);
42
45
  setTitle("AudioRecord");
43
46
 
44
- SurfaceDrawer surfaceDrawer = new SurfaceDrawer(findViewById(R.id.surfaceView));
47
+ surfaceDrawer = new SurfaceDrawer(findViewById(R.id.surfaceView));
45
48
 
49
+ button = findViewById(R.id.button);
50
+ button.setEnabled(true);
51
+ button.setText(START_TEXT);
52
+ button.setOnClickListener(v -> {
53
+ switch(button.getText().toString()) { //トグル
54
+ case START_TEXT:
55
+ permissionCheckAndStart();
56
+ break;
57
+ case STOP_TEXT:
58
+ recorder.stop();
59
+ button.setText(START_TEXT);
60
+ break;
61
+ }
62
+ });
63
+ }
64
+
65
+ //権限要求ダイアログ表示・返答受付
66
+ private final ActivityResultLauncher<String> requestPermissionLauncher =
67
+ registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
68
+ if (isGranted) {
69
+ start();
70
+ } else {
71
+ Toast t = Toast.makeText(this, "権限が許可されなかった為、実行できません。", Toast.LENGTH_LONG);
72
+ t.setGravity(Gravity.CENTER, 0, 0);
73
+ t.show();
74
+ }
75
+ });
76
+
77
+ //権限説明ダイアログ ok 押下 → 権限要求ダイアログ
78
+ public static class ExplainDialogFragment extends DialogFragment {
79
+ @Override
80
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
81
+ MainActivity activity = (MainActivity)requireActivity();
82
+ return new AlertDialog.Builder(activity)
83
+ .setTitle("使用許可の説明")
84
+ .setMessage("音声波形を表示するにはマイクの使用(「音声の録音」)の許可が必要です")
85
+ .setNeutralButton(android.R.string.cancel, (dialog,which)->{})
86
+ .setPositiveButton(android.R.string.ok, (dialog,which)->{
87
+ activity.requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO);
88
+ })
89
+ .create();
90
+ }
91
+ }
92
+
93
+ //権限チェック
94
+ private void permissionCheckAndStart() {
95
+ if(ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
96
+ start();
97
+ } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
98
+ new ExplainDialogFragment().show(getSupportFragmentManager(), "Explain");
99
+ } else {
100
+ requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO);
101
+ }
102
+ }
103
+
104
+ //波形表示開始
105
+ private void start() {
106
+ if(recorder == null) initialize();
107
+ recorder.startRecording();
108
+ button.setText(STOP_TEXT);
109
+ }
110
+
111
+ private void initialize() {
46
112
  int frameBufferSize = AUDIO_SAMPLE_FREQ / FRAME_RATE;
47
113
  short[] audioData = new short[frameBufferSize];
48
114
 
@@ -54,56 +120,24 @@
54
120
  recorder.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
55
121
  @Override
56
122
  public void onMarkerReached(AudioRecord recorder) {}
123
+
57
124
  @Override
58
125
  public void onPeriodicNotification(AudioRecord recorder) {
59
126
  recorder.read(audioData, 0, audioData.length);
60
127
  surfaceDrawer.draw(audioData);
61
128
  }
62
129
  }, new Handler(listenerThread.getLooper())); //HandlerThread でリスナを実行
63
-
64
- button = findViewById(R.id.button);
65
- button.setText(START_TEXT);
66
- button.setEnabled(false);
67
- button.setOnClickListener(v -> {
68
- switch(button.getText().toString()) { //トグル
69
- case START_TEXT:
70
- recorder.startRecording();
71
- button.setText(STOP_TEXT);
72
- break;
73
- case STOP_TEXT:
74
- recorder.stop();
75
- button.setText(START_TEXT);
76
- break;
77
- }
78
- });
79
-
80
- int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
81
- if(permissionCheck == PackageManager.PERMISSION_GRANTED) { // すでにユーザーがパーミッションを許可
82
- button.setEnabled(true);
83
- } else { // ユーザーはパーミッションを許可していない
84
- requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSIONS_REQUEST_RECORD_AUDIO);
85
- }
86
130
  }
87
131
 
88
- @Override
89
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
90
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
132
+ private void terminate() {
91
-
92
- if(requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO) {
133
+ if(recorder != null) {
93
- if(grantResults[0] != PackageManager.PERMISSION_GRANTED) { // ユーザが許可しなかったらアプリを終了する
94
- finish();
134
+ recorder.stop();
95
- }
96
- button.setEnabled(true);
135
+ recorder.release();
97
136
  }
98
- }
137
+ recorder = null;
99
138
 
100
- @Override
101
- protected void onDestroy() {
102
- super.onDestroy();
103
139
  if(listenerThread != null) listenerThread.quit();
104
140
  listenerThread = null;
105
- if(recorder != null) recorder.release();
106
- recorder = null;
107
141
  }
108
142
 
109
143
  private AudioRecord settingRecorder(int sampleRateInHz, int channelConfig, int audioFormat, int frameBufferSize) {
@@ -120,6 +154,20 @@
120
154
 
121
155
  return recorder;
122
156
  }
157
+
158
+ @Override
159
+ protected void onPause() { //停止
160
+ super.onPause();
161
+ terminate();
162
+ }
163
+
164
+ @Override
165
+ protected void onResume() { //再開
166
+ super.onResume();
167
+ if(button.getText().toString().equals(STOP_TEXT)) { //実行中だった
168
+ permissionCheckAndStart();
169
+ }
170
+ }
123
171
  }
124
172
  ```
125
173
  レイアウト: activity_main.xml
@@ -162,6 +210,7 @@
162
210
  import android.graphics.Color;
163
211
  import android.graphics.Paint;
164
212
  import android.graphics.Path;
213
+ import android.util.Log;
165
214
  import android.view.SurfaceHolder;
166
215
  import android.view.SurfaceView;
167
216
 
@@ -189,6 +238,7 @@
189
238
 
190
239
  @Override
191
240
  public void surfaceCreated(SurfaceHolder holder) {
241
+ //Log.d(TAG, "surfaceCreated");
192
242
  Canvas canvas = holder.lockCanvas();
193
243
  if(canvas == null) return;
194
244
 
@@ -198,20 +248,24 @@
198
248
 
199
249
  @Override
200
250
  public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
251
+ //Log.d(TAG, "surfaceChanged w="+w+", h="+h);
201
252
  canvasWidth = w;
202
253
  canvasVerticalCenter = h / 2f;
203
254
  }
204
255
 
205
256
  @Override
206
257
  public void surfaceDestroyed(SurfaceHolder holder) {
207
- this.holder = null;
258
+ //Log.d(TAG, "surfaceDestroyed");
208
259
  }
209
260
 
210
261
  public void draw(short[] data) {
211
262
  if(data == null || data.length < 2) return;
212
263
 
213
264
  Canvas canvas = holder.lockCanvas();
214
- if(canvas == null) return;
265
+ if(canvas == null) {
266
+ Log.d(TAG, "holder.lockCanvas() is null.");
267
+ return;
268
+ }
215
269
 
216
270
  canvas.drawColor(Color.BLACK); //塗り潰し
217
271
 
@@ -227,71 +281,11 @@
227
281
  }
228
282
  ```
229
283
  app/build.gradle
284
+ dependencies に追加
230
285
  ```plain
231
- plugins {
232
- id 'com.android.application'
233
- }
234
-
235
- android {
236
- compileSdk 30
237
-
238
- defaultConfig {
239
- applicationId "com.teratail.q362100"
240
- minSdk 29
241
- targetSdk 30
242
- versionCode 1
243
- versionName "1.0"
244
-
245
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
246
- }
247
-
248
- buildTypes {
249
- release {
250
- minifyEnabled false
251
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
252
- }
253
- }
254
- compileOptions {
255
- sourceCompatibility JavaVersion.VERSION_1_8
256
- targetCompatibility JavaVersion.VERSION_1_8
257
- }
258
- }
259
-
260
- dependencies {
261
-
262
- implementation 'androidx.appcompat:appcompat:1.3.1'
286
+ implementation 'androidx.activity:activity:1.3.1'
263
- implementation 'com.google.android.material:material:1.4.0'
264
- implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
265
- testImplementation 'junit:junit:4.+'
266
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
267
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
268
- }
269
287
  ```
270
- AndroidManifest.xml
288
+ AndroidManifest.xml 追加
271
289
  ```xml
272
- <?xml version="1.0" encoding="utf-8"?>
273
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
274
- package="com.teratail.q362100">
275
-
276
290
  <uses-permission android:name="android.permission.RECORD_AUDIO" />
277
-
278
- <application
279
- android:allowBackup="true"
280
- android:icon="@mipmap/ic_launcher"
281
- android:label="@string/app_name"
282
- android:roundIcon="@mipmap/ic_launcher_round"
283
- android:supportsRtl="true"
284
- android:theme="@style/Theme.Q362100">
285
- <activity
286
- android:name=".MainActivity"
287
- android:exported="true">
288
- <intent-filter>
289
- <action android:name="android.intent.action.MAIN" />
290
-
291
- <category android:name="android.intent.category.LAUNCHER" />
292
- </intent-filter>
293
- </activity>
294
- </application>
295
-
296
- </manifest>
297
291
  ```

4

settingRecorder() 修正、AndroidManifest.xml 追加

2021/10/05 14:21

投稿

jimbe
jimbe

スコア13355

answer CHANGED
@@ -109,14 +109,14 @@
109
109
  private AudioRecord settingRecorder(int sampleRateInHz, int channelConfig, int audioFormat, int frameBufferSize) {
110
110
  int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
111
111
  if(minBufferSize == AudioRecord.ERROR_BAD_VALUE) throw new IllegalArgumentException();
112
- if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException();
112
+ if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException("minBufferSize");
113
113
 
114
114
  int bufferSizeInBytes = Math.max(minBufferSize, frameBufferSize*10); //"*10"は余裕分
115
115
 
116
116
  AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
117
- if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException();
117
+ if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException("recorder.getState");
118
118
 
119
- if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException();
119
+ if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException("setPositionNotificationPeriod frameBufferSize="+frameBufferSize);
120
120
 
121
121
  return recorder;
122
122
  }
@@ -266,4 +266,32 @@
266
266
  androidTestImplementation 'androidx.test.ext:junit:1.1.3'
267
267
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
268
268
  }
269
+ ```
270
+ AndroidManifest.xml
271
+ ```xml
272
+ <?xml version="1.0" encoding="utf-8"?>
273
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
274
+ package="com.teratail.q362100">
275
+
276
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
277
+
278
+ <application
279
+ android:allowBackup="true"
280
+ android:icon="@mipmap/ic_launcher"
281
+ android:label="@string/app_name"
282
+ android:roundIcon="@mipmap/ic_launcher_round"
283
+ android:supportsRtl="true"
284
+ android:theme="@style/Theme.Q362100">
285
+ <activity
286
+ android:name=".MainActivity"
287
+ android:exported="true">
288
+ <intent-filter>
289
+ <action android:name="android.intent.action.MAIN" />
290
+
291
+ <category android:name="android.intent.category.LAUNCHER" />
292
+ </intent-filter>
293
+ </activity>
294
+ </application>
295
+
296
+ </manifest>
269
297
  ```

3

コード微量修正

2021/10/05 02:59

投稿

jimbe
jimbe

スコア13355

answer CHANGED
@@ -11,6 +11,7 @@
11
11
  ```java
12
12
  package com.teratail.q362100;
13
13
 
14
+ import androidx.annotation.NonNull;
14
15
  import androidx.appcompat.app.AppCompatActivity;
15
16
  import androidx.core.content.ContextCompat;
16
17
 
@@ -21,6 +22,7 @@
21
22
  import android.widget.Button;
22
23
 
23
24
  public class MainActivity extends AppCompatActivity {
25
+ @SuppressWarnings("UnusedDeclaration")
24
26
  private static final String TAG = "MainActivity";
25
27
 
26
28
  private static final int PERMISSIONS_REQUEST_RECORD_AUDIO = 99;
@@ -30,9 +32,9 @@
30
32
  private static final String START_TEXT = "START";
31
33
  private static final String STOP_TEXT = "STOP";
32
34
 
33
- private short audioData[];
34
35
  private Button button;
35
36
  private HandlerThread listenerThread;
37
+ private AudioRecord recorder;
36
38
 
37
39
  public void onCreate(Bundle bundle) {
38
40
  super.onCreate(bundle);
@@ -42,9 +44,9 @@
42
44
  SurfaceDrawer surfaceDrawer = new SurfaceDrawer(findViewById(R.id.surfaceView));
43
45
 
44
46
  int frameBufferSize = AUDIO_SAMPLE_FREQ / FRAME_RATE;
45
- audioData = new short[frameBufferSize];
47
+ short[] audioData = new short[frameBufferSize];
46
48
 
47
- AudioRecord recorder = settingRecorder(AUDIO_SAMPLE_FREQ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, frameBufferSize);
49
+ recorder = settingRecorder(AUDIO_SAMPLE_FREQ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, frameBufferSize);
48
50
 
49
51
  listenerThread = new HandlerThread("RecordPositionUpdateListenerThread");
50
52
  listenerThread.start();
@@ -84,7 +86,7 @@
84
86
  }
85
87
 
86
88
  @Override
87
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
89
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
88
90
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
89
91
 
90
92
  if(requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO) {
@@ -99,6 +101,9 @@
99
101
  protected void onDestroy() {
100
102
  super.onDestroy();
101
103
  if(listenerThread != null) listenerThread.quit();
104
+ listenerThread = null;
105
+ if(recorder != null) recorder.release();
106
+ recorder = null;
102
107
  }
103
108
 
104
109
  private AudioRecord settingRecorder(int sampleRateInHz, int channelConfig, int audioFormat, int frameBufferSize) {
@@ -161,14 +166,15 @@
161
166
  import android.view.SurfaceView;
162
167
 
163
168
  public class SurfaceDrawer implements SurfaceHolder.Callback {
169
+ @SuppressWarnings("UnusedDeclaration")
164
170
  private static final String TAG = "SurfaceDrawer";
165
171
 
166
- private static final int COMPRESSION_RATE = 10;
172
+ private static final float COMPRESSION_RATE = 10f;
167
173
 
168
174
  private SurfaceHolder holder;
169
175
  private float canvasWidth, canvasVerticalCenter;
170
- private Paint pathPaint;
176
+ private final Paint pathPaint;
171
- private Path path = new Path();
177
+ private final Path path = new Path();
172
178
 
173
179
  public SurfaceDrawer(SurfaceView surfaceView) {
174
180
  holder = surfaceView.getHolder();
@@ -193,7 +199,7 @@
193
199
  @Override
194
200
  public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
195
201
  canvasWidth = w;
196
- canvasVerticalCenter = h / 2;
202
+ canvasVerticalCenter = h / 2f;
197
203
  }
198
204
 
199
205
  @Override
@@ -209,7 +215,7 @@
209
215
 
210
216
  canvas.drawColor(Color.BLACK); //塗り潰し
211
217
 
212
- path.reset();
218
+ path.rewind();
213
219
  path.moveTo(0, canvasVerticalCenter + data[0] / COMPRESSION_RATE);
214
220
  for(int x=1; x<canvasWidth; x++) {
215
221
  path.lineTo(x, canvasVerticalCenter + data[(int)(data.length / canvasWidth * x)] / COMPRESSION_RATE);

2

app/build.gradle 追加

2021/10/01 11:39

投稿

jimbe
jimbe

スコア13355

answer CHANGED
@@ -219,4 +219,45 @@
219
219
  holder.unlockCanvasAndPost(canvas);
220
220
  }
221
221
  }
222
+ ```
223
+ app/build.gradle
224
+ ```plain
225
+ plugins {
226
+ id 'com.android.application'
227
+ }
228
+
229
+ android {
230
+ compileSdk 30
231
+
232
+ defaultConfig {
233
+ applicationId "com.teratail.q362100"
234
+ minSdk 29
235
+ targetSdk 30
236
+ versionCode 1
237
+ versionName "1.0"
238
+
239
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
240
+ }
241
+
242
+ buildTypes {
243
+ release {
244
+ minifyEnabled false
245
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
246
+ }
247
+ }
248
+ compileOptions {
249
+ sourceCompatibility JavaVersion.VERSION_1_8
250
+ targetCompatibility JavaVersion.VERSION_1_8
251
+ }
252
+ }
253
+
254
+ dependencies {
255
+
256
+ implementation 'androidx.appcompat:appcompat:1.3.1'
257
+ implementation 'com.google.android.material:material:1.4.0'
258
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
259
+ testImplementation 'junit:junit:4.+'
260
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
261
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
262
+ }
222
263
  ```

1

コード追加

2021/10/01 11:13

投稿

jimbe
jimbe

スコア13355

answer CHANGED
@@ -1,4 +1,222 @@
1
1
  > クラス間での配列データの受け渡し?移動?の仕方
2
2
 
3
3
  のみに関しましては、 MySurfaceView に frn 配列へのセッターメソッドを作り、 MainPage で録音後に MySurfaceView インスタンスに対してそのセッターを呼ぶだけに思います。
4
- ですが、双方とも別スレッドで動作していますので、使用・更新のタイミングを意識しておかないと、表示が乱れる等の現象が発生するかもしれません。
4
+ ですが、双方とも別スレッドで動作していますので、使用・更新のタイミングを意識しておかないと、表示が乱れる等の現象が発生するかもしれません。
5
+
6
+ ----
7
+
8
+ オシロスコープのように動くモノが出来ましたので公開させていただきます。
9
+
10
+ MainActivity.java
11
+ ```java
12
+ package com.teratail.q362100;
13
+
14
+ import androidx.appcompat.app.AppCompatActivity;
15
+ import androidx.core.content.ContextCompat;
16
+
17
+ import android.Manifest;
18
+ import android.content.pm.PackageManager;
19
+ import android.media.*;
20
+ import android.os.*;
21
+ import android.widget.Button;
22
+
23
+ public class MainActivity extends AppCompatActivity {
24
+ private static final String TAG = "MainActivity";
25
+
26
+ private static final int PERMISSIONS_REQUEST_RECORD_AUDIO = 99;
27
+
28
+ private static final int AUDIO_SAMPLE_FREQ = 44100;//サンプリング周波数
29
+ private static final int FRAME_RATE = 10;
30
+ private static final String START_TEXT = "START";
31
+ private static final String STOP_TEXT = "STOP";
32
+
33
+ private short audioData[];
34
+ private Button button;
35
+ private HandlerThread listenerThread;
36
+
37
+ public void onCreate(Bundle bundle) {
38
+ super.onCreate(bundle);
39
+ setContentView(R.layout.activity_main);
40
+ setTitle("AudioRecord");
41
+
42
+ SurfaceDrawer surfaceDrawer = new SurfaceDrawer(findViewById(R.id.surfaceView));
43
+
44
+ int frameBufferSize = AUDIO_SAMPLE_FREQ / FRAME_RATE;
45
+ audioData = new short[frameBufferSize];
46
+
47
+ AudioRecord recorder = settingRecorder(AUDIO_SAMPLE_FREQ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, frameBufferSize);
48
+
49
+ listenerThread = new HandlerThread("RecordPositionUpdateListenerThread");
50
+ listenerThread.start();
51
+
52
+ recorder.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
53
+ @Override
54
+ public void onMarkerReached(AudioRecord recorder) {}
55
+ @Override
56
+ public void onPeriodicNotification(AudioRecord recorder) {
57
+ recorder.read(audioData, 0, audioData.length);
58
+ surfaceDrawer.draw(audioData);
59
+ }
60
+ }, new Handler(listenerThread.getLooper())); //HandlerThread でリスナを実行
61
+
62
+ button = findViewById(R.id.button);
63
+ button.setText(START_TEXT);
64
+ button.setEnabled(false);
65
+ button.setOnClickListener(v -> {
66
+ switch(button.getText().toString()) { //トグル
67
+ case START_TEXT:
68
+ recorder.startRecording();
69
+ button.setText(STOP_TEXT);
70
+ break;
71
+ case STOP_TEXT:
72
+ recorder.stop();
73
+ button.setText(START_TEXT);
74
+ break;
75
+ }
76
+ });
77
+
78
+ int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
79
+ if(permissionCheck == PackageManager.PERMISSION_GRANTED) { // すでにユーザーがパーミッションを許可
80
+ button.setEnabled(true);
81
+ } else { // ユーザーはパーミッションを許可していない
82
+ requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSIONS_REQUEST_RECORD_AUDIO);
83
+ }
84
+ }
85
+
86
+ @Override
87
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
88
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
89
+
90
+ if(requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO) {
91
+ if(grantResults[0] != PackageManager.PERMISSION_GRANTED) { // ユーザが許可しなかったらアプリを終了する
92
+ finish();
93
+ }
94
+ button.setEnabled(true);
95
+ }
96
+ }
97
+
98
+ @Override
99
+ protected void onDestroy() {
100
+ super.onDestroy();
101
+ if(listenerThread != null) listenerThread.quit();
102
+ }
103
+
104
+ private AudioRecord settingRecorder(int sampleRateInHz, int channelConfig, int audioFormat, int frameBufferSize) {
105
+ int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
106
+ if(minBufferSize == AudioRecord.ERROR_BAD_VALUE) throw new IllegalArgumentException();
107
+ if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException();
108
+
109
+ int bufferSizeInBytes = Math.max(minBufferSize, frameBufferSize*10); //"*10"は余裕分
110
+
111
+ AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
112
+ if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException();
113
+
114
+ if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException();
115
+
116
+ return recorder;
117
+ }
118
+ }
119
+ ```
120
+ レイアウト: activity_main.xml
121
+ ```xml
122
+ <?xml version="1.0" encoding="utf-8"?>
123
+ <androidx.constraintlayout.widget.ConstraintLayout
124
+ xmlns:android="http://schemas.android.com/apk/res/android"
125
+ xmlns:app="http://schemas.android.com/apk/res-auto"
126
+ xmlns:tools="http://schemas.android.com/tools"
127
+ android:layout_width="match_parent"
128
+ android:layout_height="match_parent"
129
+ tools:context=".MainActivity">
130
+
131
+ <SurfaceView
132
+ android:id="@+id/surfaceView"
133
+ android:layout_width="0dp"
134
+ android:layout_height="0dp"
135
+ app:layout_constraintBottom_toTopOf="@id/button"
136
+ app:layout_constraintLeft_toLeftOf="parent"
137
+ app:layout_constraintRight_toRightOf="parent"
138
+ app:layout_constraintTop_toTopOf="parent" />
139
+
140
+ <Button
141
+ android:id="@+id/button"
142
+ android:layout_width="0dp"
143
+ android:layout_height="wrap_content"
144
+ android:text="START"
145
+ android:layout_margin="10dp"
146
+ app:layout_constraintBottom_toBottomOf="parent"
147
+ app:layout_constraintLeft_toLeftOf="parent"
148
+ app:layout_constraintRight_toRightOf="parent" />
149
+
150
+ </androidx.constraintlayout.widget.ConstraintLayout>
151
+ ```
152
+ SurfaceDrawer.java
153
+ ```java
154
+ package com.teratail.q362100;
155
+
156
+ import android.graphics.Canvas;
157
+ import android.graphics.Color;
158
+ import android.graphics.Paint;
159
+ import android.graphics.Path;
160
+ import android.view.SurfaceHolder;
161
+ import android.view.SurfaceView;
162
+
163
+ public class SurfaceDrawer implements SurfaceHolder.Callback {
164
+ private static final String TAG = "SurfaceDrawer";
165
+
166
+ private static final int COMPRESSION_RATE = 10;
167
+
168
+ private SurfaceHolder holder;
169
+ private float canvasWidth, canvasVerticalCenter;
170
+ private Paint pathPaint;
171
+ private Path path = new Path();
172
+
173
+ public SurfaceDrawer(SurfaceView surfaceView) {
174
+ holder = surfaceView.getHolder();
175
+ holder.addCallback(this);
176
+
177
+ pathPaint = new Paint();
178
+ pathPaint.setAntiAlias(true);
179
+ pathPaint.setStrokeWidth(2);//線幅
180
+ pathPaint.setStyle(Paint.Style.STROKE);
181
+ pathPaint.setColor(Color.GREEN);
182
+ }
183
+
184
+ @Override
185
+ public void surfaceCreated(SurfaceHolder holder) {
186
+ Canvas canvas = holder.lockCanvas();
187
+ if(canvas == null) return;
188
+
189
+ canvas.drawColor(Color.BLACK);
190
+ holder.unlockCanvasAndPost(canvas);
191
+ }
192
+
193
+ @Override
194
+ public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
195
+ canvasWidth = w;
196
+ canvasVerticalCenter = h / 2;
197
+ }
198
+
199
+ @Override
200
+ public void surfaceDestroyed(SurfaceHolder holder) {
201
+ this.holder = null;
202
+ }
203
+
204
+ public void draw(short[] data) {
205
+ if(data == null || data.length < 2) return;
206
+
207
+ Canvas canvas = holder.lockCanvas();
208
+ if(canvas == null) return;
209
+
210
+ canvas.drawColor(Color.BLACK); //塗り潰し
211
+
212
+ path.reset();
213
+ path.moveTo(0, canvasVerticalCenter + data[0] / COMPRESSION_RATE);
214
+ for(int x=1; x<canvasWidth; x++) {
215
+ path.lineTo(x, canvasVerticalCenter + data[(int)(data.length / canvasWidth * x)] / COMPRESSION_RATE);
216
+ }
217
+ canvas.drawPath(path, pathPaint);
218
+
219
+ holder.unlockCanvasAndPost(canvas);
220
+ }
221
+ }
222
+ ```