Audio Sampler Example

This example shows how to record a sound using the Android media APIs with parameters selected from a user interface.

A screenshot of the application.

We import the classes and modules needed by our application. The most relevant classes are those from the android.media module.

from java.io import File, FileOutputStream
from java.lang import Math, Runnable, Thread
from java.nio import ByteBuffer
from java.text import SimpleDateFormat
from java.util import Date
from android.media import AudioFormat, AudioRecord, MediaRecorder
from android.os import Environment
from android.view import View, ViewGroup
from android.widget import AdapterView, Button, GridLayout, LinearLayout, \
                           Spinner, TextView
from serpentine.activities import Activity
from serpentine.adapters import StringListAdapter

The AudioSamplerActivity class is derived from the standard Activity class and represents the application. Android will create an instance of this class when the user runs it.

class AudioSamplerActivity(Activity):

    __interfaces__ = [AdapterView.OnItemSelectedListener, Runnable,
                      View.OnClickListener]

The class declares that it implements the AdapterView.OnItemSelectedListener, View.OnClickListener and Runnable interfaces.

The initialisation method simply calls the corresponding method in the base class.

    def __init__(self):
    
        Activity.__init__(self)
        
        self.recording = False
        self.encodings = [AudioFormat.ENCODING_PCM_8BIT,
                          AudioFormat.ENCODING_PCM_16BIT]
        self.sampleRates = [9600, 22050, 44100, 48000]
        self.channels = [AudioFormat.CHANNEL_IN_MONO,
                         AudioFormat.CHANNEL_IN_STEREO]
        
        self.encoding = AudioFormat.ENCODING_PCM_8BIT
        self.sampleRate = 9600
        self.channel = AudioFormat.CHANNEL_IN_MONO

The onCreate method calls the corresponding method in the base class to help set up the activity, and we set up the user interface.

    def onCreate(self, bundle):
    
        Activity.onCreate(self, bundle)
        
        channelLabel = TextView(self)
        channelLabel.setText("Input channels:")
        self.channelSpinner = Spinner(self)
        adapter = StringListAdapter(["Mono", "Stereo"])
        self.channelSpinner.setAdapter(adapter)
        self.channelSpinner.setOnItemSelectedListener(self)
        
        sampleRateLabel = TextView(self)
        sampleRateLabel.setText("Sample rate:")
        self.sampleRateSpinner = Spinner(self)
        l = []
        for i in self.sampleRates:
            l.add(str(i))
        adapter = StringListAdapter(l)
        self.sampleRateSpinner.setAdapter(adapter)
        self.sampleRateSpinner.setOnItemSelectedListener(self)
        
        encodingLabel = TextView(self)
        encodingLabel.setText("Output encoding:")
        self.encodingSpinner = Spinner(self)
        adapter = StringListAdapter(["8-bit", "16-bit"])
        self.encodingSpinner.setAdapter(adapter)
        self.encodingSpinner.setOnItemSelectedListener(self)
        
        grid = GridLayout(self)
        grid.setColumnCount(2)
        lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT)
        lp.bottomMargin = 12
        grid.setLayoutParams(lp)
        
        grid.addView(channelLabel)
        grid.addView(self.channelSpinner)
        grid.addView(sampleRateLabel)
        grid.addView(self.sampleRateSpinner)
        grid.addView(encodingLabel)
        grid.addView(self.encodingSpinner)

We create a button with a "Play sound" label and register the activity as its listener for click callbacks.

        self.button = Button(self)
        self.button.setText("Start recording")
        self.button.setOnClickListener(self)
        self.button.setEnabled(False)

We also create a text view to show the location of the last file written.

        self.textView = TextView(self)

The button and text view are placed in a vertical layout which is used as the main content in the activity.

        layout = LinearLayout(self)
        layout.setOrientation(layout.VERTICAL)
        layout.addView(grid)
        layout.addView(self.button)
        layout.addView(self.textView)
        
        self.setContentView(layout)

