Skip to content

feat(apds9960): Add pentatonic light theremin example.#367

Open
Kaanoz-en wants to merge 1 commit intomainfrom
feat/apds9960-light-theremin
Open

feat(apds9960): Add pentatonic light theremin example.#367
Kaanoz-en wants to merge 1 commit intomainfrom
feat/apds9960-light-theremin

Conversation

@Kaanoz-en
Copy link
Copy Markdown
Contributor

Summary

Adds a light_theremin.py example using the APDS9960 ambient light sensor and the board's buzzer.

Changes

  • Initialized APDS9960 in ambient light mode and configured the buzzer using hardware PWM.
  • Mapped ambient light values to a C Major Pentatonic scale (Auto-Tune) to avoid jarring, continuous frequency slides ("wiou wiou" effect).
  • Calibrated custom MIN/MAX light boundaries for standard room lighting.
  • Ensured the buzzer mutes when the sensor is fully covered.
  • Implemented proper hardware cleanup (buzzer_ch.pulse_width_percent(0)) in the finally block.

Checklist

  • ruff check passes
  • python -m pytest tests/ -k mock -v passes (no mock test broken)
  • Tested on hardware (if applicable)
  • README updated (if adding/changing public API)
  • Examples added/updated (if applicable)
  • Commit messages follow <scope>: <Description.> format

Copilot AI review requested due to automatic review settings April 10, 2026 09:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new APDS9960 example that turns ambient light readings into discrete pitches on a C Major pentatonic scale, driving the board buzzer via hardware PWM (a “light theremin”).

Changes:

  • Added light_theremin.py example wiring up APDS9960 ALS readings to buzzer PWM frequency.
  • Implemented MIN/MAX light calibration, note quantization, and muting when covered.
  • Ensured buzzer is silenced in a finally block on exit.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +54 to +56
note_index = (clamped_light - MIN_LIGHT) * (TOTAL_NOTES - 1) // (MAX_LIGHT - MIN_LIGHT)
print("Light Level: {}, Note Index: {}".format(light_level, note_index))

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The loop prints on every 20ms iteration, which will spam the serial console (~50 lines/sec) and can noticeably slow the example on MicroPython targets. Consider printing only when the note changes or at a much slower interval (or guard behind a debug flag).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right, 50 lines per second is way too much and could definitely cause performance bottlenecks over serial on the MicroPython board. I've updated the loop to include a state cache (last_freq) so the print() statement is now guarded and only triggers when the note actually changes or when the device is muted.

Comment on lines +60 to +62
# Update the buzzer tone
buzzer_tim.freq(freq)
buzzer_ch.pulse_width_percent(50)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The buzzer timer frequency is reconfigured every loop iteration even when the mapped note hasn’t changed; this adds overhead and can introduce audible jitter. Cache the last note index/frequency and only call buzzer_tim.freq() (and duty updates) when it changes.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-initializing the PWM timer constantly was indeed causing unnecessary hardware overhead and slight audible jitter. I've implemented a last_freq state cache to track the current note. Now, we only call buzzer_tim.freq() and update the duty cycle when the mapped frequency actually changes. The audio output is much cleaner and smoother now!

@Kaanoz-en Kaanoz-en requested a review from nedseb April 10, 2026 09:17
@Kaanoz-en Kaanoz-en force-pushed the feat/apds9960-light-theremin branch from 22351a2 to 4bbbc80 Compare April 10, 2026 09:26
@nedseb nedseb requested review from Charly-sketch and removed request for nedseb April 10, 2026 12:03
@Charly-sketch
Copy link
Copy Markdown
Contributor

Review

Très bon travail sur cet exemple.
L’ensemble est globalement propre, mais il y a quelques points à améliorer pour le rendre plus robuste et bien aligné avec le projet.


1. Mapping lumière → note (division)

note_index = (clamped_light - MIN_LIGHT) * (TOTAL_NOTES - 1) // (MAX_LIGHT - MIN_LIGHT)

Ici il manque une protection si MAX_LIGHT == MIN_LIGHT, ce qui provoquerait une division par zéro.

Suggestion :

range_light = MAX_LIGHT - MIN_LIGHT
if range_light <= 0:
    range_light = 1

note_index = (clamped_light - MIN_LIGHT) * (TOTAL_NOTES - 1) // range_light

2. Clamp incomplet

clamped_light = min(light_level, MAX_LIGHT)

Tu clamps uniquement le max, mais pas le min. Du coup la formule peut produire des valeurs inattendues si light_level < MIN_LIGHT.

Suggestion :

clamped_light = max(MIN_LIGHT, min(light_level, MAX_LIGHT))

3. Prints très verbeux

print("Light Level: {}, Note Index: {}".format(light_level, note_index))
print("Playing: {} Hz".format(freq))

Ça spam beaucoup la console et ralentit un peu la boucle.

Suggestion :
Tu peux faire un print sur une seule ligne qui se met à jour :

print("Light: {} | Note: {} | Freq: {} Hz".format(light_level, note_index, freq), end="\r")

4. README non mis à jour

L’exemple n’est pas ajouté dans le README du driver.

À faire :

  • ajouter light_theremin.py dans le tableau des exemples
  • mentionner clairement la dépendance au buzzer (PWM / pin SPEAKER)

5. Description de la PR

Il manque la référence à l’issue dans la description.

À ajouter :

Closes #XXX

Conclusion

Très bon exemple, original et agréable à utiliser.
La logique est propre (gestion du jitter, gamme musicale, mute), et c’est clairement un plus pour le repo.

À corriger principalement :

  • sécuriser le calcul du mapping (division)
  • améliorer le clamp
  • réduire le spam console
  • mettre à jour le README
  • ajouter le Closes #issue dans la PR

Avec ces ajustements, ce sera un excellent exemple

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