回答編集履歴

5

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

2021/10/05 14:21

投稿

jimbe
jimbe

スコア12648

test CHANGED
@@ -24,23 +24,31 @@
24
24
 
25
25
 
26
26
 
27
- import androidx.annotation.NonNull;
27
+ import androidx.activity.result.ActivityResultLauncher;
28
+
28
-
29
+ import androidx.activity.result.contract.ActivityResultContracts;
30
+
29
- import androidx.appcompat.app.AppCompatActivity;
31
+ import androidx.appcompat.app.*;
30
32
 
31
33
  import androidx.core.content.ContextCompat;
32
34
 
35
+ import androidx.fragment.app.DialogFragment;
36
+
33
37
 
34
38
 
35
39
  import android.Manifest;
36
40
 
41
+ import android.app.Dialog;
42
+
37
43
  import android.content.pm.PackageManager;
38
44
 
39
45
  import android.media.*;
40
46
 
41
47
  import android.os.*;
42
48
 
49
+ import android.view.Gravity;
50
+
43
- import android.widget.Button;
51
+ import android.widget.*;
44
52
 
45
53
 
46
54
 
@@ -52,10 +60,6 @@
52
60
 
53
61
 
54
62
 
55
- private static final int PERMISSIONS_REQUEST_RECORD_AUDIO = 99;
56
-
57
-
58
-
59
63
  private static final int AUDIO_SAMPLE_FREQ = 44100;//サンプリング周波数
60
64
 
61
65
  private static final int FRAME_RATE = 10;
@@ -66,6 +70,8 @@
66
70
 
67
71
 
68
72
 
73
+ private SurfaceDrawer surfaceDrawer;
74
+
69
75
  private Button button;
70
76
 
71
77
  private HandlerThread listenerThread;
@@ -84,9 +90,135 @@
84
90
 
85
91
 
86
92
 
87
- SurfaceDrawer surfaceDrawer = new SurfaceDrawer(findViewById(R.id.surfaceView));
93
+ surfaceDrawer = new SurfaceDrawer(findViewById(R.id.surfaceView));
94
+
95
+
96
+
88
-
97
+ button = findViewById(R.id.button);
98
+
89
-
99
+ button.setEnabled(true);
100
+
101
+ button.setText(START_TEXT);
102
+
103
+ button.setOnClickListener(v -> {
104
+
105
+ switch(button.getText().toString()) { //トグル
106
+
107
+ case START_TEXT:
108
+
109
+ permissionCheckAndStart();
110
+
111
+ break;
112
+
113
+ case STOP_TEXT:
114
+
115
+ recorder.stop();
116
+
117
+ button.setText(START_TEXT);
118
+
119
+ break;
120
+
121
+ }
122
+
123
+ });
124
+
125
+ }
126
+
127
+
128
+
129
+ //権限要求ダイアログ表示・返答受付
130
+
131
+ private final ActivityResultLauncher<String> requestPermissionLauncher =
132
+
133
+ registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
134
+
135
+ if (isGranted) {
136
+
137
+ start();
138
+
139
+ } else {
140
+
141
+ Toast t = Toast.makeText(this, "権限が許可されなかった為、実行できません。", Toast.LENGTH_LONG);
142
+
143
+ t.setGravity(Gravity.CENTER, 0, 0);
144
+
145
+ t.show();
146
+
147
+ }
148
+
149
+ });
150
+
151
+
152
+
153
+ //権限説明ダイアログ ok 押下 → 権限要求ダイアログ
154
+
155
+ public static class ExplainDialogFragment extends DialogFragment {
156
+
157
+ @Override
158
+
159
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
160
+
161
+ MainActivity activity = (MainActivity)requireActivity();
162
+
163
+ return new AlertDialog.Builder(activity)
164
+
165
+ .setTitle("使用許可の説明")
166
+
167
+ .setMessage("音声波形を表示するにはマイクの使用(「音声の録音」)の許可が必要です")
168
+
169
+ .setNeutralButton(android.R.string.cancel, (dialog,which)->{})
170
+
171
+ .setPositiveButton(android.R.string.ok, (dialog,which)->{
172
+
173
+ activity.requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO);
174
+
175
+ })
176
+
177
+ .create();
178
+
179
+ }
180
+
181
+ }
182
+
183
+
184
+
185
+ //権限チェック
186
+
187
+ private void permissionCheckAndStart() {
188
+
189
+ if(ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
190
+
191
+ start();
192
+
193
+ } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
194
+
195
+ new ExplainDialogFragment().show(getSupportFragmentManager(), "Explain");
196
+
197
+ } else {
198
+
199
+ requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO);
200
+
201
+ }
202
+
203
+ }
204
+
205
+
206
+
207
+ //波形表示開始
208
+
209
+ private void start() {
210
+
211
+ if(recorder == null) initialize();
212
+
213
+ recorder.startRecording();
214
+
215
+ button.setText(STOP_TEXT);
216
+
217
+ }
218
+
219
+
220
+
221
+ private void initialize() {
90
222
 
91
223
  int frameBufferSize = AUDIO_SAMPLE_FREQ / FRAME_RATE;
92
224
 
@@ -110,6 +242,8 @@
110
242
 
111
243
  public void onMarkerReached(AudioRecord recorder) {}
112
244
 
245
+
246
+
113
247
  @Override
114
248
 
115
249
  public void onPeriodicNotification(AudioRecord recorder) {
@@ -122,123 +256,305 @@
122
256
 
123
257
  }, new Handler(listenerThread.getLooper())); //HandlerThread でリスナを実行
124
258
 
125
-
126
-
127
- button = findViewById(R.id.button);
128
-
129
- button.setText(START_TEXT);
130
-
131
- button.setEnabled(false);
132
-
133
- button.setOnClickListener(v -> {
134
-
135
- switch(button.getText().toString()) { //トグル
136
-
137
- case START_TEXT:
259
+ }
138
-
260
+
261
+
262
+
139
- recorder.startRecording();
263
+ private void terminate() {
140
-
141
- button.setText(STOP_TEXT);
264
+
142
-
143
- break;
144
-
145
- case STOP_TEXT:
265
+ if(recorder != null) {
146
-
266
+
147
- recorder.stop();
267
+ recorder.stop();
148
-
149
- button.setText(START_TEXT);
268
+
150
-
151
- break;
152
-
153
- }
154
-
155
- });
156
-
157
-
158
-
159
- int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
160
-
161
- if(permissionCheck == PackageManager.PERMISSION_GRANTED) { // すでにユーザーがパーミッションを許可
162
-
163
- button.setEnabled(true);
269
+ recorder.release();
164
-
165
- } else { // ユーザーはパーミッションを許可していない
166
-
167
- requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSIONS_REQUEST_RECORD_AUDIO);
168
270
 
169
271
  }
170
272
 
273
+ recorder = null;
274
+
275
+
276
+
277
+ if(listenerThread != null) listenerThread.quit();
278
+
279
+ listenerThread = null;
280
+
281
+ }
282
+
283
+
284
+
285
+ private AudioRecord settingRecorder(int sampleRateInHz, int channelConfig, int audioFormat, int frameBufferSize) {
286
+
287
+ int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
288
+
289
+ if(minBufferSize == AudioRecord.ERROR_BAD_VALUE) throw new IllegalArgumentException();
290
+
291
+ if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException("minBufferSize");
292
+
293
+
294
+
295
+ int bufferSizeInBytes = Math.max(minBufferSize, frameBufferSize*10); //"*10"は余裕分
296
+
297
+
298
+
299
+ AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
300
+
301
+ if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException("recorder.getState");
302
+
303
+
304
+
305
+ if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException("setPositionNotificationPeriod frameBufferSize="+frameBufferSize);
306
+
307
+
308
+
309
+ return recorder;
310
+
171
311
  }
172
312
 
173
313
 
174
314
 
175
315
  @Override
176
316
 
