foxnail

우노Q를 AI스피커로 활용

2026. 07. 26  ·  아두이노우노Q

지난 글은 이렇게 끝났다. “듣기. USB 마이크를 꽂으면 된다. 나가는 길은 다 뚫어놨으니 들어오는 길만 붙이면 말 → 클로드 → 말이 이어진다.”

그 뒤로 며칠. 이제 말을 걸면 대답하고, 부탁하면 노래를 골라 튼다. “비 오는 날 듣기 좋은 노래들 틀어줘”라고 하면 알아서 플레이리스트를 찾아 튼다.

가는 길에 벽이 또 몇 개 있었다. 그중 하나는 내가 돈을 쓰게 만든 벽이다. 이번에도 성공담보다 그쪽 기록이 길다.

귀를 붙이다

USB 마이크(COMS CM421)를 꽂으니 바로 잡혔다. 녹음 전용 장치라 arecord -l에는 뜨고 aplay -l에는 안 뜬다. 조용한 방에서 3초 녹음해보니 최대 진폭 717/32767. 신호는 확실히 들어온다.

여기서 첫 함정. USB를 다시 꽂으면 ALSA 카드 번호가 바뀐다. plughw:1,0으로 박아뒀더니 어느 날 카드가 0번이 되면서 “Invalid argument”가 났다. 번호가 아니라 이름으로 부르면 안 흔들린다.

arecord -D plughw:CARD=CM421,DEV=0 -f S16_LE -r 16000 -c 1

받아쓰기를 보드에서 하려다 포기한 이야기

처음엔 음성인식을 보드 안에서 돌리려 했다. 클라우드에 안 보내면 그게 제일 깔끔하니까. vosk 한국어 소형 모델, whisper base — 둘 다 돌긴 돌았다.

문제는 속도였다. 실시간의 0.6배속. 1초 말하면 처리에 1.6초가 걸린다는 뜻이고, 이건 말하는 동안 계속 밀린다는 뜻이다. 버퍼가 넘치고 소리가 유실됐다. 배치로 돌리면 7~13초. 한 마디 하고 열을 세야 대답이 온다.

쿼드코어 A53에 기대할 만한 결과였다. 인정하고 Deepgram 스트리밍으로 갈아탔다. 말하는 중에 중간 결과가 오고, 말을 멈추면 종료 신호가 온다. 정확도도 속도도 비교가 안 됐다.

세 언어를 동시에 듣기

한국어만 되면 아쉬웠다. 그런데 Deepgram 스트리밍은 언어 자동감지를 거부한다(400을 준다). 다국어 모드도 한국어·일본어는 안 된다.

그래서 뒤집었다. 한/일/영 세 개를 동시에 접속해서 같은 마이크 오디오를 셋 다에 보낸다. 그리고 발견한 것 — 틀린 언어 스트림은 빈 결과를 낸다. 한국어로 말하면 ko가 0.997을 뱉고 ja·en은 아무것도 안 뱉는다. 그러니 글자가 나온 쪽이 곧 정답이다.

전환 명령 없이 그냥 아무 언어로 말하면 그 언어로 대답한다. 목소리도 언어별로 바뀐다.

다만 부작용이 있다. 잡음이나 추임새를 en·ja 스트림이 “Mhmm”, “うん” 따위로 오인해서 엉뚱한 영어 대답을 하는 것이다. 추임새 사전과 신뢰도 하한선(0.5)으로 막았다. 완벽하진 않다.

스피커가 자꾸 죽는다

블루투스 스피커가 소리를 안 내면 혼자 꺼진다. 충전 중에도 꺼진다. 디버깅 중에 이게 몇 번이나 페어링을 끊어먹었고, 그때마다 리눅스 잘못인 줄 알고 엉뚱한 데를 팠다.

해법은 안 들리는 소리를 계속 흘려보내는 것이다. A2DP는 진폭과 무관하게 프레임을 전송하니까, 진폭이 0에 가까워도 링크는 살아 있다. 화이트노이즈를 32767분의 6 세기로 틀어놨다. 귀에는 안 들리고 스피커는 안 잔다.

여기서도 한 번 넘어졌다. 처음엔 샘플을 매번 파이썬으로 생성했더니 코어 하나의 53%를 먹었다. 1초 분량을 미리 만들어 반복 재생으로 바꾸니 0.3%가 됐다. 같은 결과, 176배 싼 값.

여기까지가 AI 스피커

이 시점에 물건이 됐다. 마이크 → Deepgram → 클로드(웹검색 포함) → 음성합성 → 블루투스 스피커. 응답까지 1.5~2초. “서울 날씨?”라고 물으면 3초 남짓에 실시간 날씨를 말해준다.

부팅하면 알아서 뜬다. 스피커가 붙을 때까지 최대 120초 기다렸다가 인사한다. 볼륨은 30%로 맞춰서 시작한다(아침에 놀란 적이 있다).

이제 음악

말이 되니 노래가 하고 싶어졌다. 폰에서 스트리밍하는 게 아니라 이 상자가 스스로 트는 것.

