Hacker des radios pour une exposition / Musée d'Aquitaine

Bonjour à tous, je me présente, Nico, j’ai filmé la dernière Pycon avec mon association, Raffut, et je serai là pour filmer celle qui arrive à Strasbourg.
J’ai aussi un vrai métier, je suis technicien audiovisuel au Musée d’Aquitaine. En Mai, se tiendra une importante exposition sur l’immédiate après-guerre dans la région bordelaise.
Le commissaire d’exposition a proposé une idée intéressante : mettre tout au long du parcours de l’exposition des radios anciennes “hackées” pour diffuser des archives d’époque. En gros on tourne un potard, ça change d’archive sonore, et idéalement ça fait un petit bruit de modulation de fréquence à l’ancienne quand on change de “station”.
L’idée est super, le problème c’est que c’est sur moi que ça tombe, et je ne suis pas du tout compétent pour ce qui est de l’interaction entre le potard et le matériel qui va lire les sons (genre un orangepi, un ESP32 ou équivalent je suppose?). J’ai des notions, j’ai une idée de comment faire, mais j’ai besoin d’aide, je ne pense pas pouvoir devenir une bête de Python en 6 mois.
Je me suis inscrit au Fablab de la fac de Bordeaux, déjà, et je vais donner des nouvelles régulières de l’avancement des travaux ici, mais je prendrai toute lumière que vous pourrez apporter.
Evidemment, l’idée est de détailler ce projet sur un wiki, et rendre tout ouvert à la fin. C’est pour un musée, ça doit être ouvert à tous, réparable, et si possible durable.

1 « J'aime »

Bienvenu ici !

Je ferai ça avec un raspberry pi en effet (ou équivalent), quelque chose avec une prise jack pour ta sortie son quoi.

Pour « lire » ton potar le montage doit être, je pense :

+5v — résistance — potar — gnd
                 \
                  \ - Analog input

l’entrée analogique te donne une mesure entre 0V et 5V, donc avec ce montage tu mesure la tension au bornes du potar. La résistance est là pour réduire l’intensité max.

Plus ta résistance sera grande moins tu gaspillera d’énergie, mais moins tu verras de tension a ses bornes : faut tester, ça dépend de ton potar etc… au doigt mouillé je te dirais de ne pas descendre sous 1k ohm (résistance + potar au minimum), pour avoir 5 mA maximum.

Attention tout ce que je dis là est à prendre avec des pincettes, je suis dev Python, pas électronicien.

Bon un orange pi je ne sais pas mais un Raspbery Pi ça n’a pas d’entrée analogique, il te faut un MCP3008 entre les deux (ton moteur de recherche favoris t’aidera sur le cablage).

Une fois ton truc branché, tu peux la lire via Python, j’imagine qu’une recherche sur ton moteur favoris « read analog input Python raspberry mcp3008 » te donnera des résultats.

Pour le corps du programme, on peut essayer de commencer très simple (sale mais simple) en lançant et en tuant des VLC :

from time import sleep
import os
from subprocess import Popen

class VLCRunner:
    """This class starts and kills VLC as needed."""
    def __init__(self):
        self.vlc_pid = None

    def stop(self):
        """Stop a running VLC by... killing it."""
        if self.vlc_pid:
            os.kill(self.vlc_pid, 15)
            self.vlc_pid = None

    def play(self, file):
        """Play a file (stop VLC first if needed)."""
        self.stop()
        self.vlc_pid = Popen(["vlc", file]).pid

vlc = VLCRunner()

vlc.play('Stup Virus/01 Stupeflip - Intro.flac')
sleep(5)
vlc.play('Stup Virus/02 Stupeflip - The Antidote.flac')
sleep(5)
vlc.play('Stup Virus/03 Stupeflip - Creepy Slugs.flac')
sleep(5)
vlc.stop()

Ensuite il faut réagir au potar, que je n’ai pas, donc je vais le simuler…

Le code ressemble maintenant à ça :

https://p.afpy.org/g3r7/potar.py

et pour simuler le potar j’écris dans le ficher /tmp/potar comme ça j’ai pu tester :

$ echo 0.01 > /tmp/potar
$ echo 0.05 > /tmp/potar
$ echo 0.50 > /tmp/potar
$ echo 0.21 > /tmp/potar