177
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
178
-
179
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
180
-
181
-
182
-
183
- if(requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO) {
317
+ protected void onPause() { //停止
184
-
318
+
185
- if(grantResults[0] != PackageManager.PERMISSION_GRANTED) { // ユーザが許可しなかったらアプリを終了する
319
+ super.onPause();
186
-
320
+
187
- finish();
321
+ terminate();
188
-
322
+
189
- }
323
+ }
324
+
325
+
326
+
190
-
327
+ @Override
328
+
329
+ protected void onResume() { //再開
330
+
331
+ super.onResume();
332
+
333
+ if(button.getText().toString().equals(STOP_TEXT)) { //実行中だった
334
+
191
- button.setEnabled(true);
335
+ permissionCheckAndStart();
192
336
 
193
337
  }
194
338
 
195
339
  }
196
340
 
341
+ }
342
+
343
+ ```
344
+
345
+ レイアウト: activity_main.xml
346
+
347
+ ```xml
348
+
349
+ <?xml version="1.0" encoding="utf-8"?>
350
+
351
+ <androidx.constraintlayout.widget.ConstraintLayout
352
+
353
+ xmlns:android="http://schemas.android.com/apk/res/android"
354
+
355
+ xmlns:app="http://schemas.android.com/apk/res-auto"
356
+
357
+ xmlns:tools="http://schemas.android.com/tools"
358
+
359
+ android:layout_width="match_parent"
360
+
361
+ android:layout_height="match_parent"
362
+
363
+ tools:context=".MainActivity">
364
+
365
+
366
+
367
+ <SurfaceView
368
+
369
+ android:id="@+id/surfaceView"
370
+
371
+ android:layout_width="0dp"
372
+
373
+ android:layout_height="0dp"
374
+
375
+ app:layout_constraintBottom_toTopOf="@id/button"
376
+
377
+ app:layout_constraintLeft_toLeftOf="parent"
378
+
379
+ app:layout_constraintRight_toRightOf="parent"
380
+
381
+ app:layout_constraintTop_toTopOf="parent" />
382
+
383
+
384
+
385
+ <Button
386
+
387
+ android:id="@+id/button"
388
+
389
+ android:layout_width="0dp"
390
+
391
+ android:layout_height="wrap_content"
392
+
393
+ android:text="START"
394
+
395
+ android:layout_margin="10dp"
396
+
397
+ app:layout_constraintBottom_toBottomOf="parent"
398
+
399
+ app:layout_constraintLeft_toLeftOf="parent"
400
+
401
+ app:layout_constraintRight_toRightOf="parent" />
402
+
403
+
404
+
405
+ </androidx.constraintlayout.widget.ConstraintLayout>
406
+
407
+ ```
408
+
409
+ SurfaceDrawer.java
410
+
411
+ ```java
412
+
413
+ package com.teratail.q362100;
414
+
415
+
416
+
417
+ import android.graphics.Canvas;
418
+
419
+ import android.graphics.Color;
420
+
421
+ import android.graphics.Paint;
422
+
423
+ import android.graphics.Path;
424
+
425
+ import android.util.Log;
426
+
427
+ import android.view.SurfaceHolder;
428
+
429
+ import android.view.SurfaceView;
430
+
431
+
432
+
433
+ public class SurfaceDrawer implements SurfaceHolder.Callback {
434
+
435
+ @SuppressWarnings("UnusedDeclaration")
436
+
437
+ private static final String TAG = "SurfaceDrawer";
438
+
439
+
440
+
441
+ private static final float COMPRESSION_RATE = 10f;
442
+
443
+
444
+
445
+ private SurfaceHolder holder;
446
+
447
+ private float canvasWidth, canvasVerticalCenter;
448
+
449
+ private final Paint pathPaint;
450
+
451
+ private final Path path = new Path();
452
+
453
+
454
+
455
+ public SurfaceDrawer(SurfaceView surfaceView) {
456
+
457
+ holder = surfaceView.getHolder();
458
+
459
+ holder.addCallback(this);
460
+
461
+
462
+
463
+ pathPaint = new Paint();
464
+
465
+ pathPaint.setAntiAlias(true);
466
+
467
+ pathPaint.setStrokeWidth(2);//線幅
468
+
469
+ pathPaint.setStyle(Paint.Style.STROKE);
470
+
471
+ pathPaint.setColor(Color.GREEN);
472
+
473
+ }
474
+
197
475
 
198
476
 
199
477
  @Override
200
478
 
201
- protected void onDestroy() {
202
-
203
- super.onDestroy();
204
-
205
- if(listenerThread != null) listenerThread.quit();
206
-
207
- listenerThread = null;
208
-
209
- if(recorder != null) recorder.release();
210
-
211
- recorder = null;
212
-
213
- }
214
-
215
-
216
-
217
- private AudioRecord settingRecorder(int sampleRateInHz, int channelConfig, int audioFormat, int frameBufferSize) {
218
-
219
- int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
220
-
221
- if(minBufferSize == AudioRecord.ERROR_BAD_VALUE) throw new IllegalArgumentException();
222
-
223
- if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException("minBufferSize");
224
-
225
-
226
-
227
- int bufferSizeInBytes = Math.max(minBufferSize, frameBufferSize*10); //"*10"は余裕分
228
-
229
-
230
-
231
- AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
232
-
233
- if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException("recorder.getState");
234
-
235
-
236
-
237
- if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException("setPositionNotificationPeriod frameBufferSize="+frameBufferSize);
238
-
239
-
240
-
241
- return recorder;
479
+ public void surfaceCreated(SurfaceHolder holder) {
480
+
481
+ //Log.d(TAG, "surfaceCreated");
482
+
483
+ Canvas canvas = holder.lockCanvas();
484
+
485
+ if(canvas == null) return;
486
+
487
+
488
+
489
+ canvas.drawColor(Color.BLACK);
490
+
491
+ holder.unlockCanvasAndPost(canvas);
492
+
493
+ }
494
+
495
+
496
+
497
+ @Override
498
+
499
+ public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
500
+
501
+ //Log.d(TAG, "surfaceChanged w="+w+", h="+h);
502
+
503
+ canvasWidth = w;
504
+
505
+ canvasVerticalCenter = h / 2f;
506
+
507
+ }
508
+
509
+
510
+
511
+ @Override
512
+
513
+ public void surfaceDestroyed(SurfaceHolder holder) {
514
+
515
+ //Log.d(TAG, "surfaceDestroyed");
516
+
517
+ }
518
+
519
+
520
+
521
+ public void draw(short[] data) {
522
+
523
+ if(data == null || data.length < 2) return;
524
+
525
+
526
+
527
+ Canvas canvas = holder.lockCanvas();
528
+
529
+ if(canvas == null) {
530
+
531
+ Log.d(TAG, "holder.lockCanvas() is null.");
532
+
533
+ return;
534
+
535
+ }
536
+
537
+
538
+
539
+ canvas.drawColor(Color.BLACK); //塗り潰し
540
+
541
+
542
+
543
+ path.rewind();
544
+
545
+ path.moveTo(0, canvasVerticalCenter + data[0] / COMPRESSION_RATE);
546
+
547
+ for(int x=1; x<canvasWidth; x++) {
548
+
549
+ path.lineTo(x, canvasVerticalCenter + data[(int)(data.length / canvasWidth * x)] / COMPRESSION_RATE);
550
+
551
+ }
552
+
553
+ canvas.drawPath(path, pathPaint);
554
+
555
+
556
+
557
+ holder.unlockCanvasAndPost(canvas);
242
558
 
243
559
  }
244
560
 
@@ -246,348 +562,20 @@
246
562
 
247
563
  ```
248
564
 
565
+ app/build.gradle
566
+
567
+ dependencies に追加
568
+
569
+ ```plain
570
+
571
+ implementation 'androidx.activity:activity:1.3.1'
572
+
573
+ ```
574
+
249
- レイアウト: activity_main.xml
575
+ AndroidManifest.xml 追加
250
576
 
251
577
  ```xml
252
578
 
253
- <?xml version="1.0" encoding="utf-8"?>
579
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
254
-
255
- <androidx.constraintlayout.widget.ConstraintLayout
256
-
257
- xmlns:android="http://schemas.android.com/apk/res/android"
258
-
259
- xmlns:app="http://schemas.android.com/apk/res-auto"
260
-
261
- xmlns:tools="http://schemas.android.com/tools"
262
-
263
- android:layout_width="match_parent"
264
-
265
- android:layout_height="match_parent"
266
-
267
- tools:context=".MainActivity">
268
-
269
-
270
-
271
- <SurfaceView
272
-
273
- android:id="@+id/surfaceView"
274
-
275
- android:layout_width="0dp"
276
-
277
- android:layout_height="0dp"
278
-
279
- app:layout_constraintBottom_toTopOf="@id/button"
280
-
281
- app:layout_constraintLeft_toLeftOf="parent"
282
-
283
- app:layout_constraintRight_toRightOf="parent"
284
-
285
- app:layout_constraintTop_toTopOf="parent" />
286
-
287
-
288
-
289
- <Button
290
-
291
- android:id="@+id/button"
292
-
293
- android:layout_width="0dp"
294
-
295
- android:layout_height="wrap_content"
296
-
297
- android:text="START"
298
-
299
- android:layout_margin="10dp"
300
-
301
- app:layout_constraintBottom_toBottomOf="parent"
302
-
303
- app:layout_constraintLeft_toLeftOf="parent"
304
-
305
- app:layout_constraintRight_toRightOf="parent" />
306
-
307
-
308
-
309
- </androidx.constraintlayout.widget.ConstraintLayout>
310
580
 
311
581
  ```
312
-
313
- SurfaceDrawer.java
314
-
315
- ```java
316
-
317
- package com.teratail.q362100;
318
-
319
-
320
-
321
- import android.graphics.Canvas;
322
-
323
- import android.graphics.Color;
324
-
325
- import android.graphics.Paint;
326
-
327
- import android.graphics.Path;
328
-
329
- import android.view.SurfaceHolder;
330
-
331
- import android.view.SurfaceView;
332
-
333
-
334
-
335
- public class SurfaceDrawer implements SurfaceHolder.Callback {
336
-
337
- @SuppressWarnings("UnusedDeclaration")
338
-
339
- private static final String TAG = "SurfaceDrawer";
340
-
341
-
342
-
343
- private static final float COMPRESSION_RATE = 10f;
344
-
345
-
346
-
347
- private SurfaceHolder holder;
348
-
349
- private float canvasWidth, canvasVerticalCenter;
350
-
351
- private final Paint pathPaint;
352
-
353
- private final Path path = new Path();
354
-
355
-
356
-
357
- public SurfaceDrawer(SurfaceView surfaceView) {
358
-
359
- holder = surfaceView.getHolder();
360
-
361
- holder.addCallback(this);
362
-
363
-
364
-
365
- pathPaint = new Paint();
366
-
367
- pathPaint.setAntiAlias(true);
368
-
369
- pathPaint.setStrokeWidth(2);//線幅
370
-
371
- pathPaint.setStyle(Paint.Style.STROKE);
372
-
373
- pathPaint.setColor(Color.GREEN);
374
-
375
- }
376
-
377
-
378
-
379
- @Override
380
-
381
- public void surfaceCreated(SurfaceHolder holder) {
382
-
383
- Canvas canvas = holder.lockCanvas();
384
-
385
- if(canvas == null) return;
386
-
387
-
388
-
389
- canvas.drawColor(Color.BLACK);
390
-
391
- holder.unlockCanvasAndPost(canvas);
392
-
393
- }
394
-
395
-
396
-
397
- @Override
398
-
399
- public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
400
-
401
- canvasWidth = w;
402
-
403
- canvasVerticalCenter = h / 2f;
404
-
405
- }
406
-
407
-
408
-
409
- @Override
410
-
411
- public void surfaceDestroyed(SurfaceHolder holder) {
412
-
413
- this.holder = null;
414
-
415
- }
416
-
417
-
418
-
419
- public void draw(short[] data) {
420
-
421
- if(data == null || data.length < 2) return;
422
-
423
-
424
-
425
- Canvas canvas = holder.lockCanvas();
426
-
427
- if(canvas == null) return;
428
-
429
-
430
-
431
- canvas.drawColor(Color.BLACK); //塗り潰し
432
-
433
-
434
-
435
- path.rewind();
436
-
437
- path.moveTo(0, canvasVerticalCenter + data[0] / COMPRESSION_RATE);
438
-
439
- for(int x=1; x<canvasWidth; x++) {
440
-
441
- path.lineTo(x, canvasVerticalCenter + data[(int)(data.length / canvasWidth * x)] / COMPRESSION_RATE);
442
-
443
- }
444
-
445
- canvas.drawPath(path, pathPaint);
446
-
447
-
448
-
449
- holder.unlockCanvasAndPost(canvas);
450
-
451
- }
452
-
453
- }
454
-
455
- ```
456
-
457
- app/build.gradle
458
-
459
- ```plain
460
-
461
- plugins {
462
-
463
- id 'com.android.application'
464
-
465
- }
466
-
467
-
468
-
469
- android {
470
-
471
- compileSdk 30
472
-
473
-
474
-
475
- defaultConfig {
476
-
477
- applicationId "com.teratail.q362100"
478
-
479
- minSdk 29
480
-
481
- targetSdk 30
482
-
483
- versionCode 1
484
-
485
- versionName "1.0"
486
-
487
-
488
-
489
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
490
-
491
- }
492
-
493
-
494
-
495
- buildTypes {
496
-
497
- release {
498
-
499
- minifyEnabled false
500
-
501
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
502
-
503
- }
504
-
505
- }
506
-
507
- compileOptions {
508
-
509
- sourceCompatibility JavaVersion.VERSION_1_8
510
-
511
- targetCompatibility JavaVersion.VERSION_1_8
512
-
513
- }
514
-
515
- }
516
-
517
-
518
-
519
- dependencies {
520
-
521
-
522
-
523
- implementation 'androidx.appcompat:appcompat:1.3.1'
524
-
525
- implementation 'com.google.android.material:material:1.4.0'
526
-
527
- implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
528
-
529
- testImplementation 'junit:junit:4.+'
530
-
531
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
532
-
533
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
534
-
535
- }
536
-
537
- ```
538
-
539
- AndroidManifest.xml
540
-
541
- ```xml
542
-
543
- <?xml version="1.0" encoding="utf-8"?>
544
-
545
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
546
-
547
- package="com.teratail.q362100">
548
-
549
-
550
-
551
- <uses-permission android:name="android.permission.RECORD_AUDIO" />
552
-
553
-
554
-
555
- <application
556
-
557
- android:allowBackup="true"
558
-
559
- android:icon="@mipmap/ic_launcher"
560
-
561
- android:label="@string/app_name"
562
-
563
- android:roundIcon="@mipmap/ic_launcher_round"
564
-
565
- android:supportsRtl="true"
566
-
567
- android:theme="@style/Theme.Q362100">
568
-
569
- <activity
570
-
571
- android:name=".MainActivity"
572
-
573
- android:exported="true">
574
-
575
- <intent-filter>
576
-
577
- <action android:name="android.intent.action.MAIN" />
578
-
579
-
580
-
581
- <category android:name="android.intent.category.LAUNCHER" />
582
-
583
- </intent-filter>
584
-
585
- </activity>
586
-
587
- </application>
588
-
589
-
590
-
591
- </manifest>
592
-
593
- ```

4

settingRecorder() 修正、AndroidManifest.xml 追加

2021/10/05 14:21

投稿

jimbe
jimbe

スコア12648

test CHANGED
@@ -220,7 +220,7 @@
220
220
 
221
221
  if(minBufferSize == AudioRecord.ERROR_BAD_VALUE) throw new IllegalArgumentException();
222
222
 
223
- if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException();
223
+ if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException("minBufferSize");
224
224
 
225
225
 
226
226
 
@@ -230,11 +230,11 @@
230
230
 
231
231
  AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
232
232
 
233
- if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException();
233
+ if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException("recorder.getState");
234
-
235
-
236
-
234
+
235
+
236
+
237
- if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException();
237
+ if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException("setPositionNotificationPeriod frameBufferSize="+frameBufferSize);
238
238
 
239
239
 
240
240
 
@@ -535,3 +535,59 @@
535
535
  }
536
536
 
537
537
  ```
