Il y a environ cinq ans (vers 2015), quand je suis allé dîner avec une connaissance sourde ** Parfois, je n'entends pas le son tactile de Suica (de la carte IC de transport) et je suis inquiet si je pourrais le toucher ** J'ai entendu l'histoire. À ce moment-là, j'étais sur le point d'acheter un Raspberry Pi à Akihabara, et je me demandais si je pouvais faire quelque chose à ce sujet. ** Puis-je fabriquer une machine qui allume la LED lorsque je capte le son tactile de Suica (PASMO ou ICOCA, peu importe)? ** ** J'y ai pensé et j'ai essayé de réussir.
Il est bon de s'en souvenir, mais comme l'équipement (câblage) a déjà été démonté, je me demande comment je l'ai expliqué. Au fait, même si j'étais dans une telle situation au moment d'écrire cet article, je l'ai laissé encore deux ans jusqu'à ce que je le publie ...
Dans l'image ci-dessus, un vérificateur de courant / tension USB est également connecté. Je pense que c'était en veille (lorsque le programme de détection n'était pas en cours d'exécution), mais comme l'alimentation USB est d'environ 5V, elle fonctionne à environ 1,4W.
Si vous regardez de près l'image de la carte plus tôt, il y a une résistance, mais c'est pour limiter le courant de la LED à 7 segments. En regardant la Fiche technique de la LED à 7 segments,
Donc, je l'ai connecté au GPIO de RPi via la résistance de 220Ω que j'avais. Pour GPIO, par exemple, veuillez consulter la page suivante. ON / OFF peut être contrôlé par des commandes et des programmes.
[^ 1]: Il y a une limite de 16 mA sur le courant qui peut être passé à travers la broche GPIO du Raspberry Pi, il est donc nécessaire de remplir cette condition ainsi que les pièces.
Soit dit en passant, le GPIO de RPi1 est de 3,3 V, donc si vous utilisez une résistance de 220 Ω, est-ce que cela passera environ 6 mA à la LED [^ 2]?
[^ 2]: Il y a une limite que le courant total qui peut être passé à travers la broche GPIO du Raspberry Pi est jusqu'à 50mA, mais dans ce cas, cela devrait être correct car il est de 42mA même si les 7 segments sont allumés.
Si vous regardez un segment (éclairé), vous devriez voir un circuit comme celui-ci. En fait, il semble que les sept segments soient connectés en parallèle, comme l'expérience de l'ampoule miniature de la science. Cependant, il convient de noter que le côté + est en fait 3,3V uniquement dans le segment éclairé, et le segment éclairé est à 0V sur le côté +. Le segment off a 0V sur les côtés + et, donc aucun courant ne circule.
Après cela, écrivez un programme qui s'exécute sur Python sur Raspbian. Largement divisé
-Où capturer le son du microphone --Où détecter le son tactile --Où allumer la LED
Vous devez réussir.
Un microphone connecté via un convertisseur audio USB peut être traité comme un périphérique ALSA, je l'ai donc écrit en utilisant le module alsaaudio. Je voulais également lire le fichier wav pour le débogage, donc je l'ai écrit d'une manière qui prend en charge les deux.
Une chose à noter à propos de Raspberry Pi est que l'écriture d'un mauvais programme est lourde et ne peut être aidée. Le traitement qui peut être effectué en un instant sur un PC est une tâche difficile pour RPi (surtout lorsque vous souhaitez opérer en temps réel comme cette fois). Alors que FFT doit traiter un nombre fixe d'images (2 carrés), l'entrée microphone ne peut pas toujours lire un nombre fixe d'images. C'est pourquoi il existe un processus tel que la création d'un tampon en anneau, la combinaison des données lues et la découpe à une certaine longueur pour FFT. Si je l'ai écrit en combinant des listes et en découpant sans penser à rien ici, cela aurait été trop lourd pour travailler en temps réel ...
pcmmod.py
# -*- coding: utf-8 -*-
import numpy
import alsaaudio
import wave
import collections
FrameArrayTuple = collections.namedtuple(
"FrameArrayTuple",
["array", "nframes_read"])
# Based on https://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/
class RingBuffer:
"A 1D ring buffer using numpy arrays"
def __init__(self, length):
pow2 = int(numpy.ceil(numpy.log2(length)))
self.length = 2 ** pow2
self.data = numpy.zeros(self.length, dtype='float64')
self.index_top = 0
self.index_bottom = 0
def extend(self, x):
"adds array x to ring buffer"
if x.size > 0:
x_index = (self.index_bottom + numpy.arange(x.size)) & (self.length-1)
self.data[x_index] = x
self.index_bottom = x_index[-1] + 1
def get(self, n=None):
"Returns the first-in-first-out data in the ring buffer"
idx = (self.index_top + numpy.arange(min(n, self.count()) if n is not None else self.count())) & (self.length-1)
self.index_top = idx[-1] + 1
return self.data[idx]
def count(self):
c = (self.index_bottom - self.index_top + self.length) & (self.length-1)
return c
class PCMInputStream:
def __init__(self, maxNumFrame):
#Ne peut pas être instancié indépendamment, mais peut être appelé à partir de la destination d'héritage
self.maxNumFrame = maxNumFrame
self.op_array = self.getFramesToArrayOperator()
def readFrames(self):
raise NotImplementedError("readFrames() must be implemented")
def readFrameArray(self):
frames = self.readFrames()
return self.op_array(frames)
def getNumChannels(self):
raise NotImplementedError("getNumChannels() must be implemented")
def getFrameRate(self):
raise NotImplementedError("getFrameRate() must be implemented")
def getSampleWidthInBytes(self):
raise NotImplementedError("getSampleWidthInBytes() must be implemented")
def getFramesToArrayOperator(self):
sw = self.getSampleWidthInBytes()
if sw == 1:
fmt = "uint8"
shift_amp = 128.0
max_amp = 128.0
elif sw == 2:
fmt = "int16"
shift_amp = 0.0
max_amp = 32768.0
else:
raise ValueError("getSampleWidthInBytes() must be return 1 or 2")
return
return (lambda frames: (numpy.frombuffer(frames, dtype=fmt) - shift_amp) / max_amp)
def getFrameArrayIterator(self):
#Essayez de toujours renvoyer numMaxFrame à la fois.
nframes_read = 0
num_channels = self.getNumChannels()
arr_buffer = RingBuffer(self.maxNumFrame * 10)
l = -1
while l != 0:
if arr_buffer.count() >= self.maxNumFrame * num_channels:
nframes_read += self.maxNumFrame
#Diviser en canaux
arr_channels = arr_buffer.get(self.maxNumFrame * num_channels).reshape(self.maxNumFrame, num_channels).T
yield FrameArrayTuple(arr_channels, nframes_read)
else:
arr = self.readFrameArray()
l = arr.shape[0]
assert l % num_channels == 0
#Combinez le contenu lu
arr_buffer.extend(arr)
#Renvoie les dernières données
arr = arr_buffer.get()
nframes_read += arr.shape[0] / num_channels
arr_channels = arr.reshape(arr.shape[0] / num_channels, num_channels).T
yield FrameArrayTuple(arr_channels, nframes_read)
def close(self):
pass
class PCMInputStreamFromWave(PCMInputStream):
def __init__(self, filename, maxNumFrame):
self.wv = wave.open(filename, "r")
self.ch = self.wv.getnchannels()
self.rate = self.wv.getframerate()
self.sw = self.wv.getsampwidth()
self.maxNumFrame = maxNumFrame
PCMInputStream.__init__(self, maxNumFrame)
def readFrames(self):
return self.wv.readframes(self.maxNumFrame)
def getNumChannels(self):
return self.ch
def getFrameRate(self):
return self.rate
def getSampleWidthInBytes(self):
return self.sw
def close(self):
self.wv.close()
class PCMInputStreamFromMic(PCMInputStream):
def __init__(self, rate, sampleWidth, maxNumFrame):
self.ch = 1
self.rate = rate
self.sw = sampleWidth
self.maxNumFrame = maxNumFrame
#Initialisation du périphérique d'enregistrement
self.pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE)
self.pcm.setchannels(self.ch)
self.pcm.setrate(self.rate)
#Lisez deux fois à la fois pour accélérer le traitement
print self.pcm.setperiodsize(self.maxNumFrame * 4)
if self.sw == 1:
self.pcm.setformat(alsaaudio.PCM_FORMAT_U8)
elif self.sw == 2:
self.pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
else:
raise ValueError("sampleWidth must be 1 or 2")
PCMInputStream.__init__(self, maxNumFrame)
def readFrames(self):
length, frames = self.pcm.read()
return frames
def getNumChannels(self):
return self.ch
def getFrameRate(self):
return self.rate
def getSampleWidthInBytes(self):
return self.sw
C'est aussi un souvenir assez difficile. Fondamentalement, le son a été analysé en fréquence par FFT, et si la fréquence donnée (du son tactile) avait plus de puissance que les autres fréquences, elle aurait été jugée "touchée" ... Cependant, si vous utilisez la boucle for facilement comme si vous écriviez un programme sur un PC, il sera trop lourd de travailler en temps réel lors du calcul de la puissance d'une fréquence spécifique à partir des composants du bac de fréquences, par exemple (si vous n'êtes pas bon dans ce domaine, le traitement de la FFT lui-même). Je me souviens avoir fait diverses choses, comme réécrire les parties qui peuvent être écrites avec des modules intégrés, NumPy et SciPy.
suicadetection.py
# -*- coding: utf-8 -*-
import numpy
import scipy.fftpack
import time
import bisect
import collections
DetectionHistoryTuple = collections.namedtuple(
"DetectionHistoryTuple",
["cond_energy", "energy_peak", "freq_center_detected"])
#Constante représentant l'état de détection
DETECTION_ON = "on"
DETECTION_OFF = "off"
class SuicaDetection:
#Rayon de fréquence du calcul d'énergie[Hz]
FREQ_TOLERANCE = 50
#Nombre d'histoires enregistrées
NUM_HIST_SAVED = 3
#Seuil du rapport d'énergie au moment du jugement d'augmentation
THRES_ENERGY_RATIO = 0.25
#Valeur minimale de float64
EPS_FLOAT64 = numpy.finfo(numpy.float64).eps
def freq_filter_vector(self, freq_center, freq_tolerance):
freq_delta = self.freq_axis[1] - self.freq_axis[0]
# freq_center +/- freq_L'énergie contenue dans la tolérance
#Renvoie un vecteur de poids à calculer.
energy_weight = numpy.array(
[(lambda freq_min, freq_max:
(1.0 if freq_min <= f and f + freq_delta <= freq_max
else (freq_max - f) / freq_delta if freq_min <= f <= freq_max <= f + freq_delta
else (f + freq_delta - freq_min) / freq_delta if f <= freq_min <= f + freq_delta <= freq_max
else (freq_tolerance * 2 / freq_delta) if f <= freq_min and freq_max <= f + freq_delta
else 0.0))
(freq_center - freq_tolerance, freq_center + freq_tolerance)
for f in self.freq_axis])
return energy_weight
def __init__(self, center_freqs, freq_axis):
self.ts = time.time()
self.hist = []
self.time_axis = []
self.nframes_read = 0
self.detected_freq = None
self.init_energy = None
self.freq_axis = freq_axis
#Poids énergétique pour la fréquence centrale que vous souhaitez détecter
self.center_freqs = center_freqs
self.energy_weight_array = numpy.array([
self.freq_filter_vector(center_freq, self.FREQ_TOLERANCE)
for center_freq in center_freqs
]).T
def input_array(self, arr):
assert len(arr.shape) == 1
num_frames = arr.shape[0]
self.detect(arr)
status = None
if self.detected_freq:
#Lorsqu'il y a un son, le son de cette bande de fréquences est entendu trois fois immédiatement avant-Fin lorsque 5 dB chute
if all((t.energy_peak is None) or (t.energy_peak - self.init_energy) < -5 for t in self.hist[-3:]):
self.detected_freq = None
status = DETECTION_OFF
else:
#S'il n'y a pas de son, c'est OK si la condition d'énergie est remplie deux fois sur les trois dernières fois.
if len([t for t in self.hist[-3:] if t.cond_energy]) >= 2:
self.detected_freq = self.hist[-1].freq_center_detected
self.init_energy = self.hist[-1].energy_peak
status = DETECTION_ON
self.nframes_read += num_frames
return (self.nframes_read, status)
def detect(self, arr):
#print "start", time.time()
assert len(arr.shape) == 1
num_frames = arr.shape[0]
# FFT
f = scipy.fftpack.fft(arr)
e = numpy.square(numpy.absolute(f))
#Prévention des erreurs
e = numpy.maximum(e, self.EPS_FLOAT64)
#Énergie totale (temps constants)
energy_total = e.sum()
#Énergie pour chaque fréquence
# +/-Combinez (double) l'énergie de la fréquence de
energy_axis = 2 * e[0:num_frames/2]
log_energy_axis = 10 * numpy.log10(energy_axis)
#Calculer le rapport d'énergie près de la fréquence spécifiée
energy_weighted = energy_axis.dot(self.energy_weight_array)
energy_ratio_max, freq_center_detected = \
max(zip(list(energy_weighted / energy_total), self.center_freqs),
key=lambda t: t[0])
#Conditions énergétiques
#Concentrez-vous sur la fréquence la plus forte
cond_energy = (energy_ratio_max >= self.THRES_ENERGY_RATIO)
# +/-Basé sur la puissance maximale à moins de 100 Hz
idx_low = bisect.bisect_left(self.freq_axis, freq_center_detected - 100)
idx_high = bisect.bisect_right(self.freq_axis, freq_center_detected + 100)
energy_peak = log_energy_axis[idx_low:idx_high+1].max()
#Ajouter à l'histoire
self.hist.append(DetectionHistoryTuple(cond_energy=cond_energy,
energy_peak=energy_peak,
freq_center_detected=freq_center_detected))
#Supprimer l'ancienne histoire
if len(self.hist) > self.NUM_HIST_SAVED:
self.hist.pop(0)
Contrôlez l'activation / désactivation du GPIO correspondant en fonction du numéro que vous souhaitez briller. Le GPIO à utiliser est défini par le programme appelant. Le module RPi.GPIO a été utilisé pour ce contrôle ON / OFF.
Il semble que GPIO ne peut être contrôlé que s'il est exécuté en tant que root, alors soyez prudent.
sevenseg.py
# -*- coding: utf-8 -*-
import sys
import RPi.GPIO as GPIO
import time
class GPIO7seg:
sevenseg_on = [[0, 2, 3, 5, 6, 7, 8, 9],
[0, 1, 2, 3, 4, 7, 8, 9],
[0, 1, 3, 4, 5, 6, 7, 8, 9],
[0, 2, 3, 5, 6, 8, 9],
[0, 2, 6, 8],
[0, 4, 5, 6, 8, 9],
[2, 3, 4, 5, 6, 8, 9]]
def __init__(self, id_pin_seg):
self.id_pin_seg = id_pin_seg
GPIO.setmode(GPIO.BCM)
for i in id_pin_seg:
GPIO.setup(i, GPIO.OUT)
def digit(self, n):
for i in xrange(7):
GPIO.output(self.id_pin_seg[i], n not in self.sevenseg_on[i])
Lors de l'appel de chaque module depuis le programme principal, la LED s'allume en fonction du résultat du jugement.
Par exemple, 1 s'affiche lorsqu'un bip retentit et 2 s'affiche lorsqu'un bip retentit. Lors du passage à la porte du ticket, la signification est différente une fois et deux, essayons donc de le comprendre C'est un programme appelé.
Il s'agit d'un GPIO pour éclairer la LED 7 segments, mais cette fois, les broches 15 à 21 correspondent respectivement aux segments A à G (voir fiche technique) de la LED 7 segments.
Si vous exécutez ce programme principal en tant que root (pour le contrôle GPIO), nous espérons que les nombres changeront en réponse au son tactile capté par le microphone.
suica_main.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import sys
import numpy
import itertools
import pcmmod
import suicadetection
import sevenseg
# ================================
def main():
#Longueur du signal (nombre d'échantillons)
MAX_NUM_FRAMES = 512
#Fréquence que vous souhaitez détecter
FREQS_CENTER = [2550, 2700, 3000]
#Fonction de fenêtre
ham_window = numpy.hamming(MAX_NUM_FRAMES)
#Lecture WAVE ou entrée microphone
if len(sys.argv) == 1:
#Microphone
rate = 44100
sw = 2
sound = pcmmod.PCMInputStreamFromMic(rate, sw, MAX_NUM_FRAMES)
else:
# WAVE
sound = pcmmod.PCMInputStreamFromWave(sys.argv[1], MAX_NUM_FRAMES)
rate = sound.getFrameRate()
#Axe de fréquence
freq_axis = numpy.fft.fftfreq(MAX_NUM_FRAMES, 1.0/rate)[0:MAX_NUM_FRAMES/2]
sd = suicadetection.SuicaDetection(FREQS_CENTER, freq_axis)
#compteur(7SEG LED)
counter_ring = 0
id_pin_seg = [15, 16, 17, 18, 19, 20, 21]
gpio7 = sevenseg.GPIO7seg(id_pin_seg)
gpio7.digit(counter_ring)
#Voir le dernier temps compté
counted_last = None
#Lire la forme d'onde
#Utiliser jusqu'à la partie complète et lue
for arr, nframes_read in itertools.takewhile(lambda t: t.array.shape[1] == MAX_NUM_FRAMES,
sound.getFrameArrayIterator()):
#Utilisé pour le jugement:Prendre L pour 2ch
time_frames, status = sd.input_array(arr[0] * ham_window)
if status == suicadetection.DETECTION_ON:
print float(time_frames) / rate, "ON"
# 0.Ne comptez pas avant 1 seconde
counted_last = time_frames
pending = True
elif status == suicadetection.DETECTION_OFF:
print float(time_frames) / rate, "OFF"
print
pending = False
if counted_last is not None:
if time_frames > counted_last + rate * 2.0:
#Réinitialisez la LED après 2 secondes
gpio7.digit(counter_ring)
counted_last = None
elif time_frames > counted_last + rate * 1.0:
#Aucun jugement de combo après 1 seconde (réinitialisation du compteur uniquement en interne)
counter_ring = 0
elif pending and time_frames > counted_last + rate * 0.1:
counter_ring += 1
gpio7.digit(counter_ring)
pending = False
if __name__ == "__main__":
main()
Je pense que j'ai fait beaucoup d'ajustements pour juger du son tactile. Si vous le regardez simplement avec le pouvoir, il réagira dans une cour de gare bruyante.
Après cela, j'ai constaté que la fréquence du son tactile n'était pas unifiée de manière inattendue. ** Même si vous comparez le cas de la billetterie et le cas du caissier au dépanneur, la hauteur (fréquence) du son tactile est en fait différente **, donc cela devient de plus en plus compliqué quand on pense à permettre de détecter l'un ou l'autre. ….
À propos, je n'avais pas été préoccupé par la fréquence du son tactile auparavant, alors quand j'ai écrit ce programme, j'ai recherché des nombres spécifiques dans Audacity pour la première fois. Quand j'ai signalé à la connaissance au début que "le son tactile semble être de 2550 Hz ou 2700 Hz", j'étais convaincu que "je ne peux pas l'entendre".
Cela faisait longtemps que les gens ne disaient pas que c'était une conception sans obstacle ou universelle. ** Regardez-vous uniquement les étapes visibles? Concevez-vous le son avec la bande de fréquences à l'esprit? ** ** Je pense que j'ai remarqué cela. Cependant, si le son est faible, il peut être difficile à entendre à cause de la foule. c'est difficile.
Recommended Posts