Skip to content

Conversation

samblenny
Copy link
Contributor

@samblenny samblenny commented Aug 24, 2025

This PR is meant to deal with volume and gain setting problems for speakers and headphones, as discussed in Volume setting properties don't work right # 9.

This makes several interrelated changes with the goals of:

  1. Fix the broken analog volume control setters and getters to use Table 6-24 from the datasheet
  2. Improve the documentation comments for properties related to volume, gain, and muting of the headphone and speaker signal chains.
  3. Merge some of the setter and getter documentation comments to account for the fact that Sphinx totally ignores the comments on the setters when it renders the html docs. Anything important needs to go in the getter's comment.
  4. Smooth out some rough edges in the API that would have made for really awkward documentation. In particular, the speaker_gain property used different data types for the setter and getter, and the setter was using undocumented constants.

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.
@samblenny samblenny changed the title Overhaul Volume Control Imlementation Overhaul Volume Control Implementation Aug 24, 2025
Copy link

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One q re backslash-escape commas.

You can put the methods in any order you want: they don't have to be in alphabetical order. So you could put the more general methods first and the lower-level ones later, or put all the headphone ones together, etc.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the ,s backslash-escaped here and elsewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Sphinx parser is weird. Without the backslash escaped punctuation, it can potentially do a greedy capture that puts stuff which should be between two code blocks on the inside of a single code block. You can also put a space between the double backtick and the punctuation, but then it looks weird to have extra space in front of the comma.

Copy link