538
+
539
+ AndroidManifest.xml
540
+
541
+ ```xml
542
+
543
+ <?xml version="1.0" encoding="utf-8"?>
544
+
545
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
546
+
547
+ package="com.teratail.q362100">
548
+
549
+
550
+
551
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
552
+
553
+
554
+
555
+ <application
556
+
557
+ android:allowBackup="true"
558
+
559
+ android:icon="@mipmap/ic_launcher"
560
+
561
+ android:label="@string/app_name"
562
+
563
+ android:roundIcon="@mipmap/ic_launcher_round"
564
+
565
+ android:supportsRtl="true"
566
+
567
+ android:theme="@style/Theme.Q362100">
568
+
569
+ <activity
570
+
571
+ android:name=".MainActivity"
572
+
573
+ android:exported="true">
574
+
575
+ <intent-filter>
576
+
577
+ <action android:name="android.intent.action.MAIN" />
578
+
579
+
580
+
581
+ <category android:name="android.intent.category.LAUNCHER" />
582
+
583
+ </intent-filter>
584
+
585
+ </activity>
586
+
587
+ </application>
588
+
589
+
590
+
591
+ </manifest>
592
+
593
+ ```

3

コード微量修正

2021/10/05 02:59

投稿

jimbe
jimbe

スコア12648

