From 71294d01453f3b22190bab86c76a1a204228edd7 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Sun, 24 Aug 2025 09:44:50 +0000 Subject: [PATCH 01/10] first pass at volume control overhaul This commit makes several interrelated changes at once: 1. There's a new lookup table based dB to int7 conversion mechansim for the analog volume setting properties (based on Table 6-24) 2. Major docs comment revisions for properties involved in DAC volume, speaker volume, headphone volume, speaker gain, and headphone gain 3. Added "_" prefix to private helper classes to stop them from cluttering up the Sphinx html docs build 4. Merged setter & getter comments into the setter comment for the properties I modified. NOTE: Sphinx does not render docs comments on property setters! 4. Assorted small-ish revisions to exception handling and arguments (convert SPK_GAIN_* constants to dB) to resolve inconsistent or surprising behavior discovered while revising docs comments Overall, the goals here are: 1. Make volume setting implementation work and be non-surprising 2. Document how it works 3. Make the TLV320 html docs more readable and complete. A lot of the docs comment info wasn't making it through into the html docs build because Sphinx ignores setter comments. The comments build fine, but it's possible the code has errors. Saving that testing for another day. --- adafruit_tlv320.py | 423 ++++++++++++++++++++++++++++++++------------- 1 file changed, 306 insertions(+), 117 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 803f1fb..66fd50c 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -15,6 +15,25 @@ Implementation Notes **Hardware:** +* The TLV320DAC chip has moderately complex onboard audio filtering, routing, + and amplification capability. Each of the signal chains for speaker, headphone + left, and headphone right start with the DAC, then they go through a mixer + stage, an analog volume (attenuation) stage, and finally an analog amplifier + stage. Parameters for each stage of each signal chain can be separately set + with different properties. + +* To understand how the different audio stages (DAC, volume, amplifier gain) + relate to each other, it can help to look at the Functional Block Diagram in + the TLV320DAC3100 datasheet: + https://learn.adafruit.com/adafruit-tlv320dac3100-i2s-dac/downloads + +* **CAUTION**: The TLV320 speaker amplifier has enough power to easily burn out + small 1W speakers if you max out the volume and gain settings. To be safe, + start with lower levels for ``speaker_volume`` and ``speaker_gain``, then work + your way up to find a comfortable listening level. Similarly, for the + headphone output, start low with ``headphone_volume``, + ``headphone_left_gain``\, and ``headphone_right_gain``\, then increase as + needed. **Software and Dependencies:** @@ -134,12 +153,6 @@ DAC_ROUTE_NONE = const(0b00) # DAC not routed DAC_ROUTE_MIXER = const(0b01) # DAC routed to mixer amplifier DAC_ROUTE_HP = const(0b10) # DAC routed directly to HP driver -# Speaker amplifier gain options -SPK_GAIN_6DB = const(0b00) # 6 dB gain -SPK_GAIN_12DB = const(0b01) # 12 dB gain -SPK_GAIN_18DB = const(0b10) # 18 dB gain -SPK_GAIN_24DB = const(0b11) # 24 dB gain - # Headphone common mode voltage settings HP_COMMON_1_35V = const(0b00) # Common-mode voltage 1.35V HP_COMMON_1_50V = const(0b01) # Common-mode voltage 1.50V @@ -160,10 +173,143 @@ BTN_DEBOUNCE_8MS = const(0b01) # 8ms debounce (1ms clock) BTN_DEBOUNCE_16MS = const(0b10) # 16ms debounce (2ms clock) BTN_DEBOUNCE_32MS = const(0b11) # 32ms debounce (4ms clock) +# Lookup table for speaker_volume and headphone_volume. +# These are from TLV320DAC3100 datasheet Table 6-24. +TABLE_6_24 = ( + 0, # 0 Begin linear segment: round((-1.99 * dB) - 0.2) + -0.5, # 1 + -1, # 2 + -1.5, # 3 + -2, # 4 + -2.5, # 5 + -3, # 6 + -3.5, # 7 + -4, # 8 + -4.5, # 9 + -5, # 10 + -5.5, # 11 + -6, # 12 + -6.5, # 13 + -7, # 14 + -7.5, # 15 + -8, # 16 + -8.5, # 17 + -9, # 18 + -9.5, # 19 + -10, # 20 + -10.5, # 21 + -11, # 22 + -11.5, # 23 + -12, # 24 + -12.5, # 25 + -13, # 26 + -13.5, # 27 + -14, # 28 + -14.5, # 29 + -15, # 30 + -15.5, # 31 + -16, # 32 + -16.5, # 33 + -17, # 34 + -17.5, # 35 + -18.1, # 36 + -18.6, # 37 + -19.1, # 38 + -19.6, # 39 + -20.1, # 40 + -20.6, # 41 + -21.1, # 42 + -21.6, # 43 + -22.1, # 44 + -22.6, # 45 + -23.1, # 46 + -23.6, # 47 + -24.1, # 48 + -24.6, # 49 + -25.1, # 50 + -25.6, # 51 + -26.1, # 52 + -26.6, # 53 + -27.1, # 54 + -27.6, # 55 + -28.1, # 56 + -28.6, # 57 + -29.1, # 58 + -29.6, # 59 + -30.1, # 60 + -30.6, # 61 + -31.1, # 62 + -31.6, # 63 + -32.1, # 64 + -32.6, # 65 + -33.1, # 66 + -33.6, # 67 + -34.1, # 68 + -34.6, # 69 + -35.2, # 70 + -35.7, # 71 + -36.2, # 72 + -36.7, # 73 + -37.2, # 74 + -37.7, # 75 + -38.2, # 76 + -38.7, # 77 + -39.2, # 78 + -39.7, # 79 + -40.2, # 80 + -40.7, # 81 + -41.2, # 82 + -41.7, # 83 + -42.1, # 84 + -42.7, # 85 + -43.2, # 86 + -43.8, # 87 + -44.3, # 88 + -44.8, # 89 + -45.2, # 90 + -45.8, # 91 + -46.2, # 92 + -46.7, # 93 + -47.4, # 94 + -47.9, # 95 + -48.2, # 96 + -48.7, # 97 + -49.3, # 98 + -50, # 99 + -50.3, # 100 + -51, # 101 + -51.4, # 102 + -51.8, # 103 + -52.2, # 104 + -52.7, # 105 End linear segment: round((-1.99 * dB) - 0.2) + -53.7, # 106 Begin curved segment + -54.2, # 107 + -55.3, # 108 + -56.7, # 109 + -58.3, # 110 + -60.2, # 111 + -62.7, # 112 + -64.3, # 113 + -66.2, # 114 + -68.7, # 115 + -72.2, # 116 End curved segment + -78.3, # 117 Begin constant segment -78.3 dB + -78.3, # 118 + -78.3, # 119 + -78.3, # 120 + -78.3, # 121 + -78.3, # 122 + -78.3, # 123 + -78.3, # 124 + -78.3, # 125 + -78.3, # 126 + -78.3, # 127 +) + # ruff: noqa: PLR0904, PLR0912, PLR0913, PLR0915, PLR0917 -class PagedRegisterBase: +class _PagedRegisterBase: """Base class for paged register access.""" def __init__(self, i2c_device, page): @@ -233,7 +379,7 @@ class PagedRegisterBase: self._write_register(register, reg_value) -class Page0Registers(PagedRegisterBase): +class _Page0Registers(_PagedRegisterBase): """Page 0 registers containing system configuration, clocking, etc.""" def __init__(self, i2c_device): @@ -629,7 +775,7 @@ class Page0Registers(PagedRegisterBase): time.sleep(0.01) -class Page1Registers(PagedRegisterBase): +class _Page1Registers(_PagedRegisterBase): """Page 1 registers containing analog output settings, HP/SPK controls, etc.""" def __init__(self, i2c_device): @@ -707,26 +853,37 @@ class Page1Registers(PagedRegisterBase): self._write_register(_SPK_VOL, value) def _configure_hpl_pga(self, gain_db=0, unmute=True): - """HPL driver PGA settings.""" - if gain_db > 9: - raise ValueError("Gain cannot be greater than 9") + """HPL driver PGA settings. + :raises ValueError: If set to anything outside of range 0 to 9 + """ + if not (0 <= gain_db <= 9): + raise ValueError("HPL gain must be in range 0 to 9") value = (gain_db & 0x0F) << 3 if unmute: value |= 1 << 2 self._write_register(_HPL_DRIVER, value) def _configure_hpr_pga(self, gain_db=0, unmute=True): - """HPR driver PGA settings.""" - if gain_db > 9: - raise ValueError("Gain cannot be greater than 9") + """HPR driver PGA settings. + :raises ValueError: If set to anything outside of range 0 to 9 + """ + if not (0 <= gain_db <= 9): + raise ValueError("HPR gain must be in range 0 to 9") value = (gain_db & 0x0F) << 3 if unmute: value |= 1 << 2 self._write_register(_HPR_DRIVER, value) - def _configure_spk_pga(self, gain=SPK_GAIN_6DB, unmute=True): - """Speaker driver settings.""" - value = (gain & 0x03) << 3 + def _configure_spk_pga(self, gain_db=6, unmute=True): + """Speaker driver settings. + :raises ValueError: If set to anything other than 6, 12, 18, or 24 + """ + if gain_db not in (6, 12, 18, 24): + raise ValueError( + f"Invalid speaker gain: {gain_db}. Must be 6, 12, 18, or 24." + ) + uint2_val = (gain_db / 6) - 1 + value = (uint2_val & 0x03) << 3 if unmute: value |= 1 << 2 self._write_register(_SPK_DRIVER, value) @@ -828,7 +985,7 @@ class Page1Registers(PagedRegisterBase): self._write_register(_INPUT_CM, value) -class Page3Registers(PagedRegisterBase): +class _Page3Registers(_PagedRegisterBase): """Page 3 registers containing timer settings.""" def __init__(self, i2c_device): @@ -857,9 +1014,9 @@ class TLV320DAC3100: self._device: I2CDevice = I2CDevice(i2c, address) # Initialize register page classes - self._page0: "Page0Registers" = Page0Registers(self._device) - self._page1: "Page1Registers" = Page1Registers(self._device) - self._page3: "Page3Registers" = Page3Registers(self._device) + self._page0: "Page0Registers" = _Page0Registers(self._device) + self._page1: "Page1Registers" = _Page1Registers(self._device) + self._page3: "Page3Registers" = _Page3Registers(self._device) self._sample_rate: int = 44100 self._bit_depth: int = 16 self._mclk_freq: int = 0 # Default blck @@ -878,6 +1035,35 @@ class TLV320DAC3100: ) self._page0._set_dac_volume_control(False, False, VOL_INDEPENDENT) + def _table_6_24_db_to_uint7(self, db: float) -> int: + """Convert gain dB to 7-bit unsigned int following datasheet Table 6-24. + + :param db: Analog gain in dB; range is 0 dB (loud) to -78.3 dB (soft) + :return: 7-bit unsigned int value, range is 0 (loud) to 127 (soft) + """ + # Clip dB argument to fit in the valid range if it's too big or too small + db = max(-78.3, min(0, db)) + # Loop through the table, looking for the lowest table index where the + # target dB value is not greater than the table dB value + result = 0 + for (table_u7, table_db) in enumerate(TABLE_6_24): + if db < table_db: + result = table_u7 + elif db == table_db: + result = table_u7 + break + else: + break + return result + + def _table_6_24_uint7_to_db(self, u7: int) -> float: + """Convert 7-bit unsigned int to gain dB following datasheet Table 6-24. + + :param u7: 7-bit unsigned int value, range is 0 (loud) to 127 (soft) + :return: Analog gain in dB, range is 0 dB (loud) to -78.3 dB (soft) + """ + return TABLE_6_24[max(0, min(127, int(u7)))] + # Basic properties and methods def reset(self) -> bool: @@ -1256,10 +1442,22 @@ class TLV320DAC3100: @property def dac_volume(self) -> float: - """ - Get the current DAC digital volume in dB. + """Current DAC digital volume in dB. - :return: Volume in dB (-63.5 to 24 dB) + Range is -63.5 dB (soft) to 24 dB (loud). + + This acts on two registers at once. In the datasheet, they are: + + * Page 0 / Register 65 (0x41): DAC Left Volume Control + + * Page 0 / Register 66 (0x42): DAC Right Volume Control + + Changing the DAC volume will change the signal level feeding into the + analog signal chains of the speaker and both headphone channels. You + should also be aware of ``speaker_volume``\, ``speaker_gain``\, + ``speaker_mute``\, ``headphone_volume``\, ``headphone_left_gain``\, + ``headphone_right_gain``\, ``headphone_left_mute``\, and + ``headphone_right_mute``\. """ left_vol = self._page0._read_register(_DAC_LVOL) right_vol = self._page0._read_register(_DAC_RVOL) @@ -1271,11 +1469,6 @@ class TLV320DAC3100: @dac_volume.setter def dac_volume(self, db: float) -> None: - """ - Set the DAC digital volume in dB. - - :param db: Volume in dB (-63.5 to 24 dB) - """ db = max(-63.5, min(24, db)) reg_val = self._convert_db_to_reg(db) self._page0._set_page() @@ -1337,117 +1530,117 @@ class TLV320DAC3100: @property def headphone_left_gain(self) -> int: - """The left headphone gain in dB. + """Left headphone amplifier gain in dB. - :return: Gain value in dB + Range is 0 dB (soft) to 9 dB (loud) in steps of 1 dB. + + In the datasheet, this is Page 1 / Register 40 (0x28): HPL Driver. + + Note that the headphone left channel volume is also affected by + ``dac_volume``\, ``headphone_volume``\, and ``headphone_left_mute``\. + + :raises ValueError: If set to a value outside the range of 0 to 9 """ reg_value = self._page1._read_register(_HPL_DRIVER) return (reg_value >> 3) & 0x0F @headphone_left_gain.setter def headphone_left_gain(self, gain_db: int) -> None: - """The left headphone gain in dB. - - :param gain_db: Gain value in dB - """ unmute = not self.headphone_left_mute - self._page1._configure_hpl_pga(gain_db, unmute) + # This call can raise ValueError + self._page1._configure_hpl_pga(int(gain_db), unmute) @property def headphone_left_mute(self) -> bool: - """The left headphone mute status. + """Left headphone mute status. - :return: True if left headphone is muted, False otherwise + True means left headphone is muted, False means not muted. """ reg_value = self._page1._read_register(_HPL_DRIVER) return not bool(reg_value & (1 << 2)) @headphone_left_mute.setter def headphone_left_mute(self, mute: bool) -> None: - """The left headphone mute status. - - :param mute: True to mute left headphone, False to unmute - """ gain = self.headphone_left_gain + # This could in theory raise ValueError, but that's very unlikely self._page1._configure_hpl_pga(gain, not mute) @property def headphone_right_gain(self) -> int: - """The right headphone gain in dB. + """Right headphone amplifier gain in dB. - :return: Gain value in dB + Range is 0 dB (soft) to 9 dB (loud) in steps of 1 dB. + + In the datasheet, this is Page 1 / Register 41 (0x29): HPR Driver. + + Note that the headphone right channel volume is also affected by + ``dac_volume``\, ``headphone_volume``\, and ``headphone_right_mute``\. + + :raises ValueError: If set to a value outside the range of 0 to 9 """ reg_value = self._page1._read_register(_HPR_DRIVER) return (reg_value >> 3) & 0x0F @headphone_right_gain.setter def headphone_right_gain(self, gain_db: int) -> None: - """The right headphone gain in dB. - - :param gain_db: Gain value in dB - """ unmute = not self.headphone_right_mute - self._page1._configure_hpr_pga(gain_db, unmute) + # This call can raise ValueError + self._page1._configure_hpr_pga(int(gain_db), unmute) @property def headphone_right_mute(self) -> bool: - """The right headphone mute status. + """Right headphone mute status. - :return: True if right headphone is muted, False otherwise + True means right headphone is muted, False means not muted. """ reg_value = self._page1._read_register(_HPR_DRIVER) return not bool(reg_value & (1 << 2)) @headphone_right_mute.setter def headphone_right_mute(self, mute: bool) -> None: - """The right headphone mute status. - - :param mute: True to mute right headphone, False to unmute - """ gain = self.headphone_right_gain self._page1._configure_hpr_pga(gain, not mute) @property def speaker_gain(self) -> int: - """The speaker gain setting in dB. + """Speaker amplifier gain setting in dB. - :return: The gain value in dB + Range is 6 dB (soft) to 24 dB (loud) in steps of 6 dB. + + In the datasheet, this is Page 1 / Register 42 (0x2A): Class-D Speaker + (SPK) Driver. + + Note that ``dac_volume``\, ``speaker_volume``\, and ``speaker_mute`` + also affect the speaker output level. + + :raises ValueError: If set to anything other than 6, 12, 18, or 24 """ + # This gives us a 2-bit unsigned integer where 0 means 6 dB, 1 is 12 dB, + # 2 is 18 dB, and 3 is 24 dB reg_value = self._page1._read_register(_SPK_DRIVER) - return (reg_value >> 3) & 0x03 + return (((reg_value >> 3) & 0x03) + 1) * 6 @speaker_gain.setter def speaker_gain(self, gain_db: int) -> None: - """The speaker gain in dB. - - :param gain_db: Speaker gain in dB (6, 12, 18, or 24) - :raises ValueError: If gain_db is not a valid value """ - # Convert dB to register value - gain_mapping: List[int] = [SPK_GAIN_6DB, SPK_GAIN_12DB, SPK_GAIN_18DB, SPK_GAIN_24DB] - - if gain_db not in gain_mapping: - raise ValueError( - f"Invalid preset value: {gain_db}. Must be one of the SPK_GAIN_* constants." - ) + :raises ValueError: If set to anything other than 6, 12, 18, or 24 + """ unmute = not self.speaker_mute + # This relies on _configure_spk_pga() to raise ValueError if the gain + # value is out of range self._page1._configure_spk_pga(gain_db, unmute) @property def speaker_mute(self) -> bool: """The speaker mute status. - :return: True if speaker is muted, False otherwise + True means speaker is muted, False means unmuted. """ reg_value = self._page1._read_register(_SPK_DRIVER) return not bool(reg_value & (1 << 2)) @speaker_mute.setter def speaker_mute(self, mute: bool) -> None: - """The speaker mute status. - - :param mute: True to mute speaker, False to unmute - """ gain = self.speaker_gain # Unmute is inverse of mute self._page1._configure_spk_pga(gain, not mute) @@ -1884,7 +2077,7 @@ class TLV320DAC3100: self.right_dac_mute = False self.left_dac_path = DAC_PATH_NORMAL self.right_dac_path = DAC_PATH_NORMAL - self.speaker_gain = SPK_GAIN_18DB + self.speaker_gain = 6 # safest speaker amp gain option: 6 dB self._page1._set_speaker_enabled(True) self._page1._configure_analog_inputs( left_dac=DAC_ROUTE_MIXER, right_dac=DAC_ROUTE_MIXER @@ -1896,60 +2089,56 @@ class TLV320DAC3100: @property def headphone_volume(self) -> float: - """The current headphone volume in dB. - :return: The volume in dB (0 = max, -78.3 = min) - """ - left_gain = self._page1._read_register(_HPL_VOL) & 0x7F - right_gain = self._page1._read_register(_HPR_VOL) & 0x7F + """Current headphone analog volume in dB. - if left_gain == right_gain: - db = -left_gain / 2.0 - db = max(-78.3, min(0, db)) - return db + Range is 0 (loud) to -78.3 (very soft). + + This acts on two registers at once. In the datasheet they are: + + * Page 1 / Register 36 (0x24): Left Analog Volume to HPL + + * Page 1 / Register 37 (0x25) Right Analog Volume to HPR + + Note that headphone output is also affected by ``dac_volume``\, + ``headphone_left_gain``\, ``headphone_right_gain``\, + ``headphone_left_mute``\, and ``headphone_right_mute``\. + """ + left_gain_u7 = self._page1._read_register(_HPL_VOL) & 0x7F + right_gain_u7 = self._page1._read_register(_HPR_VOL) & 0x7F + left_db = self._table_6_24_uint7_to_db(left_gain_u7) + right_db = self._table_6_24_uint7_to_db(right_gain_u7) + if left_db == right_db: + return left_db else: - avg_gain = (left_gain + right_gain) / 2 - db = -avg_gain / 2.0 - db = max(-78.3, min(0, db)) - return db + return (left_db + right_db) / 2 @headphone_volume.setter def headphone_volume(self, db: float) -> None: - """ - Set headphone volume in dB (0 to -78.3 dB) - :param db: Volume in dB (0 = max, -78.3 = min) - """ - if db > 0: - db = 0 - elif db < -78.3: - db = -78.3 - gain = int(-2 * db) - gain = max(0, min(gain, 127)) - self._page1._set_hpl_volume(route_enabled=True, gain=gain) - self._page1._set_hpr_volume(route_enabled=True, gain=gain) + # The table 6-24 lookup function includes min/max range clipping + gain_u7 = self._table_6_24_db_to_uint7(db) + self._page1._set_hpl_volume(route_enabled=True, gain=gain_u7) + self._page1._set_hpr_volume(route_enabled=True, gain=gain_u7) @property def speaker_volume(self) -> float: - """The current speaker volume in dB. + """Current speaker analog volume in dB. - :return: The volume in dB (0 = max, -63.5 = min) + Range is 0 (loud) to -78.3 (very soft). + + In the datasheet, this is Page 1 / Register 38 (0x26): Left Analog + Volume to SPK. + + Note that ``dac_volume``\, ``speaker_gain``\, and ``speaker_mute`` also + affect the speaker output level. """ - gain = self._page1._read_register(_SPK_VOL) & 0x7F - # Convert from register value to dB - # 55 ≈ 0dB, 0 ≈ -63.5dB - db = (gain - 55) / 1.14 - return db + gain_u7 = self._page1._read_register(_SPK_VOL) & 0x7F + return self._table_6_24_uint7_to_db(gain_u7) @speaker_volume.setter def speaker_volume(self, db: float) -> None: - """ - - :param db: Volume in dB (0 = max, -63.5 = min) - """ - if db > 0: - db = 0 - gain = int(55 + (db * 1.14)) - gain = max(0, min(gain, 127)) - self._page1._set_spk_volume(route_enabled=True, gain=gain) + # The table 6-24 lookup function includes min/max range clipping + gain_u7 = self._table_6_24_uint7_to_dB(db) + self._page1._set_spk_volume(route_enabled=True, gain=gain_u7) @property def sample_rate(self) -> int: From f694d14682496f67ef33149bbe32de35f0cdbed2 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Sun, 24 Aug 2025 21:36:38 +0000 Subject: [PATCH 02/10] level up documentation comments This fixes assorted Sphinx documentation stuff: 1. Add CSS workaround for horizontal stacking glitch in rtd theme 2. Convert plain comments for public constants to doc-comment style so they will get included in the html docs 3. Change Sphinx autodoc's default sort order from alphabetical to groupwise. Now the class methods come first, the properties come next, and the constants go at the end. It's much easier to read this way. --- adafruit_tlv320.py | 115 +++++++++++++++++++++++++++------------- docs/_static/custom.css | 4 ++ docs/conf.py | 6 +++ 3 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 docs/_static/custom.css diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 66fd50c..3cb6730 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -15,6 +15,11 @@ Implementation Notes **Hardware:** +* `Adafruit TLV320DAC3100 - I2S DAC `_ + +* `Adafruit Fruit Jam `_ + + * The TLV320DAC chip has moderately complex onboard audio filtering, routing, and amplification capability. Each of the signal chains for speaker, headphone left, and headphone right start with the DAC, then they go through a mixer @@ -122,56 +127,92 @@ DATA_LEN_24 = const(0b10) # 24 bits DATA_LEN_32 = const(0b11) # 32 bits # GPIO1 pin mode options -GPIO1_DISABLED = const(0b0000) # GPIO1 disabled (input and output buffers powered down) -GPIO1_INPUT_MODE = const(0b0001) # Input mode (secondary BCLK/WCLK/DIN input or ClockGen) -GPIO1_GPI = const(0b0010) # General-purpose input -GPIO1_GPO = const(0b0011) # General-purpose output -GPIO1_CLKOUT = const(0b0100) # CLKOUT output -GPIO1_INT1 = const(0b0101) # INT1 output -GPIO1_INT2 = const(0b0110) # INT2 output -GPIO1_BCLK_OUT = const(0b1000) # Secondary BCLK output for codec interface -GPIO1_WCLK_OUT = const(0b1001) # Secondary WCLK output for codec interface +#: GPIO1 pin mode options: GPIO1 disabled (input and output buffers powered down) +GPIO1_DISABLED = const(0b0000) +#: GPIO1 pin mode options: Input mode (secondary BCLK/WCLK/DIN input or ClockGen) +GPIO1_INPUT_MODE = const(0b0001) +#: GPIO1 pin mode options: General-purpose input +GPIO1_GPI = const(0b0010) +#: GPIO1 pin mode options: General-purpose output +GPIO1_GPO = const(0b0011) +#: GPIO1 pin mode options: CLKOUT output +GPIO1_CLKOUT = const(0b0100) +#: GPIO1 pin mode options: INT1 output +GPIO1_INT1 = const(0b0101) +#: GPIO1 pin mode options: INT2 output +GPIO1_INT2 = const(0b0110) +#: GPIO1 pin mode options: Secondary BCLK output for codec interface +GPIO1_BCLK_OUT = const(0b1000) +#: GPIO1 pin mode options: Secondary WCLK output for codec interface +GPIO1_WCLK_OUT = const(0b1001) # DAC channel data path options -DAC_PATH_OFF = const(0b00) # DAC data path off -DAC_PATH_NORMAL = const(0b01) # Normal path (L->L or R->R) -DAC_PATH_SWAPPED = const(0b10) # Swapped path (R->L or L->R) -DAC_PATH_MIXED = const(0b11) # Mixed L+R path +#: DAC channel data path option: DAC data path off +DAC_PATH_OFF = const(0b00) +#: DAC channel data path option: Normal path (L->L or R->R) +DAC_PATH_NORMAL = const(0b01) +#: DAC channel data path option: Swapped path (R->L or L->R) +DAC_PATH_SWAPPED = const(0b10) +#: DAC channel data path option: Mixed L+R path +DAC_PATH_MIXED = const(0b11) # DAC volume control soft stepping options -VOLUME_STEP_1SAMPLE = const(0b00) # One step per sample -VOLUME_STEP_2SAMPLE = const(0b01) # One step per two samples -VOLUME_STEP_DISABLED = const(0b10) # Soft stepping disabled +#: DAC volume control soft stepping option: One step per sample +VOLUME_STEP_1SAMPLE = const(0b00) +#: DAC volume control soft stepping option: One step per two samples +VOLUME_STEP_2SAMPLE = const(0b01) +#: DAC volume control soft stepping option: Soft stepping disabled +VOLUME_STEP_DISABLED = const(0b10) # DAC volume control configuration options -VOL_INDEPENDENT = const(0b00) # Left and right channels independent -VOL_LEFT_TO_RIGHT = const(0b01) # Left follows right volume -VOL_RIGHT_TO_LEFT = const(0b10) # Right follows left volume +#: DAC volume control configuration option: Left and right channels independent +VOL_INDEPENDENT = const(0b00) +#: DAC volume control configuration option: Left follows right volume +VOL_LEFT_TO_RIGHT = const(0b01) +#: DAC volume control configuration option: Right follows left volume +VOL_RIGHT_TO_LEFT = const(0b10) # DAC output routing options -DAC_ROUTE_NONE = const(0b00) # DAC not routed -DAC_ROUTE_MIXER = const(0b01) # DAC routed to mixer amplifier -DAC_ROUTE_HP = const(0b10) # DAC routed directly to HP driver +#: DAC output routing option: DAC not routed +DAC_ROUTE_NONE = const(0b00) +#: DAC output routing option: DAC routed to mixer amplifier +DAC_ROUTE_MIXER = const(0b01) +#: DAC output routing option: DAC routed directly to HP driver +DAC_ROUTE_HP = const(0b10) -# Headphone common mode voltage settings -HP_COMMON_1_35V = const(0b00) # Common-mode voltage 1.35V -HP_COMMON_1_50V = const(0b01) # Common-mode voltage 1.50V -HP_COMMON_1_65V = const(0b10) # Common-mode voltage 1.65V -HP_COMMON_1_80V = const(0b11) # Common-mode voltage 1.80V +# Headphone common mode voltage options +#: Headphone common mode voltage option: Common-mode voltage 1.35V +HP_COMMON_1_35V = const(0b00) +#: Headphone common mode voltage option: Common-mode voltage 1.50V +HP_COMMON_1_50V = const(0b01) +#: Headphone common mode voltage option: Common-mode voltage 1.65V +HP_COMMON_1_65V = const(0b10) +#: Headphone common mode voltage option: Common-mode voltage 1.80V +HP_COMMON_1_80V = const(0b11) # Headset detection debounce time options -DEBOUNCE_16MS = const(0b000) # 16ms debounce (2ms clock) -DEBOUNCE_32MS = const(0b001) # 32ms debounce (4ms clock) -DEBOUNCE_64MS = const(0b010) # 64ms debounce (8ms clock) -DEBOUNCE_128MS = const(0b011) # 128ms debounce (16ms clock) -DEBOUNCE_256MS = const(0b100) # 256ms debounce (32ms clock) -DEBOUNCE_512MS = const(0b101) # 512ms debounce (64ms clock) +#: Headset detection debounce time option: 16ms debounce (2ms clock) +DEBOUNCE_16MS = const(0b000) +#: Headset detection debounce time option: 32ms debounce (4ms clock) +DEBOUNCE_32MS = const(0b001) +#: Headset detection debounce time option: 64ms debounce (8ms clock) +DEBOUNCE_64MS = const(0b010) +#: Headset detection debounce time option: 128ms debounce (16ms clock) +DEBOUNCE_128MS = const(0b011) +#: Headset detection debounce time option: 256ms debounce (32ms clock) +DEBOUNCE_256MS = const(0b100) +#: Headset detection debounce time option: 512ms debounce (64ms clock) +DEBOUNCE_512MS = const(0b101) # Button press debounce time options -BTN_DEBOUNCE_0MS = const(0b00) # No debounce -BTN_DEBOUNCE_8MS = const(0b01) # 8ms debounce (1ms clock) -BTN_DEBOUNCE_16MS = const(0b10) # 16ms debounce (2ms clock) -BTN_DEBOUNCE_32MS = const(0b11) # 32ms debounce (4ms clock) +#: Button press debounce time option: No debounce +BTN_DEBOUNCE_0MS = const(0b00) +#: Button press debounce time option: 8ms debounce (1ms clock) +BTN_DEBOUNCE_8MS = const(0b01) +#: Button press debounce time option: 16ms debounce (2ms clock) +BTN_DEBOUNCE_16MS = const(0b10) +#: Button press debounce time option: 32ms debounce (4ms clock) +BTN_DEBOUNCE_32MS = const(0b11) # Lookup table for speaker_volume and headphone_volume. # These are from TLV320DAC3100 datasheet Table 6-24. diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..609bc5c --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,4 @@ +/* Monkey patch the rtd theme to prevent horizontal stacking of short items + * see https://github.com/readthedocs/sphinx_rtd_theme/issues/1301 +*/ +.py.property{display: block !important;} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 6ec6938..b356836 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,9 @@ autodoc_mock_imports = ["digitalio", "busio", "adafruit_bus_device", "micropytho autodoc_preserve_defaults = True +# Override the default config in which autodoc sorts things alphabetically +autodoc_member_order = 'groupwise' + intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "BusDevice": ("https://docs.circuitpython.org/projects/busdevice/en/latest/", None), @@ -117,6 +120,9 @@ html_theme = "sphinx_rtd_theme" # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +# Include extra css to work around rtd theme glitches +html_css_files = ['custom.css'] + # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. From 5e076772c1f346fc66386f17c525e0d67e2147fc Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:37:20 +0000 Subject: [PATCH 03/10] fix failing pre-commit linter checks This gets ruff to stop complaining. Haven't tested the code yet though. That's next. --- adafruit_tlv320.py | 139 ++++++++++++++++++++-------------------- docs/_static/custom.css | 8 ++- docs/conf.py | 4 +- 3 files changed, 77 insertions(+), 74 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 3cb6730..9ad56fa 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -19,7 +19,6 @@ Implementation Notes * `Adafruit Fruit Jam `_ - * The TLV320DAC chip has moderately complex onboard audio filtering, routing, and amplification capability. Each of the signal chains for speaker, headphone left, and headphone right start with the DAC, then they go through a mixer @@ -214,44 +213,46 @@ BTN_DEBOUNCE_16MS = const(0b10) #: Button press debounce time option: 32ms debounce (4ms clock) BTN_DEBOUNCE_32MS = const(0b11) +# ruff: noqa: PLR0904, PLR0912, PLR0913, PLR0915, PLR0917 + # Lookup table for speaker_volume and headphone_volume. # These are from TLV320DAC3100 datasheet Table 6-24. TABLE_6_24 = ( - 0, # 0 Begin linear segment: round((-1.99 * dB) - 0.2) - -0.5, # 1 - -1, # 2 - -1.5, # 3 - -2, # 4 - -2.5, # 5 - -3, # 6 - -3.5, # 7 - -4, # 8 - -4.5, # 9 - -5, # 10 - -5.5, # 11 - -6, # 12 - -6.5, # 13 - -7, # 14 - -7.5, # 15 - -8, # 16 - -8.5, # 17 - -9, # 18 - -9.5, # 19 - -10, # 20 + 0, # 0 Begin linear segment: round((-1.99 * dB) - 0.2) + -0.5, # 1 + -1, # 2 + -1.5, # 3 + -2, # 4 + -2.5, # 5 + -3, # 6 + -3.5, # 7 + -4, # 8 + -4.5, # 9 + -5, # 10 + -5.5, # 11 + -6, # 12 + -6.5, # 13 + -7, # 14 + -7.5, # 15 + -8, # 16 + -8.5, # 17 + -9, # 18 + -9.5, # 19 + -10, # 20 -10.5, # 21 - -11, # 22 + -11, # 22 -11.5, # 23 - -12, # 24 + -12, # 24 -12.5, # 25 - -13, # 26 + -13, # 26 -13.5, # 27 - -14, # 28 + -14, # 28 -14.5, # 29 - -15, # 30 + -15, # 30 -15.5, # 31 - -16, # 32 + -16, # 32 -16.5, # 33 - -17, # 34 + -17, # 34 -17.5, # 35 -18.1, # 36 -18.6, # 37 @@ -316,9 +317,9 @@ TABLE_6_24 = ( -48.2, # 96 -48.7, # 97 -49.3, # 98 - -50, # 99 + -50, # 99 -50.3, # 100 - -51, # 101 + -51, # 101 -51.4, # 102 -51.8, # 103 -52.2, # 104 @@ -347,7 +348,36 @@ TABLE_6_24 = ( -78.3, # 127 ) -# ruff: noqa: PLR0904, PLR0912, PLR0913, PLR0915, PLR0917 + +def _table_6_24_db_to_uint7(self, db: float) -> int: + """Convert gain dB to 7-bit unsigned int following datasheet Table 6-24. + + :param db: Analog gain in dB; range is 0 dB (loud) to -78.3 dB (soft) + :return: 7-bit unsigned int value, range is 0 (loud) to 127 (soft) + """ + # Clip dB argument to fit in the valid range if it's too big or too small + db = max(-78.3, min(0, db)) + # Loop through the table, looking for the lowest table index where the + # target dB value is not greater than the table dB value + result = 0 + for table_u7, table_db in enumerate(TABLE_6_24): + if db < table_db: + result = table_u7 + elif db == table_db: + result = table_u7 + break + else: + break + return result + + +def _table_6_24_uint7_to_db(self, u7: int) -> float: + """Convert 7-bit unsigned int to gain dB following datasheet Table 6-24. + + :param u7: 7-bit unsigned int value, range is 0 (loud) to 127 (soft) + :return: Analog gain in dB, range is 0 dB (loud) to -78.3 dB (soft) + """ + return TABLE_6_24[max(0, min(127, int(u7)))] class _PagedRegisterBase: @@ -919,10 +949,8 @@ class _Page1Registers(_PagedRegisterBase): """Speaker driver settings. :raises ValueError: If set to anything other than 6, 12, 18, or 24 """ - if gain_db not in (6, 12, 18, 24): - raise ValueError( - f"Invalid speaker gain: {gain_db}. Must be 6, 12, 18, or 24." - ) + if gain_db not in set((6, 12, 18, 24)): + raise ValueError(f"Invalid speaker gain: {gain_db}. Must be 6, 12, 18, or 24.") uint2_val = (gain_db / 6) - 1 value = (uint2_val & 0x03) << 3 if unmute: @@ -1076,35 +1104,6 @@ class TLV320DAC3100: ) self._page0._set_dac_volume_control(False, False, VOL_INDEPENDENT) - def _table_6_24_db_to_uint7(self, db: float) -> int: - """Convert gain dB to 7-bit unsigned int following datasheet Table 6-24. - - :param db: Analog gain in dB; range is 0 dB (loud) to -78.3 dB (soft) - :return: 7-bit unsigned int value, range is 0 (loud) to 127 (soft) - """ - # Clip dB argument to fit in the valid range if it's too big or too small - db = max(-78.3, min(0, db)) - # Loop through the table, looking for the lowest table index where the - # target dB value is not greater than the table dB value - result = 0 - for (table_u7, table_db) in enumerate(TABLE_6_24): - if db < table_db: - result = table_u7 - elif db == table_db: - result = table_u7 - break - else: - break - return result - - def _table_6_24_uint7_to_db(self, u7: int) -> float: - """Convert 7-bit unsigned int to gain dB following datasheet Table 6-24. - - :param u7: 7-bit unsigned int value, range is 0 (loud) to 127 (soft) - :return: Analog gain in dB, range is 0 dB (loud) to -78.3 dB (soft) - """ - return TABLE_6_24[max(0, min(127, int(u7)))] - # Basic properties and methods def reset(self) -> bool: @@ -2146,8 +2145,8 @@ class TLV320DAC3100: """ left_gain_u7 = self._page1._read_register(_HPL_VOL) & 0x7F right_gain_u7 = self._page1._read_register(_HPR_VOL) & 0x7F - left_db = self._table_6_24_uint7_to_db(left_gain_u7) - right_db = self._table_6_24_uint7_to_db(right_gain_u7) + left_db = _table_6_24_uint7_to_db(left_gain_u7) + right_db = _table_6_24_uint7_to_db(right_gain_u7) if left_db == right_db: return left_db else: @@ -2156,7 +2155,7 @@ class TLV320DAC3100: @headphone_volume.setter def headphone_volume(self, db: float) -> None: # The table 6-24 lookup function includes min/max range clipping - gain_u7 = self._table_6_24_db_to_uint7(db) + gain_u7 = _table_6_24_db_to_uint7(db) self._page1._set_hpl_volume(route_enabled=True, gain=gain_u7) self._page1._set_hpr_volume(route_enabled=True, gain=gain_u7) @@ -2173,12 +2172,12 @@ class TLV320DAC3100: affect the speaker output level. """ gain_u7 = self._page1._read_register(_SPK_VOL) & 0x7F - return self._table_6_24_uint7_to_db(gain_u7) + return _table_6_24_uint7_to_db(gain_u7) @speaker_volume.setter def speaker_volume(self, db: float) -> None: # The table 6-24 lookup function includes min/max range clipping - gain_u7 = self._table_6_24_uint7_to_dB(db) + gain_u7 = _table_6_24_uint7_to_dB(db) self._page1._set_spk_volume(route_enabled=True, gain=gain_u7) @property diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 609bc5c..d60cf4b 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,4 +1,8 @@ +/* SPDX-FileCopyrightText: 2025 Sam Blenny + * SPDX-License-Identifier: MIT + */ + /* Monkey patch the rtd theme to prevent horizontal stacking of short items * see https://github.com/readthedocs/sphinx_rtd_theme/issues/1301 -*/ -.py.property{display: block !important;} \ No newline at end of file + */ +.py.property{display: block !important;} diff --git a/docs/conf.py b/docs/conf.py index b356836..138b384 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ autodoc_mock_imports = ["digitalio", "busio", "adafruit_bus_device", "micropytho autodoc_preserve_defaults = True # Override the default config in which autodoc sorts things alphabetically -autodoc_member_order = 'groupwise' +autodoc_member_order = "groupwise" intersphinx_mapping = { "python": ("https://docs.python.org/3", None), @@ -121,7 +121,7 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] # Include extra css to work around rtd theme glitches -html_css_files = ['custom.css'] +html_css_files = ["custom.css"] # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 From e1f10ab7df2e59d0b643e5a6e3323916a0e63c69 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Mon, 25 Aug 2025 00:04:06 +0000 Subject: [PATCH 04/10] revise doc-comments to fix missing info Thought I was done with doc-comments, but then I noticed that there wasn't a clear indication in the html about which properties were settable and which were read only. I also discovered that several properties that could raise ValueError weren't marked as such. (technically they were in the code, but Sphinx was ignoring that) Changes: 1. Systematic :getter: and :setter: attributes for properties 2. Move `:raises: ...` from setter comments to getter comments so they show up in the html doc. This particularly applies to setters that use constants. 3. Change `:return: ...` to `:getter: ...` for getter functions --- adafruit_tlv320.py | 272 ++++++++++++++++++++++++--------------------- 1 file changed, 148 insertions(+), 124 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 9ad56fa..83ae716 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -1117,7 +1117,8 @@ class TLV320DAC3100: def overtemperature(self) -> bool: """Check if the chip is overheating. - :return: True if overtemperature condition exists, False otherwise + :getter: Return True if overtemperature condition exists, False + otherwise """ return self._page0._is_overtemperature() @@ -1188,16 +1189,15 @@ class TLV320DAC3100: def left_dac(self) -> bool: """The left DAC enabled status. - :return: True if left DAC is enabled, False otherwise + True if left DAC is enabled, False otherwise + + :getter: Return status + :setter: Set status """ return self._page0._get_dac_data_path()["left_dac_on"] @left_dac.setter def left_dac(self, enabled: bool) -> None: - """The left DAC enabled status. - - :param enabled: True to enable left DAC, False to disable - """ current: DACDataPath = self._page0._get_dac_data_path() self._page0._set_dac_data_path( enabled, @@ -1211,16 +1211,15 @@ class TLV320DAC3100: def right_dac(self) -> bool: """The right DAC enabled status. - :return: True if right DAC is enabled, False otherwise + True if right DAC is enabled, False otherwise. + + :getter: Return status + :setter: Set status """ return self._page0._get_dac_data_path()["right_dac_on"] @right_dac.setter def right_dac(self, enabled: bool) -> None: - """The right DAC enabled status. - - :param enabled: True to enable right DAC, False to disable - """ current: DACDataPath = self._page0._get_dac_data_path() self._page0._set_dac_data_path( current["left_dac_on"], @@ -1234,17 +1233,16 @@ class TLV320DAC3100: def left_dac_path(self) -> int: """The left DAC path setting. - :return: One of the DAC_PATH_* constants + One of the DAC_PATH_* constants + + :getter: Return left DAC path + :setter: Set left DAC path + :raises ValueError: If set to something that's not a DAC_PATH_* constant """ return self._page0._get_dac_data_path()["left_path"] @left_dac_path.setter def left_dac_path(self, path: int) -> None: - """The left DAC path. - - :param path: One of the DAC_PATH_* constants - :raises ValueError: If path is not a valid DAC_PATH_* constant - """ valid_paths: List[int] = [DAC_PATH_OFF, DAC_PATH_NORMAL, DAC_PATH_SWAPPED, DAC_PATH_MIXED] if path not in valid_paths: @@ -1265,17 +1263,16 @@ class TLV320DAC3100: def right_dac_path(self) -> int: """The right DAC path setting. - :return: One of the DAC_PATH_* constants + One of the DAC_PATH_* constants + + :getter: Return right DAC path + :setter: Set right DAC path + :raises ValueError: If set to something that's not a DAC_PATH_* constant """ return self._page0._get_dac_data_path()["right_path"] @right_dac_path.setter def right_dac_path(self, path: int) -> None: - """The right DAC path. - - :param path: One of the DAC_PATH_* constants - :raises ValueError: If path is not a valid DAC_PATH_* constant - """ valid_paths: List[int] = [DAC_PATH_OFF, DAC_PATH_NORMAL, DAC_PATH_SWAPPED, DAC_PATH_MIXED] if path not in valid_paths: @@ -1296,17 +1293,16 @@ class TLV320DAC3100: def dac_volume_step(self) -> int: """The DAC volume step setting. - :return: One of the VOLUME_STEP_* constants + One of the VOLUME_STEP_* constants. + + :getter: Return current volume step + :setter: Set volume step + :raises ValueError: If step is not a valid VOLUME_STEP_* constant """ return self._page0._get_dac_data_path()["volume_step"] @dac_volume_step.setter def dac_volume_step(self, step: int) -> None: - """The DAC volume step setting. - - :param step: One of the VOLUME_STEP_* constants - :raises ValueError: If step is not a valid VOLUME_STEP_* constant - """ valid_steps: List[int] = [VOLUME_STEP_1SAMPLE, VOLUME_STEP_2SAMPLE, VOLUME_STEP_DISABLED] if step not in valid_steps: @@ -1363,16 +1359,15 @@ class TLV320DAC3100: def left_dac_mute(self) -> bool: """The left DAC mute status. - :return: True if left DAC is muted, False otherwise + True if left DAC is muted, False otherwise. + + :getter: Return status + :setter: Set status """ return self._page0._get_dac_volume_control()["left_mute"] @left_dac_mute.setter def left_dac_mute(self, mute: bool) -> None: - """The left DAC mute status. - - :param mute: True to mute left DAC, False to unmute - """ current: DACVolumeControl = self._page0._get_dac_volume_control() self._page0._set_dac_volume_control(mute, current["right_mute"], current["control"]) @@ -1380,16 +1375,15 @@ class TLV320DAC3100: def right_dac_mute(self) -> bool: """The right DAC mute status. - :return: True if right DAC is muted, False otherwise + True if right DAC is muted, False otherwise. + + :getter: Return status + :setter: Set status """ return self._page0._get_dac_volume_control()["right_mute"] @right_dac_mute.setter def right_dac_mute(self, mute: bool) -> None: - """The right DAC mute status. - - :param mute: True to mute right DAC, False to unmute - """ current: DACVolumeControl = self._page0._get_dac_volume_control() self._page0._set_dac_volume_control(current["left_mute"], mute, current["control"]) @@ -1397,17 +1391,16 @@ class TLV320DAC3100: def dac_volume_control_mode(self) -> int: """The DAC volume control mode. - :return: One of the VOL_* constants + One of the VOL_* constants. + + :getter: Return mode + :setter: Set mode + :raises ValueError: If mode is not a valid VOL_* constant """ return self._page0._get_dac_volume_control()["control"] @dac_volume_control_mode.setter def dac_volume_control_mode(self, mode: int) -> None: - """The volume control mode. - - :param mode: One of the VOL_* constants for volume control mode - :raises ValueError: If mode is not a valid VOL_* constant - """ valid_modes: List[int] = [VOL_INDEPENDENT, VOL_LEFT_TO_RIGHT, VOL_RIGHT_TO_LEFT] if mode not in valid_modes: raise ValueError( @@ -1420,32 +1413,26 @@ class TLV320DAC3100: def left_dac_channel_volume(self) -> float: """Left DAC channel volume in dB. - :return: Volume in dB + :getter: Return volume + :setter: Set volume """ return self._page0._get_channel_volume(False) @left_dac_channel_volume.setter def left_dac_channel_volume(self, db: float) -> None: - """Left DAC channel volume in dB. - - :param db: Volume in dB - """ self._page0._set_channel_volume(False, db) @property def right_dac_channel_volume(self) -> float: """Right DAC channel volume in dB. - :return: Volume in dB + :getter: Return volume + :setter: Set volume """ return self._page0._get_channel_volume(True) @right_dac_channel_volume.setter def right_dac_channel_volume(self, db: float) -> None: - """Right DAC channel volume in dB. - - :param db: Volume in dB - """ self._page0._set_channel_volume(True, db) @staticmethod @@ -1498,6 +1485,9 @@ class TLV320DAC3100: ``speaker_mute``\, ``headphone_volume``\, ``headphone_left_gain``\, ``headphone_right_gain``\, ``headphone_left_mute``\, and ``headphone_right_mute``\. + + :getter: Return volume + :setter: Set volume """ left_vol = self._page0._read_register(_DAC_LVOL) right_vol = self._page0._read_register(_DAC_RVOL) @@ -1579,6 +1569,8 @@ class TLV320DAC3100: Note that the headphone left channel volume is also affected by ``dac_volume``\, ``headphone_volume``\, and ``headphone_left_mute``\. + :getter: Return gain + :setter: Set gain :raises ValueError: If set to a value outside the range of 0 to 9 """ reg_value = self._page1._read_register(_HPL_DRIVER) @@ -1595,6 +1587,9 @@ class TLV320DAC3100: """Left headphone mute status. True means left headphone is muted, False means not muted. + + :getter: Return status + :setter: Set status """ reg_value = self._page1._read_register(_HPL_DRIVER) return not bool(reg_value & (1 << 2)) @@ -1616,6 +1611,8 @@ class TLV320DAC3100: Note that the headphone right channel volume is also affected by ``dac_volume``\, ``headphone_volume``\, and ``headphone_right_mute``\. + :getter: Return gain + :setter: Set gain :raises ValueError: If set to a value outside the range of 0 to 9 """ reg_value = self._page1._read_register(_HPR_DRIVER) @@ -1632,6 +1629,9 @@ class TLV320DAC3100: """Right headphone mute status. True means right headphone is muted, False means not muted. + + :getter: Return status + :setter: Set status """ reg_value = self._page1._read_register(_HPR_DRIVER) return not bool(reg_value & (1 << 2)) @@ -1653,6 +1653,8 @@ class TLV320DAC3100: Note that ``dac_volume``\, ``speaker_volume``\, and ``speaker_mute`` also affect the speaker output level. + :getter: Return gain + :setter: Set gain :raises ValueError: If set to anything other than 6, 12, 18, or 24 """ # This gives us a 2-bit unsigned integer where 0 means 6 dB, 1 is 12 dB, @@ -1662,9 +1664,6 @@ class TLV320DAC3100: @speaker_gain.setter def speaker_gain(self, gain_db: int) -> None: - """ - :raises ValueError: If set to anything other than 6, 12, 18, or 24 - """ unmute = not self.speaker_mute # This relies on _configure_spk_pga() to raise ValueError if the gain # value is out of range @@ -1675,6 +1674,9 @@ class TLV320DAC3100: """The speaker mute status. True means speaker is muted, False means unmuted. + + :getter: Return status + :setter: Set status """ reg_value = self._page1._read_register(_SPK_DRIVER) return not bool(reg_value & (1 << 2)) @@ -1689,7 +1691,7 @@ class TLV320DAC3100: def dac_flags(self) -> Dict[str, Any]: """The DAC and output driver status flags. - :return: Dictionary with status flags + :getter: Return dictionary with status flags """ return self._page0._get_dac_flags() @@ -1697,18 +1699,17 @@ class TLV320DAC3100: def gpio1_mode(self) -> int: """The current GPIO1 pin mode. - :return: One of the GPIO1_* mode constants + One of the GPIO1_* mode constants. + + :getter: Return mode + :setter: Set mode + :raises ValueError: If mode is not a valid GPIO1_* constant """ value = self._page0._read_register(_GPIO1_CTRL) return (value >> 2) & 0x0F @gpio1_mode.setter def gpio1_mode(self, mode: int) -> None: - """The GPIO1 pin mode. - - :param mode: One of the GPIO1_* mode constants - :raises ValueError: If mode is not a valid GPIO1_* constant - """ valid_modes: List[int] = [ GPIO1_DISABLED, GPIO1_INPUT_MODE, @@ -1730,7 +1731,7 @@ class TLV320DAC3100: def din_input(self) -> int: """The current DIN input value. - :return: The DIN input value + :getter: Return the DIN input value """ return self._page0._get_din_input() @@ -1738,7 +1739,7 @@ class TLV320DAC3100: def codec_interface(self) -> Dict[str, Any]: """The current codec interface settings. - :return: Dictionary with codec interface settings + :getter: Return dictionary with codec interface settings """ return self._page0._get_codec_interface() @@ -1746,7 +1747,7 @@ class TLV320DAC3100: def headphone_shorted(self) -> bool: """Check if headphone short circuit is detected. - :return: True if headphone is shorted, False otherwise + :getter: Return True if headphone is shorted, False otherwise """ return self._page1._is_headphone_shorted() @@ -1754,7 +1755,7 @@ class TLV320DAC3100: def speaker_shorted(self) -> bool: """Check if speaker short circuit is detected. - :return: True if speaker is shorted, False otherwise + :getter: Return True if speaker is shorted, False otherwise """ return self._page1._is_speaker_shorted() @@ -1762,7 +1763,7 @@ class TLV320DAC3100: def hpl_gain_applied(self) -> bool: """Check if all programmed gains have been applied to HPL. - :return: True if gains are applied, False otherwise + :getter: Return True if gains are applied, False otherwise """ return self._page1._is_hpl_gain_applied() @@ -1770,7 +1771,7 @@ class TLV320DAC3100: def hpr_gain_applied(self) -> bool: """Check if all programmed gains have been applied to HPR. - :return: True if gains are applied, False otherwise + :getter: Return True if gains are applied, False otherwise """ return self._page1._is_hpr_gain_applied() @@ -1778,7 +1779,7 @@ class TLV320DAC3100: def speaker_gain_applied(self) -> bool: """Check if all programmed gains have been applied to Speaker. - :return: True if gains are applied, False otherwise + :getter: Return True if gains are applied, False otherwise """ return self._page1._is_spk_gain_applied() @@ -1786,40 +1787,41 @@ class TLV320DAC3100: def headset_status(self) -> int: """Current headset detection status. - :return: Integer value representing headset status (0=none, 1=without mic, 3=with mic) + :getter: Return Integer value representing headset status (0=none, + 1=without mic, 3=with mic) """ return self._page0._get_headset_status() @property def reset_speaker_on_scd(self) -> bool: - """The speaker reset behavior on short circuit detection. + """The speaker reset mode for short circuit detection. - :return: True if speaker resets on short circuit, False otherwise + True if speaker resets on short circuit, False otherwise. + + :getter: Return mode + :setter: Set mode """ value = self._page1._read_register(_HP_SPK_ERR_CTL) return not bool((value >> 1) & 0x01) @reset_speaker_on_scd.setter def reset_speaker_on_scd(self, reset: bool) -> None: - """ - :param reset: True to reset speaker on short circuit, False to remain unchanged - """ self._page1._reset_speaker_on_scd(reset) @property def reset_headphone_on_scd(self) -> bool: - """The headphone reset behavior on short circuit detection. + """The headphone reset mode for short circuit detection. - :return: True if headphone resets on short circuit, False otherwise + True if headphone resets on short circuit, False otherwise. + + :getter: Return mode + :setter: Set mode """ value = self._page1._read_register(_HP_SPK_ERR_CTL) return not bool(value & 0x01) @reset_headphone_on_scd.setter def reset_headphone_on_scd(self, reset: bool) -> None: - """ - :param reset: True to reset headphone on short circuit, False to remain unchanged - """ self._page1._reset_headphone_on_scd(reset) def configure_headphone_pop( @@ -1838,24 +1840,25 @@ class TLV320DAC3100: def speaker_wait_time(self) -> int: """The current speaker power-up wait time. - :return: The wait time setting (0-7) + Speaker power-up wait duration (0-7). + + :getter: Return wait time + :setter: Set wait time """ value = self._page1._read_register(_PGA_RAMP) return (value >> 4) & 0x07 @speaker_wait_time.setter def speaker_wait_time(self, wait_time: int) -> None: - """Speaker power-up wait time. - - :param wait_time: Speaker power-up wait duration (0-7) - """ self._page1._set_speaker_wait_time(wait_time) @property def headphone_lineout(self) -> bool: """The current headphone line-out configuration. - :return: True if both channels are configured as line-out, False otherwise + :getter: Return True if both channels are configured as line-out, False + otherwise + :setter: True to configure both channels as line-out, False otherwise """ value = self._page1._read_register(_HP_DRIVER_CTRL) left = bool(value & (1 << 2)) @@ -1864,9 +1867,6 @@ class TLV320DAC3100: @headphone_lineout.setter def headphone_lineout(self, enabled: bool) -> None: - """ - :param enabled: True to configure both channels as line-out, False otherwise - """ self._page1._headphone_lineout(enabled, enabled) def config_mic_bias( @@ -1903,16 +1903,20 @@ class TLV320DAC3100: def vol_adc_pin_control(self) -> bool: """The volume ADC pin control status. - :return: True if volume ADC pin control is enabled, False otherwise + True if volume ADC pin control is enabled, False otherwise. + + This is for using an analog input pin, probably connected to a + potentiometer, to control the volume. You can ignore this if you want + to control volume from software over I2C. + + :getter: Return status + :setter: Set status """ reg_value = self._page0._read_register(_VOL_ADC_CTRL) return bool(reg_value & (1 << 7)) @vol_adc_pin_control.setter def vol_adc_pin_control(self, enabled: bool) -> None: - """ - :param enabled: True to enable volume ADC pin control, False to disable - """ current_config = self._get_vol_adc_config() self._page0._config_vol_adc( enabled, @@ -1925,16 +1929,16 @@ class TLV320DAC3100: def vol_adc_use_mclk(self) -> bool: """The volume ADC use MCLK status. - :return: True if volume ADC uses MCLK, False otherwise + True means volume ADC uses MCLK, False means internal oscillator. + + :getter: Return status + :setter: Set status """ reg_value = self._page0._read_register(_VOL_ADC_CTRL) return bool(reg_value & (1 << 6)) @vol_adc_use_mclk.setter def vol_adc_use_mclk(self, use_mclk: bool) -> None: - """ - :param use_mclk: True to use MCLK, False to use internal oscillator - """ current_config = self._get_vol_adc_config() self._page0._config_vol_adc( current_config["pin_control"], @@ -1947,16 +1951,16 @@ class TLV320DAC3100: def vol_adc_hysteresis(self) -> int: """The volume ADC hysteresis setting. - :return: Hysteresis value (0-3) + Hysteresis value (0-3). + + :getter: Return value + :setter: Set value """ reg_value = self._page0._read_register(_VOL_ADC_CTRL) return (reg_value >> 4) & 0x03 @vol_adc_hysteresis.setter def vol_adc_hysteresis(self, hysteresis: int) -> None: - """ - :param hysteresis: Hysteresis value (0-3) - """ current_config = self._get_vol_adc_config() self._page0._config_vol_adc( current_config["pin_control"], @@ -1969,17 +1973,16 @@ class TLV320DAC3100: def vol_adc_rate(self) -> int: """The volume ADC sampling rate. - :return: Rate value (0-7) + Rate value (0-7). + + :getter: Return value + :setter: Set value """ reg_value = self._page0._read_register(_VOL_ADC_CTRL) return reg_value & 0x07 @vol_adc_rate.setter def vol_adc_rate(self, rate: int) -> None: - """ - - :param rate: Rate value (0-7) - """ current_config = self._get_vol_adc_config() self._page0._config_vol_adc( current_config["pin_control"], @@ -2005,7 +2008,7 @@ class TLV320DAC3100: def vol_adc_db(self) -> float: """The current volume from the Volume ADC in dB. - :return: Volume in dB + :getter: Return Volume in dB """ return self._page0._read_vol_adc_db() @@ -2060,9 +2063,13 @@ class TLV320DAC3100: @property def headphone_output(self) -> bool: """Headphone output helper with quickstart settings for users. - Headphone output state (True if either left or right channel is powered). - :return: True if headphone output is enabled, False otherwise + :getter: Return headphone output state: True if either left or right + headphone amplifier is powered, False otherwise. + :setter: **This sets several properties to prepare for headphone use**. + Changed properties include DAC channel enable/volume/mute, DAC + path, headphone gain, headphone common mode voltage, and headphone + mute. """ hp_drivers = self._page1._read_register(_HP_DRIVERS) left_powered = bool(hp_drivers & (1 << 7)) @@ -2071,9 +2078,12 @@ class TLV320DAC3100: @headphone_output.setter def headphone_output(self, enabled: bool) -> None: - """ - :param enabled: True to enable headphone output, False to disable - """ + # ========= + # TODO: Consider if this should be changed to a regular function since + # it modifies many properties. Note how the getter above only + # checks the amplifier enable status but this setter does a bunch + # of other stuff. + # ========= if enabled: self.left_dac = True self.right_dac = True @@ -2088,6 +2098,9 @@ class TLV320DAC3100: self._page1._configure_headphone_driver( left_powered=True, right_powered=True, common=HP_COMMON_1_65V ) + # ======== + # TODO: Should probably set self.headphone_volume (-10? -20?) + # ======== self._page1._configure_analog_inputs(left_dac=DAC_ROUTE_HP, right_dac=DAC_ROUTE_HP) self.headphone_left_mute = False self.headphone_right_mute = False @@ -2097,17 +2110,23 @@ class TLV320DAC3100: @property def speaker_output(self) -> bool: """Speaker output helper with quickstart settings for users. - Speaker output state. - :return: True if speaker output is enabled, False otherwise + :getter: Return speaker output state: True if speaker amplifier is + powered, False otherwise. + :setter: **This sets several properties to prepare for speaker use**. + Changed properties include DAC channel enable/volume/mute, DAC + path, speaker volume, speaker amplifier gain, and speaker mute. """ return self._page1._get_speaker_enabled() @speaker_output.setter def speaker_output(self, enabled: bool) -> None: - """ - :param enabled: True to enable speaker, False to disable - """ + # ========= + # TODO: Consider if this should be changed to a regular function since + # it modifies many properties. Note how the getter above only + # checks the amplifier enable status but this setter does a bunch + # of other stuff. + # ========= if enabled: self.left_dac = True self.right_dac = True @@ -2136,12 +2155,14 @@ class TLV320DAC3100: This acts on two registers at once. In the datasheet they are: * Page 1 / Register 36 (0x24): Left Analog Volume to HPL - * Page 1 / Register 37 (0x25) Right Analog Volume to HPR Note that headphone output is also affected by ``dac_volume``\, ``headphone_left_gain``\, ``headphone_right_gain``\, ``headphone_left_mute``\, and ``headphone_right_mute``\. + + :getter: Return volume + :setter: Set volume """ left_gain_u7 = self._page1._read_register(_HPL_VOL) & 0x7F right_gain_u7 = self._page1._read_register(_HPR_VOL) & 0x7F @@ -2170,6 +2191,9 @@ class TLV320DAC3100: Note that ``dac_volume``\, ``speaker_gain``\, and ``speaker_mute`` also affect the speaker output level. + + :getter: Return volume + :setter: Set volume """ gain_u7 = self._page1._read_register(_SPK_VOL) & 0x7F return _table_6_24_uint7_to_db(gain_u7) @@ -2184,7 +2208,7 @@ class TLV320DAC3100: def sample_rate(self) -> int: """Configured sample rate in Hz. - :return: The sample rate in Hz + :getter: Return The sample rate in Hz """ return self._sample_rate @@ -2192,7 +2216,7 @@ class TLV320DAC3100: def bit_depth(self) -> int: """Configured bit depth. - :return: The bit depth + :getter: Return The bit depth """ return self._bit_depth @@ -2200,6 +2224,6 @@ class TLV320DAC3100: def mclk_freq(self) -> int: """Configured MCLK frequency in Hz. - :return: The MCLK frequency in Hz + :getter: Return The MCLK frequency in Hz """ return self._mclk_freq From 76d1cad3f4f82d30f386dd1c098b7de1b7920c15 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Mon, 25 Aug 2025 00:21:28 +0000 Subject: [PATCH 05/10] fix Pylance problems Mostly these were type annotations referring to non-existant types. There was also a misspelled function name. --- adafruit_tlv320.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 83ae716..cf134c7 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -609,7 +609,7 @@ class _Page0Registers(_PagedRegisterBase): "wclk_out": wclk_out, } - def _get_dac_data_path(self): + def _get_dac_data_path(self) -> dict: """The current DAC data path configuration. :return: Dictionary with DAC data path settings @@ -629,7 +629,7 @@ class _Page0Registers(_PagedRegisterBase): "volume_step": volume_step, } - def _get_dac_volume_control(self): + def _get_dac_volume_control(self) -> dict: """The current DAC volume control configuration. :return: Dictionary with volume control settings @@ -1083,9 +1083,9 @@ class TLV320DAC3100: self._device: I2CDevice = I2CDevice(i2c, address) # Initialize register page classes - self._page0: "Page0Registers" = _Page0Registers(self._device) - self._page1: "Page1Registers" = _Page1Registers(self._device) - self._page3: "Page3Registers" = _Page3Registers(self._device) + self._page0: "_Page0Registers" = _Page0Registers(self._device) + self._page1: "_Page1Registers" = _Page1Registers(self._device) + self._page3: "_Page3Registers" = _Page3Registers(self._device) self._sample_rate: int = 44100 self._bit_depth: int = 16 self._mclk_freq: int = 0 # Default blck @@ -1198,7 +1198,7 @@ class TLV320DAC3100: @left_dac.setter def left_dac(self, enabled: bool) -> None: - current: DACDataPath = self._page0._get_dac_data_path() + current: dict = self._page0._get_dac_data_path() self._page0._set_dac_data_path( enabled, current["right_dac_on"], @@ -1220,7 +1220,7 @@ class TLV320DAC3100: @right_dac.setter def right_dac(self, enabled: bool) -> None: - current: DACDataPath = self._page0._get_dac_data_path() + current: dict = self._page0._get_dac_data_path() self._page0._set_dac_data_path( current["left_dac_on"], enabled, @@ -1250,7 +1250,7 @@ class TLV320DAC3100: f"Invalid DAC path value: {path}. Must be one of the DAC_PATH_* constants." ) - current: DACDataPath = self._page0._get_dac_data_path() + current: dict = self._page0._get_dac_data_path() self._page0._set_dac_data_path( current["left_dac_on"], current["right_dac_on"], @@ -1280,7 +1280,7 @@ class TLV320DAC3100: f"Invalid DAC path value: {path}. Must be one of the DAC_PATH_* constants." ) - current: DACDataPath = self._page0._get_dac_data_path() + current: dict = self._page0._get_dac_data_path() self._page0._set_dac_data_path( current["left_dac_on"], current["right_dac_on"], @@ -1310,7 +1310,7 @@ class TLV320DAC3100: f"Invalid volume step value: {step}. Must be one of the VOLUME_STEP_* constants." ) - current: DACDataPath = self._page0._get_dac_data_path() + current: dict = self._page0._get_dac_data_path() self._page0._set_dac_data_path( current["left_dac_on"], current["right_dac_on"], @@ -1368,7 +1368,7 @@ class TLV320DAC3100: @left_dac_mute.setter def left_dac_mute(self, mute: bool) -> None: - current: DACVolumeControl = self._page0._get_dac_volume_control() + current: dict = self._page0._get_dac_volume_control() self._page0._set_dac_volume_control(mute, current["right_mute"], current["control"]) @property @@ -1384,7 +1384,7 @@ class TLV320DAC3100: @right_dac_mute.setter def right_dac_mute(self, mute: bool) -> None: - current: DACVolumeControl = self._page0._get_dac_volume_control() + current: dict = self._page0._get_dac_volume_control() self._page0._set_dac_volume_control(current["left_mute"], mute, current["control"]) @property @@ -1406,7 +1406,7 @@ class TLV320DAC3100: raise ValueError( f"Invalid volume control mode: {mode}. Must be one of the VOL_* constants." ) - current: DACVolumeControl = self._page0._get_dac_volume_control() + current: dict = self._page0._get_dac_volume_control() self._page0._set_dac_volume_control(current["left_mute"], current["right_mute"], mode) @property @@ -2201,7 +2201,7 @@ class TLV320DAC3100: @speaker_volume.setter def speaker_volume(self, db: float) -> None: # The table 6-24 lookup function includes min/max range clipping - gain_u7 = _table_6_24_uint7_to_dB(db) + gain_u7 = _table_6_24_uint7_to_db(db) self._page1._set_spk_volume(route_enabled=True, gain=gain_u7) @property From 522f615ffea480fcf5eb9dbda3eca5a326dca633 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Mon, 25 Aug 2025 03:34:03 +0000 Subject: [PATCH 06/10] fix silly bugs from blind edits This is what needed fixing when I actually started running some Fruit Jam test code with the headphone output. Haven't tried the speaker yet. --- adafruit_tlv320.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index cf134c7..5f4dfb5 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -349,7 +349,7 @@ TABLE_6_24 = ( ) -def _table_6_24_db_to_uint7(self, db: float) -> int: +def _table_6_24_db_to_uint7(db: float) -> int: """Convert gain dB to 7-bit unsigned int following datasheet Table 6-24. :param db: Analog gain in dB; range is 0 dB (loud) to -78.3 dB (soft) @@ -371,7 +371,7 @@ def _table_6_24_db_to_uint7(self, db: float) -> int: return result -def _table_6_24_uint7_to_db(self, u7: int) -> float: +def _table_6_24_uint7_to_db(u7: int) -> float: """Convert 7-bit unsigned int to gain dB following datasheet Table 6-24. :param u7: 7-bit unsigned int value, range is 0 (loud) to 127 (soft) @@ -951,7 +951,7 @@ class _Page1Registers(_PagedRegisterBase): """ if gain_db not in set((6, 12, 18, 24)): raise ValueError(f"Invalid speaker gain: {gain_db}. Must be 6, 12, 18, or 24.") - uint2_val = (gain_db / 6) - 1 + uint2_val = int((gain_db / 6) - 1) value = (uint2_val & 0x03) << 3 if unmute: value |= 1 << 2 From 12d09409be605bcc14b875596793033f1ec3da53 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Mon, 25 Aug 2025 04:44:02 +0000 Subject: [PATCH 07/10] fix headphone route, dial in default gain This fixes the headphone_output property setter to use the DAC_ROUTE_MIXER option so the headphone_volume attenuation stage doesn't get bypassed (as it did previously). This makes it a lot easier to set non-ear-bleedy volume levels for headphones. --- adafruit_tlv320.py | 60 ++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 5f4dfb5..d670142 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -2062,7 +2062,17 @@ class TLV320DAC3100: @property def headphone_output(self) -> bool: - """Headphone output helper with quickstart settings for users. + """Headphone output helper with quickstart default settings. + + If you set this property to True, the setter will set defaults that + are intended for listening at a quiet-ish level with sensitive low + impedance earbuds: + + * dac_volume = -30 + * headphone_volume = -42.1 + * headphone_left_gain = headphone_right_gain = 0 + + If you set this to False, the setter turns off the headphone amp. :getter: Return headphone output state: True if either left or right headphone amplifier is powered, False otherwise. @@ -2078,17 +2088,11 @@ class TLV320DAC3100: @headphone_output.setter def headphone_output(self, enabled: bool) -> None: - # ========= - # TODO: Consider if this should be changed to a regular function since - # it modifies many properties. Note how the getter above only - # checks the amplifier enable status but this setter does a bunch - # of other stuff. - # ========= if enabled: self.left_dac = True self.right_dac = True - self.left_dac_channel_volume = 0 - self.right_dac_channel_volume = 0 + self.left_dac_channel_volume = -30 + self.right_dac_channel_volume = -30 self.left_dac_mute = False self.right_dac_mute = False self.left_dac_path = DAC_PATH_NORMAL @@ -2098,10 +2102,18 @@ class TLV320DAC3100: self._page1._configure_headphone_driver( left_powered=True, right_powered=True, common=HP_COMMON_1_65V ) - # ======== - # TODO: Should probably set self.headphone_volume (-10? -20?) - # ======== - self._page1._configure_analog_inputs(left_dac=DAC_ROUTE_HP, right_dac=DAC_ROUTE_HP) + self.headphone_volume = -42.1 + # NOTE: If you use DAC_ROUTE_HP here instead of DAC_ROUTE_MIXER, + # the DAC output will bypass the headphone analog volume + # attenuation stage and go straight into the headphone amp. That + # might possibly be useful to save power, but it reduces your gain + # adjustment options. For low impedance headphones, it's helpful to + # have a lot of attenuation between the DAC and the headphone amp. + # Otherwise, you may have to operate the DAC volume setting down + # near the bottom of its usable range. + self._page1._configure_analog_inputs( + left_dac=DAC_ROUTE_MIXER, right_dac=DAC_ROUTE_MIXER + ) self.headphone_left_mute = False self.headphone_right_mute = False else: @@ -2109,7 +2121,15 @@ class TLV320DAC3100: @property def speaker_output(self) -> bool: - """Speaker output helper with quickstart settings for users. + """Speaker output helper with quickstart default settings. + + If you set this property to True, the setter will set: + + * dac_volume = -30 + * speaker_volume = -42.1 + * speaker_gain = 6 + + If you set this to False, the setter turns off the speaker amp. :getter: Return speaker output state: True if speaker amplifier is powered, False otherwise. @@ -2121,17 +2141,11 @@ class TLV320DAC3100: @speaker_output.setter def speaker_output(self, enabled: bool) -> None: - # ========= - # TODO: Consider if this should be changed to a regular function since - # it modifies many properties. Note how the getter above only - # checks the amplifier enable status but this setter does a bunch - # of other stuff. - # ========= if enabled: self.left_dac = True self.right_dac = True - self.left_dac_channel_volume = 0 - self.right_dac_channel_volume = 0 + self.left_dac_channel_volume = -30 + self.right_dac_channel_volume = -30 self.left_dac_mute = False self.right_dac_mute = False self.left_dac_path = DAC_PATH_NORMAL @@ -2141,7 +2155,7 @@ class TLV320DAC3100: self._page1._configure_analog_inputs( left_dac=DAC_ROUTE_MIXER, right_dac=DAC_ROUTE_MIXER ) - self.speaker_volume = -20 + self.speaker_volume = -42.1 self.speaker_mute = False else: self._page1._set_speaker_enabled(False) From bd24ba5e46b16766a011863b7ffd8d92666b2388 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Mon, 25 Aug 2025 06:47:57 +0000 Subject: [PATCH 08/10] fix speaker_volume & tune gain defaults This fixes a bug that I found while testing the speaker output. I also balanced the default speaker_volume and headphone_volume gain levels so they sound about the same loudness (to me, with my earbuds). That way you can use dac_volume for runtime volume adjustments and it will hopefully work about the same for the speaker or the headphones. To set the board up for different headphones or speakers, you could experiment with suitable values for speaker_volume and headphone_volume to set in an initialization function, like a trimpot. After that, you could use dac_volume to set volume. --- adafruit_tlv320.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index d670142..44f6b73 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -2068,8 +2068,8 @@ class TLV320DAC3100: are intended for listening at a quiet-ish level with sensitive low impedance earbuds: - * dac_volume = -30 - * headphone_volume = -42.1 + * dac_volume = -20 + * headphone_volume = -51.8 * headphone_left_gain = headphone_right_gain = 0 If you set this to False, the setter turns off the headphone amp. @@ -2091,8 +2091,8 @@ class TLV320DAC3100: if enabled: self.left_dac = True self.right_dac = True - self.left_dac_channel_volume = -30 - self.right_dac_channel_volume = -30 + self.left_dac_channel_volume = -20 + self.right_dac_channel_volume = -20 self.left_dac_mute = False self.right_dac_mute = False self.left_dac_path = DAC_PATH_NORMAL @@ -2102,7 +2102,7 @@ class TLV320DAC3100: self._page1._configure_headphone_driver( left_powered=True, right_powered=True, common=HP_COMMON_1_65V ) - self.headphone_volume = -42.1 + self.headphone_volume = -52.8 # NOTE: If you use DAC_ROUTE_HP here instead of DAC_ROUTE_MIXER, # the DAC output will bypass the headphone analog volume # attenuation stage and go straight into the headphone amp. That @@ -2123,10 +2123,12 @@ class TLV320DAC3100: def speaker_output(self) -> bool: """Speaker output helper with quickstart default settings. - If you set this property to True, the setter will set: + If you set this property to True, the setter will set defaults intended + for a relatively quiet listening level using the 8Ω 1W mini speaker + that comes bundled with the Fruit Jam: - * dac_volume = -30 - * speaker_volume = -42.1 + * dac_volume = -20 + * speaker_volume = -20.1 * speaker_gain = 6 If you set this to False, the setter turns off the speaker amp. @@ -2144,8 +2146,8 @@ class TLV320DAC3100: if enabled: self.left_dac = True self.right_dac = True - self.left_dac_channel_volume = -30 - self.right_dac_channel_volume = -30 + self.left_dac_channel_volume = -20 + self.right_dac_channel_volume = -20 self.left_dac_mute = False self.right_dac_mute = False self.left_dac_path = DAC_PATH_NORMAL @@ -2155,7 +2157,7 @@ class TLV320DAC3100: self._page1._configure_analog_inputs( left_dac=DAC_ROUTE_MIXER, right_dac=DAC_ROUTE_MIXER ) - self.speaker_volume = -42.1 + self.speaker_volume = -20.1 self.speaker_mute = False else: self._page1._set_speaker_enabled(False) @@ -2215,7 +2217,7 @@ class TLV320DAC3100: @speaker_volume.setter def speaker_volume(self, db: float) -> None: # The table 6-24 lookup function includes min/max range clipping - gain_u7 = _table_6_24_uint7_to_db(db) + gain_u7 = _table_6_24_db_to_uint7(db) self._page1._set_spk_volume(route_enabled=True, gain=gain_u7) @property From 81c00085ec92b4ce04bf7b5c081d44f35db3d3e7 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:04:27 +0000 Subject: [PATCH 09/10] remove doc-comment backslash escapes I added these several commits back because they seemed to resolve a weird Sphinx code-block rendering bug in the html docs. But, now I can't reproduce the bug, so there's no reason to keep the escapes. --- adafruit_tlv320.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 44f6b73..62c91c5 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -36,7 +36,7 @@ Implementation Notes start with lower levels for ``speaker_volume`` and ``speaker_gain``, then work your way up to find a comfortable listening level. Similarly, for the headphone output, start low with ``headphone_volume``, - ``headphone_left_gain``\, and ``headphone_right_gain``\, then increase as + ``headphone_left_gain``, and ``headphone_right_gain``, then increase as needed. **Software and Dependencies:** @@ -1481,10 +1481,10 @@ class TLV320DAC3100: Changing the DAC volume will change the signal level feeding into the analog signal chains of the speaker and both headphone channels. You - should also be aware of ``speaker_volume``\, ``speaker_gain``\, - ``speaker_mute``\, ``headphone_volume``\, ``headphone_left_gain``\, - ``headphone_right_gain``\, ``headphone_left_mute``\, and - ``headphone_right_mute``\. + should also be aware of ``speaker_volume``, ``speaker_gain``, + ``speaker_mute``, ``headphone_volume``, ``headphone_left_gain``, + ``headphone_right_gain``, ``headphone_left_mute``, and + ``headphone_right_mute``. :getter: Return volume :setter: Set volume @@ -1567,7 +1567,7 @@ class TLV320DAC3100: In the datasheet, this is Page 1 / Register 40 (0x28): HPL Driver. Note that the headphone left channel volume is also affected by - ``dac_volume``\, ``headphone_volume``\, and ``headphone_left_mute``\. + ``dac_volume``, ``headphone_volume``, and ``headphone_left_mute``. :getter: Return gain :setter: Set gain @@ -1609,7 +1609,7 @@ class TLV320DAC3100: In the datasheet, this is Page 1 / Register 41 (0x29): HPR Driver. Note that the headphone right channel volume is also affected by - ``dac_volume``\, ``headphone_volume``\, and ``headphone_right_mute``\. + ``dac_volume``, ``headphone_volume``, and ``headphone_right_mute``. :getter: Return gain :setter: Set gain @@ -1650,7 +1650,7 @@ class TLV320DAC3100: In the datasheet, this is Page 1 / Register 42 (0x2A): Class-D Speaker (SPK) Driver. - Note that ``dac_volume``\, ``speaker_volume``\, and ``speaker_mute`` + Note that ``dac_volume``, ``speaker_volume``, and ``speaker_mute`` also affect the speaker output level. :getter: Return gain @@ -2173,9 +2173,9 @@ class TLV320DAC3100: * Page 1 / Register 36 (0x24): Left Analog Volume to HPL * Page 1 / Register 37 (0x25) Right Analog Volume to HPR - Note that headphone output is also affected by ``dac_volume``\, - ``headphone_left_gain``\, ``headphone_right_gain``\, - ``headphone_left_mute``\, and ``headphone_right_mute``\. + Note that headphone output is also affected by ``dac_volume``, + ``headphone_left_gain``, ``headphone_right_gain``, + ``headphone_left_mute``, and ``headphone_right_mute``. :getter: Return volume :setter: Set volume @@ -2205,7 +2205,7 @@ class TLV320DAC3100: In the datasheet, this is Page 1 / Register 38 (0x26): Left Analog Volume to SPK. - Note that ``dac_volume``\, ``speaker_gain``\, and ``speaker_mute`` also + Note that ``dac_volume``, ``speaker_gain``, and ``speaker_mute`` also affect the speaker output level. :getter: Return volume From bc20f0b3c0ec38a0478eeeb5cc52b8290782b363 Mon Sep 17 00:00:00 2001 From: sam blenny <68084116+samblenny@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:17:03 +0000 Subject: [PATCH 10/10] more docs, mostly usage examples Hopefully, this should make it clear how to use the API for setting volume for speakers, headphones, or line-level output. --- adafruit_tlv320.py | 79 ++++++++++++++-- docs/examples.rst | 9 ++ examples/tlv320_volumetest.py | 171 ++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 examples/tlv320_volumetest.py diff --git a/adafruit_tlv320.py b/adafruit_tlv320.py index 62c91c5..78722dd 100644 --- a/adafruit_tlv320.py +++ b/adafruit_tlv320.py @@ -8,7 +8,7 @@ CircuitPython driver for the TLV320DAC3100 I2S DAC -* Author(s): Liz Clark +* Author(s): Liz Clark, Sam Blenny Implementation Notes -------------------- @@ -24,20 +24,19 @@ Implementation Notes left, and headphone right start with the DAC, then they go through a mixer stage, an analog volume (attenuation) stage, and finally an analog amplifier stage. Parameters for each stage of each signal chain can be separately set - with different properties. + with different properties. But, you can ignore most of that if you use + ``speaker_output = True`` or ``headphone_output = True`` to load defaults. * To understand how the different audio stages (DAC, volume, amplifier gain) relate to each other, it can help to look at the Functional Block Diagram in the TLV320DAC3100 datasheet: https://learn.adafruit.com/adafruit-tlv320dac3100-i2s-dac/downloads -* **CAUTION**: The TLV320 speaker amplifier has enough power to easily burn out - small 1W speakers if you max out the volume and gain settings. To be safe, - start with lower levels for ``speaker_volume`` and ``speaker_gain``, then work - your way up to find a comfortable listening level. Similarly, for the - headphone output, start low with ``headphone_volume``, - ``headphone_left_gain``, and ``headphone_right_gain``, then increase as - needed. +* **CAUTION**: The TLV320 amplifiers have enough power to easily burn out + small 1W speakers or drive headphones to levels that could damage your + hearing. To be safe, start with low volume and gain levels, then increase + them carefully to find a comfortable listening level. This is why the + default levels set by speaker_output and headphone_output are relatively low. **Software and Dependencies:** @@ -45,6 +44,68 @@ Implementation Notes https://circuitpython.org/downloads * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice + +Usage Examples +-------------- + +Fruit Jam Mini-Speaker +^^^^^^^^^^^^^^^^^^^^^^ + +This will start you off with a relatively low volume for the Fruit Jam's +bundled 8-Ohm 1 Watt speaker. Your code can adjust the volume by increasing +or decreasing ``dac_volume``. To use a higher wattage speaker that needs +more power, you might want to increase ``speaker_volume``. + +:: + + dac = TLV320DAC3100(board.I2C()) + dac.speaker_output = True # set defaults for speaker + dac.dac_volume = dac.dac_volume + 1 # increase volume by 1 dB + dac.dac_volume = dac.dac_volume - 1 # decrease volume by 1 dB + +Low Impedance Earbuds +^^^^^^^^^^^^^^^^^^^^^ + +This will start you off with a relatively low volume for low impedance +earbuds (e.g. JVC Gumy) plugged into the Fruit Jam's headphone jack. Your +code can adjust the volume by increasing or decreasing ``dac_volume``. To +use high impedance headphones that need more power, you might want to +increase ``headphone_volume``. + +:: + + dac = TLV320DAC3100(board.I2C()) + dac.speaker_output = False # make sure speaker amp is off + dac.headphone_output = True # set defaults for headphones + dac.dac_volume = dac.dac_volume + 1 # increase volume by 1 dB + dac.dac_volume = dac.dac_volume - 1 # decrease volume by 1 dB + +Line Level Output to Mixer +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For this one, the default headphone output volume will be way too low for +use with a device that expects consumer line level input (-10 dBV). To fix +that, you can increase ``dac_volume`` or ``headphone_volume``. If you want +to experiment with different ways of setting the levels, check out the +volume test example: `Volume test <../examples.html#volume-test>`_ + +:: + + dac = TLV320DAC3100(board.I2C()) + dac.speaker_output = False # make sure speaker amp is off + dac.headphone_output = True # set defaults for headphones (note: too low!) + + # Make it louder by increasing headphone_volume. We could also use + # dac_volume, but doing it this way gives a better balance between + # the speaker signal chain and the headphone jack signal chain. (think + # of headphone_volume as a mixer channel's pad switch or gain trim knob + # and dac_volume as the main volume control fader) + # + # CAUTION: This will be *way* too loud for earbuds, please be careful! + dac.headphone_volume = -15.5 # default is -51.8 dB + +API +--- """ import time diff --git a/docs/examples.rst b/docs/examples.rst index fd84618..0744ae1 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -15,3 +15,12 @@ Demos advanced features of the library. .. literalinclude:: ../examples/tlv320_fulltest.py :caption: examples/tlv320_fulltest.py :linenos: + +Volume test +----------- + +Test tone generator with interactive serial console volume controls + +.. literalinclude:: ../examples/tlv320_volumetest.py + :caption: examples/tlv320_volumetest.py + :linenos: diff --git a/examples/tlv320_volumetest.py b/examples/tlv320_volumetest.py new file mode 100644 index 0000000..77fef5d --- /dev/null +++ b/examples/tlv320_volumetest.py @@ -0,0 +1,171 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny +# +import gc +import os +import sys +import time + +import displayio +import supervisor +import synthio +from audiobusio import I2SOut +from board import I2C, I2S_BCLK, I2S_DIN, I2S_MCLK, I2S_WS, PERIPH_RESET +from digitalio import DigitalInOut, Direction, Pull +from micropython import const + +from adafruit_tlv320 import TLV320DAC3100 + +# DAC and Synthesis parameters +SAMPLE_RATE = const(11025) +CHAN_COUNT = const(2) +BUFFER_SIZE = const(1024) + +# DAC volume limits +DV_MIN = -63.5 +DV_MAX = 24.0 + +# Headphone volume limits +HV_MIN = -78.3 +HV_MAX = 0 + +# Headphone gain limits +HG_MIN = 0 +HG_MAX = 9 + +# Speaker volume limits +SV_MIN = -78.3 +SV_MAX = 0 + +# Speaker amp gain limits +SG_MIN = 6 +SG_MAX = 24 +SG_STEP = 6 + + +def init_dac_audio_synth(i2c): + """Configure TLV320 I2S DAC for audio output and make a Synthesizer. + + :param i2c: a reference to board.I2C() + :return: tuple(dac: TLV320DAC3100, audio: I2SOut, synth: Synthesizer) + """ + # 1. Reset DAC (reset is active low) + rst = DigitalInOut(PERIPH_RESET) + rst.direction = Direction.OUTPUT + rst.value = False + time.sleep(0.1) + rst.value = True + time.sleep(0.05) + # 2. Configure sample rate, bit depth, and output port + dac = TLV320DAC3100(i2c) + dac.configure_clocks(sample_rate=SAMPLE_RATE, bit_depth=16) + dac.speaker_output = True + dac.headphone_output = True + # 4. Initialize I2S for Fruit Jam rev D + audio = I2SOut(bit_clock=I2S_BCLK, word_select=I2S_WS, data=I2S_DIN) + # 5. Configure synthio patch to generate audio + vca = synthio.Envelope( + attack_time=0, decay_time=0, sustain_level=1.0, release_time=0, attack_level=1.0 + ) + synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE, channel_count=CHAN_COUNT, envelope=vca) + return (dac, audio, synth) + + +def main(): # noqa: PLR0912, PLR0915, allow long function and long if statement + # Turn off the default DVI display to free up CPU + displayio.release_displays() + gc.collect() + + # Set up the audio stuff for a basic synthesizer + i2c = I2C() + (dac, audio, synth) = init_dac_audio_synth(i2c) + audio.play(synth) + + dv = dac.dac_volume # default DAC volume + hv = dac.headphone_volume # default headphone analog volume + hg = dac.headphone_left_gain # default headphone amp gain + sv = dac.speaker_volume # default speaker analog volume + sg = dac.speaker_gain # default speaker amp gain + note = 60 + synth.press(note) + + # Check for unbuffered keystroke input on the USB serial console + print(""" +=== TLV320DAC Volume Tester === + +Controls: + q/z: dac_volume +/- 1 + w/x: headphone_volume +/- 1 + e/c: headphone_left_gain headphone_right_gain +/- 1 + r/v: speaker_volume +/- 1 + t/b: speaker_gain +/- 6 + space: toggle speaker_output (amp power), this will reset volume & gain + +For less headphone noise, turn off the speaker amp (spacebar) +""") + while True: + time.sleep(0.01) + if supervisor.runtime.serial_bytes_available: + while supervisor.runtime.serial_bytes_available: + c = sys.stdin.read(1) + if c == "q": + # Q = DAC Volume UP + dv = min(DV_MAX, max(DV_MIN, dv + 1)) + dac.dac_volume = dv + print(f"dv = {dv:.1f} ({dac.dac_volume:.1f})") + elif c == "z": + # Z = DAC Volume DOWN + dv = min(DV_MAX, max(DV_MIN, dv - 1)) + dac.dac_volume = dv + print(f"dv = {dv:.1f} ({dac.dac_volume:.1f})") + elif c == "w": + # W = Headphone Volume UP + hv = min(HV_MAX, max(HV_MIN, hv + 1)) + dac.headphone_volume = hv + print(f"hv = {hv:.1f} ({dac.headphone_volume:.1f})") + elif c == "x": + # X = Headphone Volume DOWN + hv = min(HV_MAX, max(HV_MIN, hv - 1)) + dac.headphone_volume = hv + print(f"hv = {hv:.1f} ({dac.headphone_volume:.1f})") + elif c == "e": + # E = Headphone Amp Gain UP + hg = min(HG_MAX, max(HG_MIN, hg + 1)) + dac.headphone_left_gain = hg + dac.headphone_right_gain = hg + print(f"hg = {hg:.1f} ({dac.headphone_left_gain})") + elif c == "c": + # C = Headphone Amp Gain DOWN + hg = min(HG_MAX, max(HG_MIN, hg - 1)) + dac.headphone_left_gain = hg + dac.headphone_right_gain = hg + print(f"hg = {hg:.1f} ({dac.headphone_left_gain})") + + if c == "r": + # R = Speaker Volume UP + sv = min(SV_MAX, max(SV_MIN, sv + 1)) + dac.speaker_volume = sv + print(f"sv = {sv:.1f} ({dac.speaker_volume:.1f})") + elif c == "v": + # V = Speaker Volume DOWN + sv = min(SV_MAX, max(SV_MIN, sv - 1)) + dac.speaker_volume = sv + print(f"sv = {sv:.1f} ({dac.speaker_volume:.1f})") + elif c == "t": + # T = Speaker Amp Gain UP + sg = min(SG_MAX, max(SG_MIN, sg + SG_STEP)) + dac.speaker_gain = sg + print(f"sg = {sg:.1f} ({dac.speaker_gain})") + elif c == "b": + # B = Speaker Amp Gain DOWN + sg = min(SG_MAX, max(SG_MIN, sg - SG_STEP)) + dac.speaker_gain = sg + print(f"sg = {sg:.1f} ({dac.speaker_gain})") + elif c == " ": + # Space = Toggle speaker amp enable/disable + en = not dac.speaker_output + dac.speaker_output = en + print(f"speaker_output = {en}") + + +main()