Source code for codex.codec

"""Audio driver functions for CodeX. This provides a Python interface to the
   CodeX ES8311 CODEC (enCOder-DECoder) chip

   See :py:mod:`soundlib` module for a higher-level audio interface.

"""
import board
from codex_drivers import es8311, i2sin
from codex_drivers.funcgen import FuncSample
import busio

import audiomp3
import audiobusio
import audiocore
import board
import array
import time
import math

ES8311_ADDR = 0x18
SAMPLE_FREQ = 16000

# Some ES8311 registers
ES8311_DAC = 0x37

# Equalizer coefficients:
# The actual values are NOT documented by chip vendor Everest,
# but are derived from their graphical configuration tool.
# We have confirmed they really are 30-bit values (not 32-bit)
# There is some evidence that the 30-bits are 6 groups of 5 bits,
# possibly in a sign-bit + 4 data bits format, and some may be bit-flags
ES8311_DACEQ_B0 = 0x38 # First of 4 registers holding a 30-bit EQ value
ES8311_DACEQ_B1 = 0x3C # First of 4 registers holding a 30-bit EQ value
ES8311_DACEQ_A1 = 0x40 # First of 4 registers holding a 30-bit EQ value
# Some bits within those registers
ES8311_DAC_EQBYPASS = 0x08 # DAC Equalizer is bypassed when this bit is set

# FuncSample constants
FT_SINE = 0      #:
FT_SQUARE = 1    #:
FT_SAWTOOTH = 2  #:
FT_TRIANGLE = 3  #:
FT_RANDOM = 4    #:
FT_ORGAN = 5     #:
FT_SYNTH = 6     #:
MOD_FREQUENCY = 0 #:
MOD_AMPLITUDE = 1 #:

[docs]class Codec: """Access to the audio system of the CodeX (play and record)""" def __init__(self, io_expander, power, sample_freq=SAMPLE_FREQ): """Initialize ES8311 codec, defaults to DAC mode""" self._io_expander = io_expander self._power = power self._i2c = board.I2C() self._i2c.try_lock() es8311.init(self._i2c, sample_freq) self._i2c.unlock() self._i2s = audiobusio.I2SOut(bit_clock=board.IO18, word_select=board.IO17, data=board.IO12) self.is_initialized = False self.func_sample = None self._audio_mixer_inst = None if hasattr(self, '__doc__'): return # Autodoc stop here # Speaker protection via ES8311 CODEC Equalizer # Note that the level only applies when the equalizer is ON # Basically we keep it CONFIGURED all the time, and just turn it on/off as-needed self.set_bass_level(-20) # Range is 0 to -26. Value of -20 chosen based on single-note sound tests if self.is_phones(): self.enable_dac_equalizer(False) # We want all of the audio else: self.enable_dac_equalizer(True) # We want to filter out the low-end
[docs] def initialize(self): """Enable the audio system and the speaker. You can perform these steps manually for fine tuning, but this is a quick way to get audio going. """ if self.is_initialized: return self.is_initialized = True if es8311.get_volume() == 0: self.set_volume(65) self.enable_amp(True) self._power.enable_periph_vcc(True) self._power.enable_batt_meas(True) # Mic on if not self.func_sample: self.make_pitch_func()
[docs] def make_pitch_func(self, func_type=FT_TRIANGLE): """Create and set sample function for basic 'pitch()' tone generation. Default to triangle waveform, with lovely harmonics so it's louder on CodeX little speaker than a pure sine wave. """ self.func_sample = FuncSample(func_type, SAMPLE_FREQ) return self.func_sample
[docs] def pitch(self, freq, dur): """Play a tone on the speaker, with the specified frequency. Args: freq (int): Frequency in Hertz dur (float): Duration in seconds Returns: FuncSample: a sample you can optionally control with env_attack(), env_release(), etc. """ self.initialize() if not self._i2s.playing: self.func_sample.env_release() self._i2s.play(self.func_sample, loop=True) self.func_sample.set_frequency(freq) if dur: self.func_sample.env_attack() time.sleep(dur) self.func_sample.env_release() return self.func_sample
[docs] def off(self): """Stop ALL output to speaker and headphones. Note: For "beeping" on and off it will sound better to use the FuncSample returned from the 'pitch()' function. Or see 'soundlib' module for more advanced features. """ self._i2s.stop()
[docs] def enable_amp(self, do_enable): """Enable the audio amplifier for the onboard speaker. Allows you to turn OFF the speaker if desired, with headphones jack still active. Args: do_enable (bool) : True to enable or False to disable """ # Audio amp usage implies on-board speaker usage, # which implies we want the hardware bass reduction self.enable_dac_equalizer(do_enable) bits = self._io_expander.get_outputs() if do_enable: bits |= 1 else: bits = bits & 0b_11111110 self._io_expander.set_outputs(bits)
[docs] def is_amp_enabled(self): """Return the status of the audio amplifier. Returns: bool : True if the amplifier is enabled """ return bool(self._io_expander.get_outputs() & 1)
[docs] def set_volume(self, volume): """Set the output audio volume Args: volume (int) : volume from 0 to 100 percent """ es8311.set_volume(volume)
[docs] def get_volume(self): """Get the output audio volume Returns: int : volume from 0 to 100 percent """ return es8311.get_volume()
[docs] def is_phones(self): """Are headphones plugged in? Returns: bool : True if headphones are detected """ return not self._io_expander.get_input(6)
[docs] def get_chip_id(self): """Get the chip ID of ES8311""" self._i2c.try_lock() ret = es8311.chip_id() self._i2c.unlock() return ret
def _ping(self): """Read es8311 chip ID @ reg 0xFD --> 0x83 This is an example of using CircuitPython I2C to directly communicate with ES8311. """ self._i2c.try_lock() self._i2c.writeto(ES8311_ADDR, bytes([0xFD])) result = bytearray(1) self._i2c.readfrom_into(ES8311_ADDR, result) print(f'es8311 ID: {result}') self._i2c.unlock()
[docs] def mic_gain(self, pga=0x10, adc=0xbf): """Set the microphone gain Args: pga (int) : PGA gain adc (int) : ADC volume """ es8311.write_reg(0x14, pga) # PGA gain (0x10=0dB, 0x1A=30dB) es8311.write_reg(0x17, adc) # ADC volume (0xBF=0dB)
# Reasonable gain values for CodeX mic
[docs] def record(self, duration=2.0, sample_freq=SAMPLE_FREQ, pga_gain=0x12, adc_gain=0xd0): """Record from ADC (microphone), returning a 16-bit array of samples. Note: All samples must fit in a single RAM buffer, so be careful with RAM management. (ESP32S2 WROVER module has 2M bytes of RAM available total) """ self.initialize() length = int(sample_freq * duration) buf = array.array("h", [0] * length) #print("Init for recording.") self.mic_gain(pga_gain, adc_gain) # More research is required into how to get I2SIN and I2SOUT to coexist # Today's work-around is to take down the I2SOUT during recording, # then put it back afterwards self._i2s.stop() self._i2s.deinit() # Enable ESP32 I2S input (start generating BCLK, filling DMA buffers) result = i2sin.init(sample_freq) if result == 0: # Setup the codec es8311.set_adc_dac(True) # ADC mode #print("Start recording...") # Read samples from DMA buffers until timeout or empty i2sin.readinto(buf, int(duration * 1000)) # convert secs to milliseconds #print("Recording done.") es8311.set_adc_dac(False) # return to DAC mode i2sin.deinit() # Stop clocking I2S data self._i2s = audiobusio.I2SOut(bit_clock=board.IO18, word_select=board.IO17, data=board.IO12) # Restart background audio mixer sample stream if set if self._audio_mixer_inst: self._i2s.play(self._audio_mixer_inst) return buf
def set_audio_mixer(self, mixer): self._audio_mixer_inst = mixer self._i2s.play(mixer)
[docs] def playbuf(self, buf, sample_freq=SAMPLE_FREQ, loop=False): """Play 'buf' via I2SOut. Works with array returned from record(), or any 16-bit array you create using array.array("h") and fill with samples. """ self.initialize() duration = len(buf) / sample_freq sample_buf = audiocore.RawSample(buf, channel_count=1, sample_rate=sample_freq) self._i2s.play(sample_buf, loop=True) if not loop: time.sleep(duration) self._i2s.stop()
[docs] def wav(self, filename): """Play given WAV file Args: filename (str) : Path and name of file (.wav extension is optional) """ if not filename.endswith('.wav'): filename += '.wav' with open(filename, "rb") as sf: w = audiocore.WaveFile(sf) self.initialize() self._i2s.play(w, loop=False) while self._i2s.playing: time.sleep(0.1)
[docs] def mp3(self, filename): """Play given MP3 file. Args: filename (str) : Path and name of file (.mp3 extension is optional) """ if not filename.endswith('.mp3'): filename += '.mp3' with open(filename, "rb") as sf: w = audiomp3.MP3Decoder(sf) self.initialize() self._i2s.play(w, loop=False) while self._i2s.playing: time.sleep(0.1)
[docs] def enable_dac_equalizer(self, do_enable): """Enable the output-side (DAC) equalizer in the ES8311 CODEC. Args: do_enable (bool) : True to enable or False to disable """ register_value = es8311.read_reg(ES8311_DAC) # It's a BYPASS bit in the register, not an ENABLE bit # That is why the logic below might seem backwards... if do_enable: register_value &= ~ES8311_DAC_EQBYPASS else: register_value |= ES8311_DAC_EQBYPASS es8311.write_reg(ES8311_DAC, register_value)
def _set_30_bits(self, starting_register, value): """Internal helper function used to set 30-bit values that span 4 consecutive registers""" byte4 = value & 0xFF value >>= 8 byte3 = value & 0xFF value >>= 8 byte2 = value & 0xFF value >>= 8 byte1 = value & 0x3F es8311.write_reg(starting_register, byte1) es8311.write_reg(starting_register+1, byte2) es8311.write_reg(starting_register+2, byte3) es8311.write_reg(starting_register+3, byte4) def _set_DACEQ_B0(self, value): """Internal helper function to set DAC Equalizer coefficient B0""" self._set_30_bits(ES8311_DACEQ_B0, value) def _set_DACEQ_B1(self, value): """Internal helper function to set DAC Equalizer coefficient B1""" self._set_30_bits(ES8311_DACEQ_B1, value) def _set_DACEQ_A1(self, value): """Internal helper function to set DAC Equalizer coefficient A1""" self._set_30_bits(ES8311_DACEQ_A1, value) def _set_DAC_EQ(self, B0, B1, A1): """Internal helper function to set the DAC Equalizer coefficients B0, B1, and A1""" self._set_DACEQ_B0(B0) self._set_DACEQ_B1(B1) self._set_DACEQ_A1(A1) # The point of this routine is to protect our little speaker # You will notice that we only support LOWERING the bass level
[docs] def set_bass_level(self, gain): """Adjust the low-frequency (bass) audio gain for the audio output. Args: gain (integer) : 0 to -26 dB """ # These magic filter coefficients came from a proprietary EVEREST # tool, and they refused to provide any technical details. # This particular set has a lot of redundancy, but in general this # is not the case with the EVEREST tool, and so I have kept the code generic FILTER_COEFFICIENTS = [ (0xf7fdff, 0x203a5fdF, 0x0132b7cf), # bass FS=16000 FC=0.25 G=0 dB (0xf7fdff, 0x203a5fdF, 0x0133217d), # bass FS=16000 FC=0.25 G=-1 dB (0xf7fdff, 0x203a5fdF, 0x0133e97c), # bass FS=16000 FC=0.25 G=-2 dB (0xf7fdff, 0x203a5fdF, 0x0121dd6f), # bass FS=16000 FC=0.25 G=-3 dB (0xf7fdff, 0x203a5fdF, 0x0121d4d7), # bass FS=16000 FC=0.25 G=-4 dB (0xf7fdff, 0x203a5fdF, 0x0122190a), # bass FS=16000 FC=0.25 G=-5 dB (0xf7fdff, 0x203a5fdF, 0x01222d9d), # bass FS=16000 FC=0.25 G=-6 dB (0xf7fdff, 0x203a5fdF, 0x01229d4c), # bass FS=16000 FC=0.25 G=-7 dB (0xf7fdff, 0x203a5fdF, 0x012369bf), # bass FS=16000 FC=0.25 G=-8 dB (0xf7fdff, 0x203a5fdF, 0x022b276c), # bass FS=16000 FC=0.25 G=-9 dB (0xf7fdff, 0x203a5fdF, 0x022a196c), # bass FS=16000 FC=0.25 G=-10 dB (0xf7fdff, 0x203a5fdF, 0x02329d1b), # bass FS=16000 FC=0.25 G=-11 dB (0xf7fdff, 0x203a5fdF, 0x0234b3ae), # bass FS=16000 FC=0.25 G=-12 dB (0xf7fdff, 0x203a5fdF, 0x0243236d), # bass FS=16000 FC=0.25 G=-13 dB (0xf7fdff, 0x203a5fdF, 0x025d31bf), # bass FS=16000 FC=0.25 G=-14 dB (0xf7fdff, 0x203a5fdF, 0x03542d9e), # bass FS=16000 FC=0.25 G=-15 dB (0xf7fdff, 0x203a5fdF, 0x0332a55d), # bass FS=16000 FC=0.25 G=-16 dB (0xf7fdff, 0x203a5fdF, 0x0442dd4e), # bass FS=16000 FC=0.25 G=-17 dB (0xf7fdff, 0x203a5fdF, 0x047cb3cf), # bass FS=16000 FC=0.25 G=-18 dB (0xf7fdff, 0x203a5fdF, 0x0653a98f), # bass FS=16000 FC=0.25 G=-19 dB (0xf7fdff, 0x203a5fdF, 0x08af3dff), # bass FS=16000 FC=0.25 G=-20 dB (0xf7fdff, 0x203a5fdF, 0x286ba1ff), # bass FS=16000 FC=0.25 G=-21 dB (0xf7fdff, 0x203a5fdF, 0x27435d5c), # bass FS=16000 FC=0.25 G=-22 dB (0xf7fdff, 0x203a5fdF, 0x254be77d), # bass FS=16000 FC=0.25 G=-23 dB (0xf7fdff, 0x203a5fdF, 0x225b37cf), # bass FS=16000 FC=0.25 G=-24 dB (0xf7fdff, 0x203a5fdF, 0x233adf5b), # bass FS=16000 FC=0.25 G=-25 dB (0xf7fdff, 0x203a5fdF, 0x20299f5b) # bass FS=16000 FC=0.25 G=-26 dB ] if gain > 0: return if gain < -26: return coefficients = FILTER_COEFFICIENTS[-gain] B0 = coefficients[0] B1 = coefficients[1] A1 = coefficients[2] self._set_DAC_EQ(B0, B1, A1)