je pense qu’on peut améliorer 1000 trucs, mais si déjà ça te permet d’avoir un truc qui marche c’est un début ?

2 « J'aime »

Merci beaucoup à toi !
C’est un beau début oui. Je vais déjà explorer les pistes que tu mets là, j’ai hâte de m’y mettre. Je donne des nouvelles ici dès qu’on a assemblé quelque chose.

En attendant d’avoir le matériel sous la main tu peux jouer avec :

from pathlib import Path
import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.title('Fake Potar')

current_value = tk.DoubleVar()

def slider_changed(event):
    Path("/tmp/potar").write_text(str(current_value.get()))

slider = ttk.Scale(root, from_=0.0, to=1.0, orient='horizontal', command=slider_changed, variable=current_value)
slider.pack()

root.mainloop()

pour tester le code du premier programme.

Le premier programme lit un “faux potar” dans /tmp/potar et celui là modifie /tmp/potar avec une jolie UI, comme ça vous pouvez tester vos sons, votre white noise, peaufiner, écouter ce que ça donne, faire évoluer le code, …

Salut !

Je m’y mets enfin, et ça s’avère plus compliqué qu’il n’y paraissait.
J’ai un Raspberry Pi 3b+, et un raspberry pi 4 pour faire mes tests.
J’ai pris un MCP3008, un potentiomètre, des fils, et j’ai relié tout ça au GPIO de mon RP3B+. Passé le bizutage pour comprendre les librairies Python (je pense que c’est bon maintenant, j’ai capté), j’ai un problème que je ne comprends pas trop.

J’ai utilisé un code extrêmement simple pour voir les valeurs du potar :

from gpiozero import MCP3008

pot = MCP3008(0)

while True:
    print(pot.value)

Le problème, c’est que la valeur renvoyée est toujours la même, quelle que soit la position du potar. Quelque chose comme 0.000488… Ce qui me parait très faible, donc que c’est comme si il n’y avait pas de courant, ou trop de résistance je sais pas.

J’ai essayé de voir si mon cablage était bon, j’ai pas l’impression d’avoir fait d’erreur, j’ai regardé plusieurs sites et comparé plusieurs fois le schéma (après je débute donc j’ai peut-être raté un truc énorme hein c’est possible). J’ai changé le potar, mêmes valeurs, j’ai enlevé le potard, idem.
J’ai remplacé le Pi3B+ par un Pi4B, même problème. J’ai testé avec chaque broche du MCP3008, ça bouge pas. Je commence à croire que le MCP3008 est merdique, ou que je rate peut-être quelque chose?
Je vais rerererevérifier mon cablage, pour voir, et commander un autre MCP3008, mais je commence à vraiment douter.
A quoi est-ce que ça pourrait être dû?

Edit : je crois que le problème est plus profond que prévu, vu que ça renvoie les mêmes valeurs en débranchant tout et en lançant le code quand même.

EDIT ENCORE :
J’ai tout débranché et rebranché, maintenant ça marche. Ma théorie est que j’ai du répéter la même erreur plusieurs fois à un moment. On va enfin pouvoir tester ce code, joie.
A vite pour de nouvelles aventures.

2 « J'aime »

Bon, ya eu du progrès.

La fonction principale, lire des sons en se basant sur la position du potentiomètre, marche. C’est déjà la magie. J’ai testé avec plusieurs librairies, on dirait bien que Pygame fait le taf.
Je ne suis évidemment pas devenu une Pythonvictim en si peu de temps, je me suis fait aider un peu par de l’IA quand j’arrivais à faire les bons prompts, beaucoup par des tutos, et j’ai testé de manière empirique et chaotique.
Maintenant, mon problème est d’assurer un contrôle du volume cohérent.

Je pars de ce code, qui marche bien. Le principe est de lire une des 6 musiques en fonction de la position du potentiomètre, et quand on passe de l’une à l’autre, on a un coup de bruit radio (blank noise).

from gpiozero import MCP3008
import time
import pygame

pygame.mixer.init()

musics = ['music1.wav', 'music2.wav', 'music3.wav', 'music4.wav', 'music5.wav', 'music6.wav']
blank_noise = 'blank_noise.wav'

potentiometer = MCP3008(channel=0)

def play_music(index):
    pygame.mixer.music.load(musics[index])
    pygame.mixer.music.play(loops=-1)

def play_blank_noise():
    pygame.mixer.music.load(blank_noise)
    pygame.mixer.music.play(loops=-1)

try:
    last_index = -1
    while True:
        pot_value = int(potentiometer.value * 1023)  # Get potentiometer value (0-1023)

        index = pot_value // (1024 // len(musics))  # Map pot_value to music index

        index = max(0, min(index, len(musics) - 1))

        if index != last_index:
            if last_index != -1:  # If not the first iteration, stop previous music
                pygame.mixer.music.stop()
            play_music(index)
            last_index = index

        time.sleep(0.1)

except KeyboardInterrupt:
    print("stop")

Et je veux y ajouter deux trucs :

  • un contrôle du volume qui ne fluctue pas dans tous les sens, j’ai essayé avec le code en dessous, mais ça ne fait que monter et baisser le volume, c’est insupportable.
  • Je voudrais que les musiques ne repartent pas de zéro à chaque fois qu’on repasse dessus, mais plutôt reprennent là où on les a laissées

Voilà ma tentative d’intégrer un deuxième potentiomètre pour le volume, mais qui ne marche pas.

import time
import numpy as np
import pygame
from gpiozero import MCP3008

# Initialize Pygame mixer
pygame.mixer.init()

# Load music files
music_files = ['music1.wav', 'music2.wav', 'music3.wav', 'music4.wav', 'music5.wav', 'music6.wav']
music_positions = [0] * len(music_files)
current_music = -1
blank_noise = pygame.mixer.Sound('blank_noise.wav')

# Initialize MCP3008 for potentiometers
pot_music = MCP3008(channel=0)
pot_volume = MCP3008(channel=1)

def play_music(index):
    global current_music
    if current_music != index:
        if current_music != -1:
            pygame.mixer.music.stop()
        pygame.mixer.music.load(music_files[index])
        pygame.mixer.music.play(loops=-1)
        current_music = index
        music_positions[index] = 0

def resume_music(index):
    pygame.mixer.music.set_pos(music_positions[index])
    pygame.mixer.music.unpause()

def update_music():
    global current_music
    pot_value = pot_music.value * len(music_files)
    new_music = int(pot_value)

    if new_music != current_music:
        if new_music < len(music_files) and new_music >= 0:
            if abs(pot_value - new_music) < 0.1:  # Radio blank noise condition
                blank_noise.play(loops=-1)
            else:
                blank_noise.stop()
                play_music(new_music)
        else:
            blank_noise.stop()
            current_music = -1
            pygame.mixer.music.stop()
    else:
        if current_music != -1:
            music_positions[current_music] = pygame.mixer.music.get_pos() / 1000.0

def update_volume():
    volume = pot_volume.value
    pygame.mixer.music.set_volume(volume)

try:
    while True:
        update_music()
        update_volume()
        time.sleep(0.1)
except KeyboardInterrupt:
    pygame.mixer.music.stop()
    blank_noise.stop()

Si vous avez des lumières, je suis preneur !

J’ai rajouté quelques print, comme dans :

que j’ai transformé en :

def update_volume():
    volume = pot_volume.value
    print("Set volume to", volume)
    pygame.mixer.music.set_volume(volume)

et … bah de souci de volume qui se ballade, ça me fait penser que le code est bon, mais qu’il y a un souci côté électronique ? Moi vu que je n’ai pas d’électronique ici (je teste avec mon interface en tk) je n’ai forcément pas de variations qui viennent de là, donc tant que je ne touche pas a mon interface, les print sont bien tous les mêmes :

Set volume to 1.0
Set volume to 1.0
Set volume to 1.0
Set volume to 1.0
Set volume to 1.0
Set volume to 1.0
Set volume to 1.0

déjà regarde ce que ça donne de ton côté. Si j’ai bien compris la classe MCP3008 fait le job de convertir la valeur entre 0 et 1, et pygame.set_volume veut quelque chose entre 0 et 1 donc c’est cool.

J’ai l’impression que ta “blank noise condition” est inversée : si tu es “très près” de la bonne valeur, ça part en white noise.

J’aurais tendance à dire : si tu es très près de la bonne valeur, ça joue la musique, sinon white noise.

Aussi il y a un bug, si tu lis une musique et que d’un coup l’utilisateur passe sur pile “2”, ça entre dans la “white noise condition” sans arrêter la musique, on se retrouve avec de la musique plus du bruit.

Ahh mais attends, la musique + du bruit ça peut-être super intéressant !

On pourrait avoir, en tournant progressivement le potar :

  • juste du bruit
  • du bruit mélangé à du son
  • juste du son
  • du son mélangé a du bruit
  • juste du bruit
  • juste du bruit
  • juste du bruit
  • du bruit mélangé à du son
  • du son …

J’ai un peu joué avec l’idée d’avoir une transition “bruit blanc + musique” avant d’avoir la musique.

Mais c’était pas très réaliste, ça fait pas radio du tout, ça coupe trop net.

Alors j’ai joué un peu autour de l’idée de calculer le volume du bruit blanc et de calculer le volume du son en fonction de la « fréquence » : plus on s’approche de la bonne “fréquence” plus le volume de la musique augmente et plus le volume du bruit blanc baisse.

C’est beaucoup plus réaliste ! On peut même rester “juste à côté” volontairement ou non et avoir le son bien net et un petit bruit blanc en fond.

Mon code actuel est là : radio.py

J’arrive a complètement me passer de la partie “se souvenir où on en est” : vu qu’il faut que je “mixe” du bruit blanc ET de la musique j’ai besoin que les deux jouent en même temps, donc au lieu de m’embêter, je joue tout tout le temps, et je règle juste les volumes. Oui c’est triché.

J’ai très peur que sur une petit CPU ça ne passe pas, tu me diras.

Ahh j’ai du oublier les “loops=-1”, j’ai pas la patience de tester mon programme plus de 30s, donc je ne suis pas arrivé à la fin des morceaux :smiley: mais tu sais corriger ça.

Tu peux supprimer la class MCP3008, c’est ma plomberie perso pour remplacer le vrai potar par un slider tkinter pour pouvoir tester sur mon laptop.

Mon slider tkinter pour ceux qui veulent jouer avec
from pathlib import Path
import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.title('Fake Potar')

current_freq = tk.DoubleVar()
current_volume = tk.DoubleVar()

def freq_changed(event):
    print("freq = ", current_freq.get())
    Path("/tmp/pot-0").write_text(str(current_freq.get()))

def volume_changed(event):
    print("volume = ", current_volume.get())
    Path("/tmp/pot-1").write_text(str(current_volume.get()))


slider0 = ttk.Scale(root, from_=0.0, to=1.0, length=800, orient='horizontal', command=volume_changed, variable=current_volume)
slider0.pack()

slider1 = ttk.Scale(root, from_=0.0, to=1.0, length=800, orient='horizontal', command=freq_changed, variable=current_freq)
slider1.pack()

root.mainloop()

wah merci beaucoup pour tout ça, je vais tester !
Sur le blank noise, ce que j’observe, c’est qu’il est devenu inutile à force de bidouiller, l’effet “radio” marche beaucoup mieux en faisant en sorte qu’un fichier music.wav sur deux soit en fait une plage de bruits radios, j’ai aussi mappé les “plages” de son pour que les parties avec des chansons soient plus larges que celles de bruit, c’est clairement plus agréable quand on l’utilise à la main.

Cette idée de mixage que tu proposes est super intéressante, et j’aime aussi l’idée de jouer en boucle en permanence chaque plage, mais j’ai aussi peur que ce soit trop pour le RPi3B+.
Ceci dit, dans l’esprit du projet, vu que les chansons sont le coeur de l’expo, j’ai l’impression que les conservateurs vont vouloir que chaque plage démarre sur un bout spécifique de chanson, afin que le visiteur saisisse le sens. Donc rien n’est très sûr pour le moment à ce niveau-là. En attendant, ma combine a été de changer les fichiers son eux-mêmes pour qu’il démarrent en plein milieu, et remettre le bout qui manque à la fin du fichier lui-même, ça marche mieux que je ne l’aurais cru, on n’entend pas du tout de coupure quand le fichier fait sa boucle.
Bref, je vais bosser là dessus, et encore merci !

1 « J'aime »

Cet échange me fait penser à “une histoire au bout du fil”. Le monsieur qui a eu l’idée est aussi impliqué dans Museomix, ya moyen qu’il trouve ton idée bien cool et qu’il soit très disposé à partager ses astuces.

Dis-lui que tu viens de la part d’Agnès-Tût-tûûût, il devrait me situer. :smiley:

1 « J'aime »