The next two methods are the implementation of the AdapterView.OnItemSelectedListener interface.

When an item in one of the spinners is selected, this method is called with the parent view (the spinner in this case), the view displaying the item data, the position of the item in the spinner and the item's ID. We use the parent and position to extract data from the relevant list that we defined earlier before calling the tryFormat method to see if the parameters are valid.

    def onItemSelected(self, parent, view, position, id):
    
        if parent == self.channelSpinner:
            self.channel = self.channels[position]
        elif parent == self.sampleRateSpinner:
            self.sampleRate = self.sampleRates[position]
        elif parent == self.encodingSpinner:
            self.encoding = self.encodings[position]
        
        self.tryFormat()

If no item is selected then do nothing, keeping the existing parameters.

    def onNothingSelected(self, parent):
        pass

The following method tries the current set of parameters and sets up an AudioRecord instance if successful.

    def tryFormat(self):
    
        bufferSize = AudioRecord.getMinBufferSize(self.sampleRate,
            self.channel, self.encoding)
        
        if bufferSize == AudioRecord.ERROR_BAD_VALUE:
            self.button.setEnabled(False)
            return

We use the existing parameters for the audio data we want to record. If we cannot use these parameters, we simply return early and disable the record button.

We create a byte array of the required size, allocating at least enough data for one second of mono, 8-bit audio data - less if more channels or 16-bit audio is being recorded.

        bufferSize = Math.max(bufferSize, self.sampleRate)
        
        self.audioBuffer = array(byte, bufferSize)

We create an AudioRecord instance, passing information about the sample data to ensure that it will be played correctly.

        self.recorder = AudioRecord(MediaRecorder.AudioSource.MIC,
            self.sampleRate, self.channel, self.encoding, bufferSize)
        
        if self.recorder.getState() == AudioRecord.STATE_INITIALIZED:
            self.button.setEnabled(True)

The onClick method is called whenever the button defined above is clicked. If the activity was registered as a listener with other buttons then we would distinguish between them using the View object passed to this method.

    def onClick(self, view):
    
        if not self.recording:

If we are not recording when the button is pressed then we create a file to write to, open an output stream to direct data to that file, and we start recording.

            self.button.setText("Stop recording")
            self.file = self.createFile()
            self.stream = FileOutputStream(self.file)
            self.recorder.startRecording()
            self.recording = True

We also create a thread, passing the instance of this class to it as a Runnable for it to execute, and start it.

            self.thread = Thread(self)
            self.thread.start()
        
        else:

Otherwise, we stop the recorder and interrupt the thread in order to stop processing audio data. We reset the button and print the name of the file we created to the TextView.

            self.button.setText("Start recording")
            self.recorder.stop()
            self.recording = False
            self.thread.interrupt()
            self.textView.setText("Written " + self.file.getAbsolutePath())

As a Runnable, this class provides a run method that can be executed in another context, and we use a thread to ensure that this method is run in the background while the application continues to respond to the user in the foreground. The method reads data from the audio buffer and writes it to an output stream as long as the recording flag is set and the thread is running.

    def run(self):
    
        while self.recording:
            size = self.recorder.read(self.audioBuffer, 0, len(self.audioBuffer))
            if size > 0:
                self.stream.write(self.audioBuffer[0:size])

The final method is used to conveniently create a file in the device's external storage area, returning a File object if successful or None if not.

    @args(File, [])
    def createFile(self):
    
        if Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED:
            return None

If no external storage is mounted then return None immediately.

We obtain the directory on the external storage device that is used to store music.

        storageDir = Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_MUSIC)

If possible, we create a subdirectory for this example, returning None to indicate failure.

        subdir = File(storageDir, "AudioSampler")
        if not subdir.exists():
            if not subdir.mkdirs():
                return None

Finally, we use the SimpleDateFormat class to create a file name based on the current date, then use this to create a File object which we return.

        dateString = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        return File(subdir, dateString + ".raw")

Files