벅스를 먼저 봤는데 막혔다. 리눅스 앱이 없고 공개 API도 없다. 유일한 길인 웹플레이어 자동화는 이 보드 크로미움에 DRM 모듈이 없어서 안 된다.

스포티파이는 되는 길로 보였다. spotifyd에 aarch64 프리빌트 바이너리가 있어서 컴파일도 sudo도 필요 없다. 홈에 풀고 서비스로 올리면 보드가 Spotify Connect 기기가 된다. 단, Premium 계정이 필요하다.

그래서 결제했다.

error audio key 0 1

설치는 매끄러웠다. 인증도 됐다. 폰 앱 기기 목록에 보드가 뜨고, 고르니 계정이 넘어왔다. 서버가 "type": "premium"이라고 답하는 것도 로그로 확인했다.

그런데 소리가 안 났다. 트랙은 계속 넘어갔다.

[INFO]  Loading <고백> with Spotify URI ...
[ERROR] error audio key 0 1
[WARN]  Unable to load key, continuing without decryption
[ERROR] Symphonia Decoder Error: end of stream
[ERROR] Skipping to next track

파일은 CDN에서 멀쩡히 받아온다. 그걸 풀 열쇠만 서버가 안 준다. 일곱 곡 연속 전부 같았다.

librespot 이슈를 파보니 2025년 11월부터 같은 증상이 쌓여 있었다. 반년에 걸친 스레드가 도달한 결론은 이렇다. 스포티파이가 구형 오디오 키 경로를 계정 생성 시점 기준으로 막고 있다. 2024년 이후에 만든 계정이 대상이다. 오래된 계정은 여전히 잘 된다. Premium 여부와는 무관하다.

근본 해결은 스포티파이 자체 DRM을 뚫어야 가능한데, 메인테이너들이 법적 리스크 때문에 하지 않기로 정리했다. 즉 업데이트로 풀릴 전망이 없다.

그리고 내 계정은 이 일을 하려고 그날 새로 만든 계정이었다. 정확히 차단 대상.

정공법 — 브라우저에 DRM을 얹다

포기하기 전에 하나가 남아 있었다. 스포티파이 웹플레이어는 구형 키 경로를 안 쓴다. 정식 DRM(Widevine)을 쓴다. 그러니 그 차단과 무관하다.

문제는 ARM64 리눅스에 Widevine이 없었다는 것이다. 넷플릭스도 스포티파이도 64비트 ARM 리눅스에서 안 되던 이유가 이것이다. 그런데 최근에 길이 생겼다. Asahi Linux 쪽에서 ChromeOS용 ARM64 CDM을 일반 리눅스에서 돌아가게 고치는 설치기를 만들어뒀다.

깔고 확인했다.

WV_ACCESS_OK   ← 크로미움이 com.widevine.alpha 인식
WV_KEYS_OK     ← CDM 프로세스 기동, 키 생성 성공

재생하며 크로미움 내부 진단을 뽑아보니 이렇게 나왔다.

kSetCdm  key_system = com.widevine.alpha
kIsCdmAttached = true
kIsAudioDecryptingDemuxerStream = true
kAudioTracks codec = aac
ERRORS: []

복호화가 돌고 있다는 뜻이다. 소리가 났다.

Arduino UNO Q 화면에서 돌아가는 스포티파이 웹플레이어 — 플레이리스트 '잘 때 듣는 피아노' 재생 중

손바닥만 한 보드에서 돌아가는 스포티파이다. 오른쪽에 대기열도 정상이고, 아래 재생바도 움직인다.

참고로 이 스크린샷을 찍고 나서야 안 사실 하나. 처음엔 한글이 전부 두부(▊)로 나왔다. 보드 크로미움에 한글 폰트가 아예 없었다(fc-list :lang=ko 결과 0개). 한글 폰트를 깔고 다시 찍은 게 위 화면이다. 소리만 신경 쓰다 보니 글자는 안 보고 있었다.

“Playing on Cwtch”

그런데 처음엔 소리가 안 났다. 화면에는 곡이 재생 중인데 스피커는 조용하고, 트랙만 자꾸 넘어갔다.

화면 구석에 초록 글씨가 있었다. “Playing on Cwtch”. Cwtch는 이 보드의 호스트명이다.

웹플레이어가 브라우저에서 재생하는 게 아니라, 아까 만들어둔 spotifyd원격으로 조종하고 있었던 것이다. 그러니 재생 명령은 막힌 쪽으로 갔고, 당연히 키를 못 받아 스킵됐다. spotifyd를 내리자마자 브라우저가 자기가 재생하기 시작했다.

죽은 길을 치우지 않으면 산 길을 가로챈다. 한 시간을 여기서 썼다.

목소리로 선곡하기

재생이 되니 마지막 조각이 남았다. 목소리로 시키는 것.

클로드에게 음악 제어 도구를 하나 쥐여줬다. 곡이나 가수를 콕 집으면 트랙 재생, 분위기나 상황을 말하면 플레이리스트 재생을 고르게 했다. 검색어는 클로드가 알아서 정한다.

실제로 이렇게 돈다.

발화: "비 오는 날 듣기 좋은 노래들 틀어줘"
  ↓