test CHANGED
@@ -24,6 +24,8 @@
24
24
 
25
25
 
26
26
 
27
+ import androidx.annotation.NonNull;
28
+
27
29
  import androidx.appcompat.app.AppCompatActivity;
28
30
 
29
31
  import androidx.core.content.ContextCompat;
@@ -44,6 +46,8 @@
44
46
 
45
47
  public class MainActivity extends AppCompatActivity {
46
48
 
49
+ @SuppressWarnings("UnusedDeclaration")
50
+
47
51
  private static final String TAG = "MainActivity";
48
52
 
49
53
 
@@ -62,12 +66,12 @@
62
66
 
63
67
 
64
68
 
65
- private short audioData[];
66
-
67
69
  private Button button;
68
70
 
69
71
  private HandlerThread listenerThread;
70
72
 
73
+ private AudioRecord recorder;
74
+
71
75
 
72
76
 
73
77
  public void onCreate(Bundle bundle) {
@@ -86,11 +90,11 @@
86
90
 
87
91
  int frameBufferSize = AUDIO_SAMPLE_FREQ / FRAME_RATE;
88
92
 
89
- audioData = new short[frameBufferSize];
93
+ short[] audioData = new short[frameBufferSize];
90
-
91
-
92
-
94
+
95
+
96
+
93
- AudioRecord recorder = settingRecorder(AUDIO_SAMPLE_FREQ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, frameBufferSize);
97
+ recorder = settingRecorder(AUDIO_SAMPLE_FREQ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, frameBufferSize);
94
98
 
95
99
 
96
100
 
@@ -170,7 +174,7 @@
170
174
 
171
175
  @Override
172
176
 
173
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
177
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
174
178
 
175
179
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
176
180
 
@@ -200,6 +204,12 @@
200
204
 
201
205
  if(listenerThread != null) listenerThread.quit();
202
206
 
207
+ listenerThread = null;
208
+
209
+ if(recorder != null) recorder.release();
210
+
211
+ recorder = null;
212
+
203
213
  }
204
214
 
205
215
 
@@ -324,11 +334,13 @@
324
334
 
325
335
  public class SurfaceDrawer implements SurfaceHolder.Callback {
326
336
 
337
+ @SuppressWarnings("UnusedDeclaration")
338
+
327
339
  private static final String TAG = "SurfaceDrawer";
328
340
 
329
341
 
330
342
 
331
- private static final int COMPRESSION_RATE = 10;
343
+ private static final float COMPRESSION_RATE = 10f;
332
344
 
333
345
 
334
346
 
@@ -336,9 +348,9 @@
336
348
 
337
349
  private float canvasWidth, canvasVerticalCenter;
338
350
 
339
- private Paint pathPaint;
351
+ private final Paint pathPaint;
340
-
352
+
341
- private Path path = new Path();
353
+ private final Path path = new Path();
342
354
 
343
355
 
344
356
 
@@ -388,7 +400,7 @@
388
400
 
389
401
  canvasWidth = w;
390
402
 
391
- canvasVerticalCenter = h / 2;
403
+ canvasVerticalCenter = h / 2f;
392
404
 
393
405
  }
394
406
 
@@ -420,7 +432,7 @@
420
432
 
421
433
 
422
434
 
423
- path.reset();
435
+ path.rewind();
424
436
 
425
437
  path.moveTo(0, canvasVerticalCenter + data[0] / COMPRESSION_RATE);
426
438
 

2

app/build.gradle 追加

2021/10/01 11:39

投稿

jimbe
jimbe

スコア12648

test CHANGED
@@ -441,3 +441,85 @@
441
441
  }
