260 likes | 279 Views
Explore various approaches for recording and playing audio in Android apps, including using MediaPlayer, AudioRecord, MediaRecorder, JET, SoundPool, and more. Learn about different techniques, latency issues, and permissions for a seamless audio experience.
E N D
Approaches • For playing audio • Play a stream with media player • Easiest • Works with compressed files • Works with urls and files • Works with shoutcast streams • Use audio tracks • With pcm only • Allows you to make sounds on the fly • Might be too slow to decompress on the fly (use media player for compressed audio) • Low latency for short files that fit in memory • Higher latency for longer files • But will need to repeatedly fill buffer (this is why media play is easier) • JET • For playing MIDI audio • Good for making a keyboard app • Short delay from button to sound • Not covered this semester • Sound pool • Store a bunch of optionally compressed files (included in app) • Predecompress • Play quickly on demand • Good for game sound effects • Not covered • For recording • Media player • Can record and compressed • Easy • AudioRecord • Record pcm • Need to empty buffer • NDK • Open SL ES • Can decompress to pcm • Allows sound effects (equalization, reverb, etc.) to be added • Maybe we will cover this later in the semester
MediaPlayer • Make app MediaPlayerFun with 4 buttons • Start recording (id=StartRecordingButton) • Stop recording (id=StopRecordingButton) • Start playback (id=StartPlaybackButton) • Stop playback (id=StopPlaybackButton) • Include permission to • Record Audio • Use Internet • There is no permission to play on audio device. • So a malicious app could play a scary noise in the middle of the night! • Include three class attributes for this activity • final private static String FILE_NAME = "audio.mp4"; • MediaRecorderaudioRecorder; • MediaPlayeraudioPlayer; • String pathForAppDataFiles;
Recording (1) • Make onClickListener for startRecordingButton • startRecordingButton.setOnClickListener(new View.OnClickListener() {} ); • Get MediaRecorder • if (audioRecorder!=null) audioRecorder.release(); • if (audioRecorder == null) audioRecorder = new MediaRecorder(); • Get path for file • Note: each app runs in its own VM, with its own private directory and files. The SDK provides several tools for accessing the apps directory and files • The apps directory is at /data/data/<package name> • Files are at /data/data/<package name>/files • FileOutputStreamfos; // in java.io.FileOutputStream • fos = Context.openFileOutput(“filename.txt”,MODE_PRIVATE); // opens file /data/data/<package name>/files/filename.txt for writing • similarly • FileInputStreamfis; // in java.io.FileOutputStream • fis = Context.openFileInput(“filename.txt”); // opens file /data/data/<package name>/files/filename.txt for reading • MediaRecorder and MediaPlayer need the full path • In OnCreate, add • pathAndNameOfAudioFile = getFilesDir().getAbsolutePath(); // returns /data/data/<package name>/files • pathAndNameOfAudioFile += "/"+FILE_NAME; // file name with full path
logging • The SDK provides logging • Log.e(tag, string) • E.g., add class attribute • String TAG = "MediaPlayerFun"; • Log.e(TAG ,"Set file name: "+pathAndNameOfAudioFile); • The log can be seen from the DDMS • Or from the command line • C:\android\android-sdk-windows\platform-tools> adb –d logcat • C:\android\android-sdk-windows\platform-tools> adb –e logcat
Set up media recorder • audioRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); • Options instead of MIC : • CAMCORDER Microphone audio source with same orientation as camera if available, the main device microphone otherwise • DEFAULT • MIC Microphone audio source • VOICE_CALL Voice call uplink + downlink audio source // remember this when we record phone calls • VOICE_DOWNLINK Voice call downlink (Rx) audio source • VOICE_RECOGNITION Microphone audio source tuned for voice recognition if available, behaves like DEFAULT otherwise. • VOICE_UPLINK Voice call uplink (Tx) audio source • audioRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); • options • DEFAULT • MPEG_4: MPEG4 media file format • THREE_GPP :3GPP media file format • AMR (adaptive multi-rate) good for speech • RAW_AMR • AMR_NB • NB = narrowband • Silence detection • AMR_WB • Wv = wideband • Same as G.722.2 • FLAC and .ogg are missing? Maybe added later • audioRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); • options • ACC • AMR_NB : AMR (Narrowband) for speech • AMR_WB: (AMR (wideband) • DEFAULT • audioRecorder.setOutputFile(pathAndNameOfAudioFile);
Record try { audioRecorder.prepare(); audioRecorder.start(); } catch (Exception e) { Log.e(TAG, "Failed to prepare and start audio recording", e); } startRecordingButton.setVisibility(View.INVISIBLE); //stopRecordingButton.setVisibility(View.VISIBLE); //startPlaybackButton.setVisibility(View.INVISIBLE); //stopPlaybackButton.setVisibility(View.INVISIBLE);
Stop recording if (audioRecorder==null) return; audioRecorder.stop(); audioRecorder.release(); audioRecorder = null; Log.e(TAG ,"Finished recording"); • Make onClickListener for stopRecordingButton • Make nice buttons startRecordingButton.setVisibility(View.VISIBLE); stopRecordingButton.setVisibility(View.INVISIBLE); //startPlaybackButton.setVisibility(View.INVISIBLE); //stopPlaybackButton.setVisibility(View.INVISIBLE); • Try it • Run on device or emulator • emulator is slow, so the quality is bad • Get file from emulator using the DDMS and play in quickTime • Get file from device via adb • adb -d pull /data/data/edu.udel.eleg454.AudioFun/files/audio.mp4 c:\audio.mp4
Playback • Make startPlaybackButton and onClickListener • Button startPlaybackButton = (Button)findViewById(R.id.startPlaybackButton); • startPlaybackButton.setOnClickListener(new View.OnClickListener() {}); • Add to listener • //Get clean MediaPlayer • if (audioPlayer!=null) • audioPlayer.release(); • if (audioPlayer == null) • audioPlayer = new MediaPlayer (); • //Play • try { • audioPlayer.setDataSource(pathAndNameOfAudioFile); • audioPlayer.prepare(); • audioPlayer.start(); • } catch (Exception e) { • Log.e(TAG, "Playback failed.", e); • } • Try it… it fails • The file cannot be opened for reading
File Permissions • Problem: the file does not have the correct permissions. See adb shell … ls –l • There are several ways to fix this. • Use the file descriptor from when the file was created. But what if we want to play a file that was not created when we run the app this time • Change permissions with chmod • easiest option • Android might not support exec() in the future! • Sloppy • String command = "chmod 666 " + pathAndNameOfAudioFile.toString(); • try { • Runtime.getRuntime().exec(command); • } catch (IOException e1) { • Log.e("SetPermissions", "Couldn't set permissions", e1); • } • Better approach. • Just after the filename is set, add • FileOutputStreamfos; • try { • fos = openFileOutput(FILE_NAME, Context.MODE_WORLD_READABLE|Context.MODE_WORLD_WRITEABLE); • fos.close(); • } catch (FileNotFoundException e1) { • Log.e(TAG,"could not open file"); • return; • } catch (IOException e) { • Log.e(TAG,"could not close the file"); • return; • }
When finished playing • It is important to release the mediaPlayer resource when you are done playing • Inside startPlaybackButton.setOnClickListener(new View.OnClickListener() {, add • audioPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener(){ • @Override • public void onCompletion(MediaPlayer mp) { • startRecordingButton.setVisibility(View.VISIBLE); • stopRecordingButton.setVisibility(View.INVISIBLE); • startPlaybackButton.setVisibility(View.VISIBLE); • //stopPlaybackButton.setVisibility(View.INVISIBLE); • audioPlayer.release(); • audioPlayer = null; • }} );
Stopping playback • final Button stopPlaybackButton = (Button)findViewById(R.id.stopPlaybackButton); • stopPlaybackButton.setOnClickListener(new View.OnClickListener() {}); • In onClick, add • startRecordingButton.setVisibility(View.VISIBLE); • stopRecordingButton.setVisibility(View.INVISIBLE); • startPlaybackButton.setVisibility(View.VISIBLE); • stopPlaybackButton.setVisibility(View.INVISIBLE); • if (audioPlayer==null) • return; • audioPlayer.stop(); • audioPlayer.release(); • audioPlayer = null;
Volume Control • AudioManager • Add toggleButton to UI • Id = toggleVolume • In MediaPlayerFunActivity, • Add member variable • AudioManageraudioManager = null; • In onCreate, add • audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); • Make onClickListener for ToggleButton • In onCreate, add • ToggleButtontoggleVolumeButton = (ToggleButton)findViewById(R.id.toggleVolume); • toggleVolumeButton.setOnClickListener(new View.OnClickListener(){}); • In onClick, add • if (((ToggleButton) v).isChecked()) { • audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), AudioManager.FLAG_SHOW_UI); • } else { • audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, AudioManager.FLAG_SHOW_UI); • } • AudioManager can be used to • set the volume of the ringer, in call, vibrate • Determine if music is playing • get the volume • Determine if the speakerphone is on • Play sound effect (clicks etc)
Play music from a stream • In startPlaybackButtononClickListener, replace • audioPlayer.setDataSource(pathAndNameOfAudioFile); • With • Uri uri = Uri.parse("http://85.21.79.93:8040"); • audioPlayer.setDataSource(MPFActivity.this, uri); • Note that this url as found insider a .pls file, which can be found online. Browsers can decode pls files. But the AudioPlayer cannot.
Missing Topics • Audio Focus • What to do when there are multiple audio streams? • One should play and the other should be silent • The app that has audio focus should play • You check request audio focus before playing. And only play if you get it • Remote control • When the screen is locked, you might want to adjust the volume. Remote controls allow you to do this • Wake Lock • If you are streaming, the system should stay awake even when the screen is off. • Wake locks do this • We cover wake locks later
AudioRecord and AudioTrack • AudioTrack plays audio from a buffer • The audio must be decompressed • Make a new app, AudioTrackFun • Make some sounds and play them • two buttons • Go • stop • Member variables • AudioTrackaudioTrack = null; • intsampleRate = 8000;//22050; • intchannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO; • intaudioFormat = AudioFormat.ENCODING_PCM_16BIT; // must be this • static short[] audioData; // buffer for data • intbufferSize;
Set up AudioTrack • AudioTrack needs a buffer. When constructing an AudioTrack object, we must say how big this buffer should be. • Sometimes it makes sense to have a small buffer • In onCreate, add • bufferSize = 10*AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); • Make audioTrack object • In onCreate add • audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, 2*bufferSize, AudioTrack.MODE_STREAM); • note that the buffer size is in shorts (2 bytes) so we multiple by 2 • Make buffer • In onCreate add • audioData = new short[bufferSize]; • Must release audio resources • @Override • public void onDestroy() { • super.onDestroy(); • if (audioTrack!=null) • audioTrack.release();
Button to start and stop • Button startButton = (Button)findViewById(R.id.startButton); • startButton.setOnClickListener(new View.OnClickListener() {}); • In onClick, add • startAudio(); • Button stopButton = (Button)findViewById(R.id.stopButton); • stopButton.setOnClickListener(new View.OnClickListener() {}); • In onClick, add • stopAudio();
Fill audio buffer • We can fill the buffer with decompressed music, received audio (e.g., VoIP), or synthesized sound • Make member function and variables • float t = 0, dt = (float)(1.0/(float)sampleRate); • float pi = (float) 3.141592; • float w1=(float) 300*2*pi, w2=(float) 10*2*pi, a=(float)(100.0*2.0*pi); • public void fillAudioData(int length) { • for (inti=0; i<length; i++) { • audioData[i] = (short) (Short.MAX_VALUE*Math.cos((w1+a*Math.cos(w2*t))*t)); // fm synthesizer like the yamaha DX-7 • t += dt; • if (t>10) • t = 0; • } • }
Playing audio (approach 1) • void startAudio() { • fillAudioData(bufferSize); • audioTrack.write(audioData, 0, bufferSize); • fillAudioData(bufferSize/2); // get data ready • audioTrack.setNotificationMarkerPosition(bufferSize/2); • // setPositionNotificationPeriod is another possibility • audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {}); • In onMarkerReached, add • track.write(audioData, 0, bufferSize/2); • audioTrack.setNotificationMarkerPosition(bufferSize/2); // must reset everytime • fillAudioData(bufferSize/2); // get next data ready • audioTrack.play(); • }
stop • public void stopAudio() { • audioTrack.stop(); • } • Try it • Note that the UI is delayed because we are working in the UI thread
Use thread instead of notification • Drawbacks of notification • It is not possible to check how full the buffer is. • try to write a larger buffer and wait • For sure this will block and delay the UI thread • One can use notifications and threads. But a direct thread approach seems to work well • boolean stop = false; // new member variable • public void startAudio() { • fillAudioData(bufferSize); • audioTrack.write(audioData, 0, bufferSize); • fillAudioData(bufferSize/2); // get data ready • Thread thread = new Thread(new Runnable() { • @Override • public void run() { • stop = false; • while (!stop) { • audioTrack.write(audioData, 0, bufferSize/2); • fillAudioData(bufferSize/2); • } • Log.e("AudioFun","Thread has stopped"); • }}); • thread.start(); • audioTrack.play(); • }
stopAudio • Update to • public void stopAudio() { • audioTrack.stop(); • stop = true; • }
AudioRecord • We will record from mic and then play it right back to speaker • In your project, you could send the recording to another host. • Use MediaPlayerto record to a file • Add member variable • AudioRecordaudioRecord = null; • intamountOfDataReady; • In onCreate, just after bufferSize = …, add • bufferSize = Math.max(bufferSize,10*AudioRecord.getMinBufferSize(sampleRate, channelConfig , audioFormat)); • audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, 2*bufferSize ); • New version of startAudio • public void startAudio() { • Thread thread = new Thread(new Runnable() { • @Override • public void run() { • stop = false; • while (!stop) { • amountOfDataReady = audioRecord.read(audioData, 0, bufferSize/2); • audioTrack.write(audioData, 0,amountOfDataReady); • } • Log.e("AudioFun","Thread has stopped"); • }}); • thread.start(); • audioTrack.play(); • audioRecord.startRecording(); • }
Buffer size • Try it. • As expected, there is feedback loop. • The delay is quite large. This would not work so well for VoIP • How small can we make the buffer? It depends on how much other things are going on • More member variables • long currentTime, lastTime; • boolean starting = true; • intbufferToUse;
d = max(a*d, sampledDelay); • Buffer = 1.5d/sampleRate