diff --git a/examples/modules/audio_amp/single_tone.py b/examples/modules/audio_amp/single_tone.py deleted file mode 100644 index 75be637..0000000 --- a/examples/modules/audio_amp/single_tone.py +++ /dev/null @@ -1,95 +0,0 @@ -import math -import struct -from machine import I2S -from pimoroni_yukon import Yukon -from pimoroni_yukon import SLOT6 as SLOT -from pimoroni_yukon.modules import AudioAmpModule - -""" -How to play a pure sinewave tone out of an Audio Amp Module connected to Slot1. -""" - -# Constants -I2S_ID = 0 -BUFFER_LENGTH_IN_BYTES = 20000 -TONE_FREQUENCY_IN_HZ = 1000 -SAMPLE_SIZE_IN_BITS = 16 -FORMAT = I2S.MONO # only MONO supported in this example -SAMPLE_RATE_IN_HZ = 44_100 -SLEEP = 1.0 # The time to sleep between each reading - -# Variables -yukon = Yukon() # Create a new Yukon object -amp = AudioAmpModule() # Create an AudioAmpModule object -audio_out = None # Stores the I2S audio output object created later - - -# Callback function to queue up the next section of audio -def i2s_callback(arg): - global audio_out - global samples - audio_out.write(samples) - - -# Using this for testing: https://github.com/miketeachman/micropython-i2s-examples/tree/master -def make_tone(rate, bits, frequency): - # create a buffer containing the pure tone samples - samples_per_cycle = rate // frequency - sample_size_in_bytes = bits // 8 - samples = bytearray(4 * samples_per_cycle * sample_size_in_bytes) - range = pow(2, bits) // 2 - - if bits == 16: - format = " 1.0: - raise ValueError("volume out of range. Expected 0.0 to 1.0") - - # create a buffer containing the pure tone samples - samples_per_cycle = rate // frequency - sample_size_in_bytes = bits // 8 - samples = bytearray(complete_waves * samples_per_cycle * sample_size_in_bytes) - range = pow(2, bits) // 2 - - format = " 20_000: + raise ValueError("frequency out of range. Expected between 20Hz and 20KHz") + + if amplitude < 0.0 or amplitude > 1.0: + raise ValueError("amplitude out of range. Expected 0.0 to 1.0") + + # Create a buffer containing the pure tone samples + samples_per_cycle = self.TONE_SAMPLE_RATE // frequency + sample_size_in_bytes = self.TONE_BITS_PER_SAMPLE // 8 + samples = bytearray(self.TONE_FULL_WAVES * samples_per_cycle * sample_size_in_bytes) + range = pow(2, self.TONE_BITS_PER_SAMPLE) // 2 + + format = " 0: - self.nflush -= 1 - _ = self.audio_out.write(self.silence_samples) + if self.__queued_samples is not None: + self.__tone_samples = self.__queued_samples + self.__queued_samples = None + self.__audio_out.write(self.__tone_samples) + + ### PAUSE or STOP ### + elif self.__state == WavPlayer.PAUSE or self.__state == WavPlayer.STOP: + self.__audio_out.write(self.__silence_samples) # Play silence + + ### FLUSH ### + elif self.__state == WavPlayer.FLUSH: + # Flush is used to allow the residual audio samples in the internal buffer to be written + # to the I2S peripheral. This step avoids part of the sound file from being cut off + if self.__flush_count > 0: + self.__flush_count -= 1 else: - self.wav.close() - self.audio_out.deinit() - self.state = WavPlayer.STOP - elif self.state == WavPlayer.STOP: + self.__state = WavPlayer.STOP # Enter the stop state on the next callback + self.__audio_out.write(self.__silence_samples) # Play silence + + ### NONE ### + elif self.__state == WavPlayer.NONE: pass - else: - raise SystemError("Internal error: unexpected state") - self.state == WavPlayer.STOP - def parse(self, wav_file): + @staticmethod + def __parse_wav(wav_file): chunk_ID = wav_file.read(4) if chunk_ID != b"RIFF": raise ValueError("WAV chunk ID invalid") - chunk_size = wav_file.read(4) + _ = wav_file.read(4) # chunk_size format = wav_file.read(4) if format != b"WAVE": raise ValueError("WAV format invalid") sub_chunk1_ID = wav_file.read(4) if sub_chunk1_ID != b"fmt ": raise ValueError("WAV sub chunk 1 ID invalid") - sub_chunk1_size = wav_file.read(4) - audio_format = struct.unpack("WAV converters add @@ -232,74 +368,13 @@ def parse(self, wav_file): if offset == -1: raise ValueError("WAV sub chunk 2 ID not found") - self.first_sample_offset = 44 + offset - - def queue(self, wav_file, loop=False): - if os.listdir(self.root).count(wav_file) == 0: - raise ValueError("%s: not found" % wav_file) - if self.state == WavPlayer.PLAY: - raise ValueError("already playing a WAV file") - elif self.state == WavPlayer.PAUSE: - raise ValueError("paused while playing a WAV file") - else: - self.wav = open(self.root + wav_file, "rb") - self.loop = loop - self.parse(self.wav) - - self.audio_out = I2S( - self.id, - sck=self.sck_pin, - ws=self.ws_pin, - sd=self.sd_pin, - mode=I2S.TX, - bits=self.bits_per_sample, - format=self.format, - rate=self.sample_rate, - ibuf=self.ibuf, - ) - - # advance to first byte of Data section in WAV file - _ = self.wav.seek(self.first_sample_offset) - self.audio_out.irq(self.i2s_callback) - self.nflush = self.ibuf // self.sbuf + 1 - self.state = WavPlayer.PAUSE - _ = self.audio_out.write(self.silence_samples) - - def resume(self): - if self.state != WavPlayer.PAUSE: - raise ValueError("can only resume when WAV file is paused") - else: - self.state = WavPlayer.RESUME - - def pause(self): - if self.state == WavPlayer.PAUSE: - pass - elif self.state != WavPlayer.PLAY: - raise ValueError("can only pause when WAV file is playing") - - self.state = WavPlayer.PAUSE - - def stop(self): - self.state = WavPlayer.FLUSH - - def isplaying(self): - if self.state != WavPlayer.STOP: - return True - else: - return False - - def terminate(self): - if self.audio_out is not None: - self.audio_out.deinit() - + return (format, sample_rate, bits_per_sample, 44 + offset) class AudioAmpModule(YukonModule): NAME = "Audio Amp" AMP_I2C_ADDRESS = 0x38 TEMPERATURE_THRESHOLD = 50.0 - I2S_ID = 0 - BUFFER_LENGTH_IN_BYTES = 20000 # | ADC1 | ADC2 | SLOW1 | SLOW2 | SLOW3 | Module | Condition (if any) | # |-------|-------|-------|-------|-------|----------------------|-----------------------------| @@ -308,9 +383,10 @@ class AudioAmpModule(YukonModule): def is_module(adc1_level, adc2_level, slow1, slow2, slow3): return adc1_level == ADC_FLOAT and slow1 is IO_LOW and slow2 is IO_HIGH and slow3 is IO_HIGH - def __init__(self): - self.player = None + def __init__(self, i2s_id): super().__init__() + self.__i2s_id = i2s_id + self.player = None def initialise(self, slot, adc1_func, adc2_func): # Create the enable pin object @@ -324,15 +400,15 @@ def initialise(self, slot, adc1_func, adc2_func): self.__sda_bit = 1 << tca.get_number(slot.SLOW2) self.__scl_bit = 1 << tca.get_number(slot.SLOW3) - self.I2S_DATA = slot.FAST1 - self.I2S_CLK = slot.FAST2 - self.I2S_FS = slot.FAST3 - - self.player = WavPlayer(id=self.I2S_ID, - sck_pin=self.I2S_CLK, - ws_pin=self.I2S_FS, - sd_pin=self.I2S_DATA, - ibuf=self.BUFFER_LENGTH_IN_BYTES) + self.__i2s_data = slot.FAST1 + self.__i2s_clk = slot.FAST2 + self.__i2s_fs = slot.FAST3 + + # Create the Player object, with the needed I2S pins + self.player = WavPlayer(id=self.__i2s_id, + sck_pin=self.__i2s_clk, + ws_pin=self.__i2s_fs, + sd_pin=self.__i2s_data) # Pass the slot and adc functions up to the parent now that module specific initialisation has finished super().initialise(slot, adc1_func, adc2_func) @@ -341,92 +417,86 @@ def reset(self): self.__slow_sda.init(Pin.OUT, value=True) self.__slow_scl.init(Pin.OUT, value=True) self.__amp_en.init(Pin.OUT, value=False) - + + # Stop and clean up the Player object if it exists if self.player is not None: - self.player.terminate() + self.player.__stop_i2s() def enable(self): - self.__amp_en.value(True) - - # Pre-Reset Configuration - self.write_i2c_reg(PAGE, 0x01) # Page 1 - self.write_i2c_reg(0x37, 0x3a) # Bypass - - self.write_i2c_reg(PAGE, 0xFD) # Page FD - self.write_i2c_reg(0x0D, 0x0D) # Access page - self.write_i2c_reg(0x06, 0xC1) # Set Dmin - - self.write_i2c_reg(PAGE, 0x01) # Page 1 - self.write_i2c_reg(0x19, 0xC0) # Force modulation - self.write_i2c_reg(PAGE, 0xFD) # Page FD - self.write_i2c_reg(0x0D, 0x0D) # Access page - self.write_i2c_reg(0x06, 0xD5) # Set Dmin - - # Software Reset - self.write_i2c_reg(PAGE, 0x00) # Page 0 - self.write_i2c_reg(0x7F, 0x00) # Book 0 - self.write_i2c_reg(0x01, 0x01) # Software Reset - - # Post-Reset Configuration - self.write_i2c_reg(PAGE, 0x01) # Page 1 - self.write_i2c_reg(0x37, 0x3a) # Bypass - - self.write_i2c_reg(PAGE, 0xFD) # Page FD - self.write_i2c_reg(0x0D, 0x0D) # Access page - self.write_i2c_reg(0x06, 0xC1) # Set Dmin - self.write_i2c_reg(0x06, 0xD5) # Set Dmin - - # Initial Device Configuration - PWR_MODE0 - self.write_i2c_reg(PAGE, 0x00) # Page 0 - self.write_i2c_reg(0x0E, 0x44) # TDM tx vsns transmit enable with slot 4 - self.write_i2c_reg(0x0F, 0x40) # TDM tx isns transmit enable with slot 0 - - self.write_i2c_reg(PAGE, 0x01) # Page 1 - self.write_i2c_reg(0x21, 0x00) # Disable Comparator Hysterisis - self.write_i2c_reg(0x17, 0xC8) # SARBurstMask=0 - self.write_i2c_reg(0x19, 0x00) # LSR Mode - self.write_i2c_reg(0x35, 0x74) # Noise minimized - - self.write_i2c_reg(PAGE, 0xFD) # Page FD - self.write_i2c_reg(0x0D, 0x0D) # Access page - self.write_i2c_reg(0x3E, 0x4A) # Optimal Dmin - self.write_i2c_reg(0x0D, 0x00) # Remove access - - self.write_i2c_reg(PAGE, 0x00) # Page 0 - self.write_i2c_reg(CHNL_0, 0xA8) # PWR_MODE0 selected - self.write_i2c_reg(PVDD_UVLO, 0x00) # PVDD UVLO set to 2.76V - # My addition - self.write_i2c_reg(DC_BLK0, 0xA1) # VBAT1S_MODE set to internally generated - self.write_i2c_reg(DVC, 0x68) # Go to a low default - self.write_i2c_reg(INT_CLK_CFG, 0x99 + 0b0100000) # CLK_PWR_UD_EN abled, with long time. This causes output to stay active without mute. - - self.write_i2c_reg(INT_MASK0, 0xFF) - self.write_i2c_reg(INT_MASK1, 0xFF) - self.write_i2c_reg(INT_MASK2, 0xFF) - self.write_i2c_reg(INT_MASK3, 0xFF) - self.write_i2c_reg(INT_MASK4, 0xFF) - - self.write_i2c_reg(MODE_CTRL, 0x80) # Play audio, power up with playback, IV enabled - # A second play command is required for some reason, to take it out of software shutdown - # Temp commented out self.write_i2c_reg(MODE_CTRL, 0x80) # Play audio, power up with playback, IV enabled + if not self.is_enabled(): + self.__amp_en.value(True) + + # Pre-Reset Configuration + self.write_i2c_reg(PAGE, 0x01) # Page 1 + self.write_i2c_reg(0x37, 0x3a) # Bypass + + self.write_i2c_reg(PAGE, 0xFD) # Page FD + self.write_i2c_reg(0x0D, 0x0D) # Access page + self.write_i2c_reg(0x06, 0xC1) # Set Dmin + + self.write_i2c_reg(PAGE, 0x01) # Page 1 + self.write_i2c_reg(0x19, 0xC0) # Force modulation + self.write_i2c_reg(PAGE, 0xFD) # Page FD + self.write_i2c_reg(0x0D, 0x0D) # Access page + self.write_i2c_reg(0x06, 0xD5) # Set Dmin + + # Software Reset + self.write_i2c_reg(PAGE, 0x00) # Page 0 + self.write_i2c_reg(0x7F, 0x00) # Book 0 + self.write_i2c_reg(0x01, 0x01) # Software Reset + + # Post-Reset Configuration + self.write_i2c_reg(PAGE, 0x01) # Page 1 + self.write_i2c_reg(0x37, 0x3a) # Bypass + + self.write_i2c_reg(PAGE, 0xFD) # Page FD + self.write_i2c_reg(0x0D, 0x0D) # Access page + self.write_i2c_reg(0x06, 0xC1) # Set Dmin + self.write_i2c_reg(0x06, 0xD5) # Set Dmin + + # Initial Device Configuration - PWR_MODE0 + self.write_i2c_reg(PAGE, 0x00) # Page 0 + self.write_i2c_reg(0x0E, 0x44) # TDM tx vsns transmit enable with slot 4 + self.write_i2c_reg(0x0F, 0x40) # TDM tx isns transmit enable with slot 0 + + self.write_i2c_reg(PAGE, 0x01) # Page 1 + self.write_i2c_reg(0x21, 0x00) # Disable Comparator Hysterisis + self.write_i2c_reg(0x17, 0xC8) # SARBurstMask=0 + self.write_i2c_reg(0x19, 0x00) # LSR Mode + self.write_i2c_reg(0x35, 0x74) # Noise minimized + + self.write_i2c_reg(PAGE, 0xFD) # Page FD + self.write_i2c_reg(0x0D, 0x0D) # Access page + self.write_i2c_reg(0x3E, 0x4A) # Optimal Dmin + self.write_i2c_reg(0x0D, 0x00) # Remove access + + self.write_i2c_reg(PAGE, 0x00) # Page 0 + self.write_i2c_reg(CHNL_0, 0xA8) # PWR_MODE0 selected + self.write_i2c_reg(PVDD_UVLO, 0x00) # PVDD UVLO set to 2.76V + # My addition + self.write_i2c_reg(DC_BLK0, 0xA1) # VBAT1S_MODE set to internally generated + self.write_i2c_reg(DVC, 0x68) # Go to a low default + self.write_i2c_reg(INT_CLK_CFG, 0x99 + 0b0100000) # CLK_PWR_UD_EN abled, with long time. This causes output to stay active without mute. + + self.write_i2c_reg(INT_MASK0, 0xFF) + self.write_i2c_reg(INT_MASK1, 0xFF) + self.write_i2c_reg(INT_MASK2, 0xFF) + self.write_i2c_reg(INT_MASK3, 0xFF) + self.write_i2c_reg(INT_MASK4, 0xFF) + + self.write_i2c_reg(MODE_CTRL, 0x80) # Play audio, power up with playback, IV enabled + + # Initiate I2S communication on the Player + self.player.__start_i2s() def disable(self): + if self.is_enabled(): + self.player.__stop_i2s() + self.__amp_en.value(False) def is_enabled(self): return self.__amp_en.value() == 1 - - def play(self, wav_file, volume=0.5, loop=False): - self.player.queue(wav_file, loop=loop) - self.enable() - self.set_volume(volume) - self.player.resume() - - def stop(self): - self.player.stop() - - def is_playing(self): - return self.player.isplaying() def exit_soft_shutdown(self): self.write_i2c_reg(MODE_CTRL, 0x80) # Calling this after a play seems to wake the amp up, but adds around 16ms