442
442
 
443
443
  ```
444
+
445
+ app/build.gradle
446
+
447
+ ```plain
448
+
449
+ plugins {
450
+
451
+ id 'com.android.application'
452
+
453
+ }
454
+
455
+
456
+
457
+ android {
458
+
459
+ compileSdk 30
460
+
461
+
462
+
463
+ defaultConfig {
464
+
465
+ applicationId "com.teratail.q362100"
466
+
467
+ minSdk 29
468
+
469
+ targetSdk 30
470
+
471
+ versionCode 1
472
+
473
+ versionName "1.0"
474
+
475
+
476
+
477
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
478
+
479
+ }
480
+
481
+
482
+
483
+ buildTypes {
484
+
485
+ release {
486
+
487
+ minifyEnabled false
488
+
489
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
490
+
491
+ }
492
+
493
+ }
494
+
495
+ compileOptions {
496
+
497
+ sourceCompatibility JavaVersion.VERSION_1_8
498
+
499
+ targetCompatibility JavaVersion.VERSION_1_8
500
+
501
+ }
502
+
503
+ }
504
+
505
+
506
+
507
+ dependencies {
508
+
509
+
510
+
511
+ implementation 'androidx.appcompat:appcompat:1.3.1'
512
+
513
+ implementation 'com.google.android.material:material:1.4.0'
514
+
515
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
516
+
517
+ testImplementation 'junit:junit:4.+'
518
+
519
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
520
+
521
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
522
+
523
+ }
524
+
525
+ ```

1

コード追加

2021/10/01 11:13

投稿

jimbe
jimbe

スコア12648

test CHANGED
@@ -5,3 +5,439 @@
5
5
  のみに関しましては、 MySurfaceView に frn 配列へのセッターメソッドを作り、 MainPage で録音後に MySurfaceView インスタンスに対してそのセッターを呼ぶだけに思います。
6
6
 
7
7
  ですが、双方とも別スレッドで動作していますので、使用・更新のタイミングを意識しておかないと、表示が乱れる等の現象が発生するかもしれません。
8
+
9
+
10
+
11
+ ----
12
+
13
+
14
+
15
+ オシロスコープのように動くモノが出来ましたので公開させていただきます。
16
+
17
+
18
+
19
+ MainActivity.java
20
+
21
+ ```java
22
+
23
+ package com.teratail.q362100;
24
+
25
+
26
+
27
+ import androidx.appcompat.app.AppCompatActivity;
28
+
29
+ import androidx.core.content.ContextCompat;
30
+
31
+
32
+
33
+ import android.Manifest;
34
+
35
+ import android.content.pm.PackageManager;
36
+
37
+ import android.media.*;
38
+
39
+ import android.os.*;
40
+
41
+ import android.widget.Button;
42
+
43
+
44
+
45
+ public class MainActivity extends AppCompatActivity {
46
+
47
+ private static final String TAG = "MainActivity";
48
+
49
+
50
+
51
+ private static final int PERMISSIONS_REQUEST_RECORD_AUDIO = 99;
52
+
53
+
54
+
55
+ private static final int AUDIO_SAMPLE_FREQ = 44100;//サンプリング周波数
56
+
57
+ private static final int FRAME_RATE = 10;
58
+
59
+ private static final String START_TEXT = "START";
60
+
61
+ private static final String STOP_TEXT = "STOP";
62
+
63
+
64
+
65
+ private short audioData[];
66
+
67
+ private Button button;
68
+
69
+ private HandlerThread listenerThread;
70
+
71
+
72
+
73
+ public void onCreate(Bundle bundle) {
74
+
75
+ super.onCreate(bundle);
76
+
77
+ setContentView(R.layout.activity_main);
78
+
79
+ setTitle("AudioRecord");
80
+
81
+
82
+
83
+ SurfaceDrawer surfaceDrawer = new SurfaceDrawer(findViewById(R.id.surfaceView));
84
+
85
+
86
+
87
+ int frameBufferSize = AUDIO_SAMPLE_FREQ / FRAME_RATE;
88
+
89
+ audioData = new short[frameBufferSize];
90
+
91
+
92
+
93
+ AudioRecord recorder = settingRecorder(AUDIO_SAMPLE_FREQ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, frameBufferSize);
94
+
95
+
96
+
97
+ listenerThread = new HandlerThread("RecordPositionUpdateListenerThread");
98
+
99
+ listenerThread.start();
100
+
101
+
102
+
103
+ recorder.setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
104
+
105
+ @Override
106
+
107
+ public void onMarkerReached(AudioRecord recorder) {}
108
+
109
+ @Override
110
+
111
+ public void onPeriodicNotification(AudioRecord recorder) {
112
+
113
+ recorder.read(audioData, 0, audioData.length);
114
+
115
+ surfaceDrawer.draw(audioData);
116
+
117
+ }
118
+
119
+ }, new Handler(listenerThread.getLooper())); //HandlerThread でリスナを実行
120
+
121
+
122
+
123
+ button = findViewById(R.id.button);
124
+
125
+ button.setText(START_TEXT);
126
+
127
+ button.setEnabled(false);
128
+
129
+ button.setOnClickListener(v -> {
130
+
131
+ switch(button.getText().toString()) { //トグル
132
+
133
+ case START_TEXT:
134
+
135
+ recorder.startRecording();
136
+
137
+ button.setText(STOP_TEXT);
138
+
139
+ break;
140
+
141
+ case STOP_TEXT:
142
+
143
+ recorder.stop();
144
+
145
+ button.setText(START_TEXT);
146
+
147
+ break;
148
+
149
+ }
150
+
151
+ });
152
+
153
+
154
+
155
+ int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
156
+
157
+ if(permissionCheck == PackageManager.PERMISSION_GRANTED) { // すでにユーザーがパーミッションを許可
158
+
159
+ button.setEnabled(true);
160
+
161
+ } else { // ユーザーはパーミッションを許可していない
162
+
163
+ requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSIONS_REQUEST_RECORD_AUDIO);
164
+
165
+ }
166
+
167
+ }
168
+
169
+
170
+
171
+ @Override
172
+
173
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
174
+
175
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
176
+
177
+
178
+
179
+ if(requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO) {
180
+
181
+ if(grantResults[0] != PackageManager.PERMISSION_GRANTED) { // ユーザが許可しなかったらアプリを終了する
182
+
183
+ finish();
184
+
185
+ }
186
+
187
+ button.setEnabled(true);
188
+
189
+ }
190
+
191
+ }
192
+
193
+
194
+
195
+ @Override
196
+
197
+ protected void onDestroy() {
198
+
199
+ super.onDestroy();
200
+
201
+ if(listenerThread != null) listenerThread.quit();
202
+
203
+ }
204
+
205
+
206
+
207
+ private AudioRecord settingRecorder(int sampleRateInHz, int channelConfig, int audioFormat, int frameBufferSize) {
208
+
209
+ int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
210
+
211
+ if(minBufferSize == AudioRecord.ERROR_BAD_VALUE) throw new IllegalArgumentException();
212
+
213
+ if(minBufferSize == AudioRecord.ERROR) throw new IllegalStateException();
214
+
215
+
216
+
217
+ int bufferSizeInBytes = Math.max(minBufferSize, frameBufferSize*10); //"*10"は余裕分
218
+
219
+
220
+
221
+ AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
222
+
223
+ if(recorder.getState() != AudioRecord.STATE_INITIALIZED) throw new IllegalStateException();
224
+
225
+
226
+
227
+ if(recorder.setPositionNotificationPeriod(frameBufferSize) != AudioRecord.SUCCESS) throw new IllegalStateException();
228
+
229
+
230
+
231
+ return recorder;
232
+
233
+ }
234
+
235
+ }
236
+
237
+ ```
238
+
239
+ レイアウト: activity_main.xml
240
+
241
+ ```xml
242
+
243
+ <?xml version="1.0" encoding="utf-8"?>
244
+
245
+ <androidx.constraintlayout.widget.ConstraintLayout
246
+
247
+ xmlns:android="http://schemas.android.com/apk/res/android"
248
+
249
+ xmlns:app="http://schemas.android.com/apk/res-auto"
250
+
251
+ xmlns:tools="http://schemas.android.com/tools"
252
+
253
+ android:layout_width="match_parent"
254
+
255
+ android:layout_height="match_parent"
256
+
257
+ tools:context=".MainActivity">
258
+
259
+
260
+
261
+ <SurfaceView
262
+
263
+ android:id="@+id/surfaceView"
264
+
265
+ android:layout_width="0dp"
266
+
267
+ android:layout_height="0dp"
268
+
269
+ app:layout_constraintBottom_toTopOf="@id/button"
270
+
271
+ app:layout_constraintLeft_toLeftOf="parent"
272
+
273
+ app:layout_constraintRight_toRightOf="parent"
274
+
275
+ app:layout_constraintTop_toTopOf="parent" />
276
+
277
+
278
+
279
+ <Button
280
+
281
+ android:id="@+id/button"
282
+
283
+ android:layout_width="0dp"
284
+
285
+ android:layout_height="wrap_content"
286
+
287
+ android:text="START"
288
+
289
+ android:layout_margin="10dp"
290
+
291
+ app:layout_constraintBottom_toBottomOf="parent"
292
+
293
+ app:layout_constraintLeft_toLeftOf="parent"
294
+
295
+ app:layout_constraintRight_toRightOf="parent" />
296
+
297
+
298
+
299
+ </androidx.constraintlayout.widget.ConstraintLayout>
300
+
301
+ ```
302
+
303
+ SurfaceDrawer.java
304
+
305
+ ```java
306
+
307
+ package com.teratail.q362100;
308
+
309
+
310
+
311
+ import android.graphics.Canvas;
312
+
313
+ import android.graphics.Color;
314
+
315
+ import android.graphics.Paint;
316
+
317
+ import android.graphics.Path;
318
+
319
+ import android.view.SurfaceHolder;
320
+
321
+ import android.view.SurfaceView;
322
+
323
+
324
+
325
+ public class SurfaceDrawer implements SurfaceHolder.Callback {
326
+
327
+ private static final String TAG = "SurfaceDrawer";
328
+
329
+
330
+
331
+ private static final int COMPRESSION_RATE = 10;
332
+
333
+
334
+
335
+ private SurfaceHolder holder;
336
+
337
+ private float canvasWidth, canvasVerticalCenter;
338
+
339
+ private Paint pathPaint;
340
+
341
+ private Path path = new Path();
342
+
343
+
344
+
345
+ public SurfaceDrawer(SurfaceView surfaceView) {
346
+
347
+ holder = surfaceView.getHolder();
348
+
349
+ holder.addCallback(this);
350
+
351
+
352
+
353
+ pathPaint = new Paint();
354
+
355
+ pathPaint.setAntiAlias(true);
356
+
357
+ pathPaint.setStrokeWidth(2);//線幅
358
+
359
+ pathPaint.setStyle(Paint.Style.STROKE);
360
+
361
+ pathPaint.setColor(Color.GREEN);
362
+
363
+ }
364
+
365
+
366
+
367
+ @Override
368
+
369
+ public void surfaceCreated(SurfaceHolder holder) {
370
+
371
+ Canvas canvas = holder.lockCanvas();
372
+
373
+ if(canvas == null) return;
374
+
375
+
376
+
377
+ canvas.drawColor(Color.BLACK);
378
+
379
+ holder.unlockCanvasAndPost(canvas);
380
+
381
+ }
382
+
383
+
384
+
385
+ @Override
386
+
387
+ public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
388
+
389
+ canvasWidth = w;
390
+
391
+ canvasVerticalCenter = h / 2;
392
+
393
+ }
394
+
395
+
396
+
397
+ @Override
398
+
399
+ public void surfaceDestroyed(SurfaceHolder holder) {
400
+
401
+ this.holder = null;
402
+
403
+ }
404
+
405
+
406
+
407
+ public void draw(short[] data) {
408
+
409
+ if(data == null || data.length < 2) return;
410
+
411
+
412
+
413
+ Canvas canvas = holder.lockCanvas();
414
+
415
+ if(canvas == null) return;
416
+
417
+
418
+
419
+ canvas.drawColor(Color.BLACK); //塗り潰し
420
+
421
+
422
+
423
+ path.reset();
424
+
425
+ path.moveTo(0, canvasVerticalCenter + data[0] / COMPRESSION_RATE);
426
+
427
+ for(int x=1; x<canvasWidth; x++) {
428
+
429
+ path.lineTo(x, canvasVerticalCenter + data[(int)(data.length / canvasWidth * x)] / COMPRESSION_RATE);
430
+
431
+ }
432
+
433
+ canvas.drawPath(path, pathPaint);
434
+
435
+
436
+
437
+ holder.unlockCanvasAndPost(canvas);
438
+
439
+ }
440
+
441
+ }
442
+
443
+ ```