클로드 판단: 플레이리스트, 검색어 "rainy day chill mood songs"
  ↓
Spotify: "Rainy Days Music — Songs for a Rain Day"
  ↓
음성 응답: "비 오는 날 어울리는, 잔잔한 곡들로 채워드렸어요.
           지금은 빌리 아일리시와 칼리드의 lovely가 흐르고 있어요."

재미있는 건 한국어 요청을 영어 검색어로 바꿔서 던졌다는 점이다. 그게 결과가 더 좋다는 걸 알고 한 일이다. 이게 “선곡”이라고 부를 만한 부분이다.

멈춤은 클로드에게 묻지 않는다

여기서 설계를 한 번 고쳤다. 처음엔 모든 발화를 클로드에게 넘겼는데, 어느 순간 API 호출 한도에 걸려서 “노래 멈춰”조차 안 먹었다.

생각해보면 당연히 잘못된 구조다. 멈춤·다음곡·이전곡·”지금 무슨 노래야”는 판단이 필요 없는 명령이다. 이걸 왜 클라우드에 물어보나.

정규식으로 먼저 걸러서 바로 실행하게 바꿨다. 이제 한도가 막혀 있어도, 인터넷이 느려도 멈춤은 멈춤이다. 선곡처럼 판단이 필요한 것만 클로드에게 간다.

화면을 긁는 일의 대가

웹플레이어를 브라우저 자동화로 조작하는 동안, 하루에 다섯 번 서로 다른 방식으로 틀렸다.

  • 뮤직비디오를 걸러내다 정답을 버렸다. “아이유 밤편지”의 정답인 Through the Night가 상위에 뮤직비디오로만 있었다. 뮤직비디오도 소리는 정상으로 나오는데, 필터가 그걸 건너뛰고 엉뚱한 곡을 틀었다.
  • 사이드바 버튼을 눌렀다. 플레이리스트 페이지에서 첫 번째 재생버튼을 집었더니, 왼쪽 사이드바의 “Liked Songs” 버튼이 잡혔다. 화면엔 원하는 플레이리스트가 떠 있는데 전혀 다른 노래가 나왔다.
  • 이전 검색 결과를 잡았다. 두 번 당했다. 페이지가 로드돼도 화면 앱이 옛 데이터로 목록을 한 번 다시 그린다. 낡은 노드에 표시를 해두는 방법도 뚫렸다 — 다시 그리면 표시 없는 새 노드가 되니까.
  • 타임아웃. 이 보드에서 스포티파이 화면이 그려지는 데만 18~23초가 걸린다. 30초 제한으론 부하가 조금만 걸려도 넘친다.

마지막은 검색창에 찍힌 글자가 내 질의와 같아질 때까지 기다리는 방식으로 잡았다. 지금은 통과한다. 하지만 스포티파이가 화면을 조금만 바꾸면 또 깨진다.

고칠 때마다 다음 게 나왔다는 건, 접근이 틀렸다는 신호다. 정식 Web API로 가면 검색은 JSON 한 번, 재생은 REST 한 번이라 클릭할 화면 자체가 없다. 속도도 60초에서 1초 아래로 떨어진다. 다음 작업은 그것이다.

참고로 웹플레이어가 쓰는 토큰을 가로채는 길도 기술적으로는 열려 있었다. 실제로 성공도 했다. 그런데 스포티파이가 그 용도를 약관 위반으로 명시하고 있어서 쓰지 않기로 했다. 되는 것과 써도 되는 것은 다르다.

오늘 배운 것

  • 내 메모가 낡는다. spotifyd는 예전 조사에선 “되는 길”이었다. 그 메모를 믿고 시작해서 결제까지 갔다. 시작 전에 최신 상태를 확인했어야 했다.
  • 죽은 길은 치워야 한다. 막힌 spotifyd를 살려둔 채로 웹플레이어를 띄웠더니, 웹플레이어가 그쪽을 원격조종하면서 한 시간을 잡아먹었다.
  • 기본 조작을 판단 엔진에 의존시키지 말 것. “멈춰”가 API 한도 때문에 안 먹는 건 사고가 아니라 설계 잘못이다.
  • 화면을 긁는 건 임시방편이다. 다섯 번 고쳤고 다섯 번 다른 데서 터졌다. 같은 자리를 계속 때우게 되면 접근을 의심할 때다.
  • 안 들리는 소리도 신호다. 32767분의 6짜리 화이트노이즈가 스피커를 안 자게 만든다. 사람 기준으로 0이어도 프로토콜 기준으론 0이 아니다.
  • 느린 건 안 되는 것이다. 0.6배속 받아쓰기는 “일단 되긴 한다”가 아니라 “안 된다”였다. 대화에선 특히 그렇다.

남은 것

음악이 나오는 동안 마이크가 그 노래를 받아먹는다. 스피커에서 나온 가사가 마이크로 들어가 발화로 오인될 수 있다. 지금은 비서가 말할 때 음악을 15%로 낮추는 것까지 해뒀는데, 근본 해결은 호출어(“쿠치야” 같은)를 두는 것이다. 그게 다음 순서다.

그리고 정식 API. 화면 긁기를 걷어내는 일.

공유하기