@dhalbert dhalbert Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not seen this myself, hmm. Do you mean the closing double-tick does not close the opening double-tick if followed by punctuation? There are a lot of examples of a closing double-tick followed by a comma in the libraries that don't exhibit a problem. Could you give a specific example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes exactly. In the code you highlighted above, it was rendering as (approximately, working from memory here):
headphone_left_gain`, and ``headphone_right_gain

If this is an issue I can just remove all such references in the documentation. That stuff is less important than I'd thought originally. Now that the speaker_output = True and headphone_output = True stuff is working well, those are definitely the properties that people should use first. Those combined with dac_volume should handle most of what people would need.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the weirdness to trigger, it may require that both of the code blocks are on the same line (no intervening linebreak). I did not see any merging of code blocks across linebreaks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it makes any difference, I've been using sphinx-build version 8.0.2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some kind of non-deterministic chaos at play here. I tried removing the backslashes and now I can't reproduce the thing I was seeing yesterday. The trigger conditions must be more obscure. If you want a commit without the backslash escapes, just let me know.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go ahead and remove them, and then we'll look at the release output and fix it if necessary. Might have been a weird typo. I looked at the sphinx-doc issues and could not find any related issues newer than several years old.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done: 81c0008

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.
This gets ruff to stop complaining. Haven't tested the code yet
though. That's next.
@samblenny
Copy link
Contributor Author

Current status:

  • HTML docs are looking pretty good.
  • Need to test headphone_volume and speaker_volume changes. I tested the Table 6-24 conversion functions separately, but now I need to find out if I broke anything while integrating that code into this library.
  • Need to test speaker_gain changes.

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
Mostly these were type annotations referring to non-existant types.
There was also a misspelled function name.
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.
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.
@samblenny
Copy link
Contributor Author

samblenny commented Aug 25, 2025

Current Status:

  • Headphone config with TLV320DAC3100.headphone_output = True now works well. I fixed the signal routing to go through the mixer, so now the headphone_volume property actually works. Also dialed in some default volume and gain settings for reasonable volume on the low impedance earbuds I usually test with.
  • Speaker settings still need testing

The code I used for testing volume adjustments on a Fruit Jam rev D is available below or at:
samblenny/tlv320dac-stuff/code.py

from audiobusio import I2SOut
from board import (
    I2C, I2S_BCLK, I2S_DIN, I2S_MCLK, I2S_WS, PERIPH_RESET
)
from digitalio import DigitalInOut, Direction, Pull
import displayio
import gc
from micropython import const
import os
import synthio
import sys
import supervisor
import time
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

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 = False
    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
    )
    audio.play(synth)
    return (dac, audio, synth)

def main():
    # 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)

    dv = dac.dac_volume           # default DAC volume
    hv = dac.headphone_volume     # default headphone analog volume
    hg = dac.headphone_left_gain  # default headphone amp gain
    note = 60
    synth.press(note)
    # Check for unbuffered keystroke input on the USB serial console
    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})")

main()

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.
@samblenny
Copy link
Contributor Author

Speaker controls work (found and fixed a bug, all good now). Tested on Fruit Jam rev D with the 8Ω 1W bundled speaker. Works well. I changed the default volume level for TLV320DAC3100.speaker_output = True to be pretty quiet.

Normal usage would be to enable speaker_output, then provide a way to adjust the volume to a comfortable level by changing dac_volume. If you want to use a speaker that needs more drive strength, you could boost the signal with a combination of speaker_volume (first), then speaker_gain (if you need even more power).

NOTE: Enabling the speaker amp makes the headphone amp very noisy. My test code below lets you toggle the speaker amp power by pressing spacebar in the serial terminal.

My updated (now with speaker volume controls) code I used to test this is available below or at samblenny/tlv320dac-stuff/code.py:

from audiobusio import I2SOut
from board import (
    I2C, I2S_BCLK, I2S_DIN, I2S_MCLK, I2S_WS, PERIPH_RESET
)
from digitalio import DigitalInOut, Direction, Pull
import displayio
import gc
from micropython import const
import os
import synthio
import sys
import supervisor
import time

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
    )
    audio.play(synth)
    return (dac, audio, synth)

def main():
    # 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)

    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})")

                elif 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()

@samblenny samblenny marked this pull request as ready for review August 25, 2025 07:06
@samblenny samblenny requested a review from dhalbert August 25, 2025 13:05
@dhalbert dhalbert requested a review from FoamyGuy August 25, 2025 14:08
Copy link

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, but not tested. @FoamyGuy how does look to you, particularly the quieter default volume?

Still wondering about the escaped commas, though we could merge with those -- it doesn't affect anything.

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.
Copy link
Contributor

@FoamyGuy FoamyGuy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samblenny thanks for working on this!

The changes to volume behavior look good to me. I confirmed speaker and headphone volume properties are behaving better now. And that dac_volume, speaker_volume, and headphone_volume all do correctly raise and lower sound level now.

One request though, please add the latest version of your comprehensive test code from the comment above as an example in this repo in the examples folder like "tlv320_volumetest.py" or similar.

Hopefully, this should make it clear how to use the API for setting
volume for speakers, headphones, or line-level output.
@samblenny
Copy link
Contributor Author

Latest commit adds the volume tester to the examples folder along with some brief usage example docs at the top of the API reference page.

@samblenny samblenny requested a review from FoamyGuy August 25, 2025 19:53
Copy link
Contributor

@FoamyGuy FoamyGuy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. Thank you!

@FoamyGuy FoamyGuy merged commit 49f95b2 into adafruit:main Aug 25, 2025
1 check passed
@samblenny samblenny deleted the volume-fixes branch August 25, 2025 21:50
adafruit-adabot added a commit to adafruit/Adafruit_CircuitPython_Bundle that referenced this pull request Aug 29, 2025
Updating https://github.com/adafruit/Adafruit_CircuitPython_DotStar to 2.2.18 from 2.2.17:
  > Merge pull request adafruit/Adafruit_CircuitPython_DotStar#71 from dhalbert/spi-lock-managment

Updating https://github.com/adafruit/Adafruit_CircuitPython_EPD to 2.15.0 from 2.14.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_EPD#94 from adafruit/UC8197
  > Merge pull request adafruit/Adafruit_CircuitPython_EPD#95 from adafruit/ssd1883
  > Merge pull request adafruit/Adafruit_CircuitPython_EPD#93 from adafruit/ssd1680_fix
  > Merge pull request adafruit/Adafruit_CircuitPython_EPD#90 from AJMansfield/patch-1

Updating https://github.com/adafruit/Adafruit_CircuitPython_JD79661 to 1.0.1 from 1.0.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_JD79661#1 from adafruit/cleanup

Updating https://github.com/adafruit/Adafruit_CircuitPython_TLV320 to 1.2.1 from 1.1.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_TLV320#11 from samblenny/fix-example-link
  > Merge pull request adafruit/Adafruit_CircuitPython_TLV320#10 from samblenny/volume-fixes

Updating https://github.com/adafruit/Adafruit_CircuitPython_FruitJam to 1.2.0 from 0.5.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_FruitJam#13 from FoamyGuy/volume_api
  > Merge pull request adafruit/Adafruit_CircuitPython_FruitJam#14 from mikeysklar/ntp-helper
  > Merge pull request adafruit/Adafruit_CircuitPython_FruitJam#12 from mikeysklar/headphone-speaker
  > Merge pull request adafruit/Adafruit_CircuitPython_FruitJam#11 from adafruit/TheKitty-patch-1
  > Merge pull request adafruit/Adafruit_CircuitPython_FruitJam#9 from relic-se/request_display_config-default
  > Merge pull request adafruit/Adafruit_CircuitPython_FruitJam#8 from relic-se/any_button_pressed-fix

Updating https://github.com/adafruit/Adafruit_CircuitPython_Bundle/circuitpython_library_list.md to NA from NA:
  > Added the following libraries: Adafruit_CircuitPython_UC8253
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants