•
5. MIDI & Raw MIDI
5.0 실전 임베디드 카오디오 시스템에서의 MIDI
실전 임베디드 카 오디오 시스템(Automotive Infotainment System)에서 MIDI는 우리가 흔히 생각하는 '음악 제작'보다는 시스템 제어, 알림음 합성, 그리고 외부 장치와의 인터페이스 목적으로 주로 사용됩니다.
주요 활용 사례는 다음과 같습니다.
1. 효율적인 알림음(ADW - Acoustic Detection & Warning) 재생
자동차에는 안전벨트 미착용, 후방 주차 센서, 방향 지시등 등 수많은 알림음이 필요합니다.
- 용량 최적화: 고음질 WAV나 MP3 파일을 수십 개 저장하는 대신, 아주 작은 용량의 MIDI 시퀀스와 샘플 음원(Soundfont)만 저장하여 실시간으로 합성(Synthesis)합니다.
- 동적 변형: 상황에 따라 알림음의 피치(Pitch)나 템포를 실시간으로 바꿀 수 있습니다. 예를 들어, 장애물이 가까워질수록 비프음의 속도를 빠르게 조절하는 로직을 MIDI 컨트롤러 값(Control Change) 변경만으로 구현할 수 있습니다.
2. 하드웨어 컨트롤 및 모니터링 (HMI)
임베디드 시스템 내에서 버튼, 노브(Knob), 슬라이더의 물리적 움직임을 소프트웨어에 전달하는 프로토콜로 MIDI가 쓰이기도 합니다.
- MCU 간 통신: 메인 CPU와 오디오 전용 DSP(Digital Signal Processor) 사이에서 볼륨 값이나 이퀄라이저(EQ) 설정을 주고받을 때 Raw MIDI 바이트 형식을 빌려 쓰면 매우 가볍고 표준화된 통신이 가능합니다.
- 진단 도구: 엔지니어들이 차량 오디오 성능을 튜닝할 때, 외부 PC와 연결하여 실시간으로 파라미터를 조정하는 인터페이스로 MIDI over USB/Serial을 사용합니다.
3. 외부 악기 및 앱 연동 (CarPlay / Android Auto)
최근 인포테인먼트 시스템은 스마트폰 연동이 필수입니다.
- 가상 악기 앱: 사용자가 차 안에서 스마트폰의 음악 제작 앱을 실행했을 때, 차량의 터치스크린이나 핸들 버튼으로 앱을 제어할 수 있도록 MIDI over Bluetooth나 USB MIDI 프로토콜을 지원합니다.
- 악기 연결: 일부 고사양 시스템에서는 실제 미디 건반을 차량 USB 포트에 연결하여 차내 스피커로 연주할 수 있는 기능을 제공하기도 합니다.
실전 시스템 아키텍처 (Embedded 구조)
실제 임베디드 환경(커널 드라이버 수준)에서는 다음과 같은 흐름으로 처리됩니다.
- Application Layer: Android 기반 인포테인먼트 앱이 ALSA Sequencer를 통해 이벤트를 보냄.
- ALSA Layer: snd-virmidi 또는 사용자 정의 Raw MIDI 드라이버가 이벤트를 바이트 스트림으로 변환.
- Kernel/Driver Layer: 앞서 보신 my_midi_output_trigger 같은 함수가 UART/I2C/SPI를 통해 DSP로 데이터를 전송.
- Hardware (DSP): 전달받은 MIDI 바이트를 해석하여 내장된 웨이브테이블(Wavetable) 음원으로 소리를 출력하거나 필터 값을 변경.
요약하자면
자동차 임베디드 시스템에서 MIDI는 "매우 적은 리소스로 소리를 정밀하게 제어하고 장치 간 통신을 표준화하기 위한 도구"로 정의할 수 있습니다.
5.1 Raw MIDI API
Raw MIDI는 하드웨어 MIDI 포트를 직접 제어합니다:
ALSA Raw MIDI API는 리눅스에서 MIDI 하드웨어에 바이트(byte) 단위로 직접 접근할 수 있게 해주는 가장 낮은 수준의 인터페이스입니다. 타이밍이나 라우팅을 관리하는 'Sequencer API'와 달리, 하드웨어와 데이터를 있는 그대로 주고받을 때 사용합니다.
1. Raw MIDI vs. Sequencer API
어떤 API를 사용할지 결정하는 것이 중요합니다.
- Raw MIDI: 하드웨어에 대한 독점적 제어권을 갖습니다. 타이밍 처리를 직접 해야 하며, 주로 시스템 익스클루시브(SysEx) 덤프나 저지연(Low-latency) 하드웨어 통신에 적합합니다.
- Sequencer API: 여러 앱이 동시에 접근할 수 있고, 이벤트 기반이며 고해상도 타임스탬프를 지원합니다. DAW나 가상 악기 개발에 더 유리합니다.
2. 기본 작업 흐름
Raw MIDI API를 사용할 때는 보통 다음의 단계를 거칩니다.
- Open (열기): snd_rawmidi_open()을 호출하여 입력/출력 스트림 핸들을 얻습니다.
- Configure (설정): 필요하다면 snd_rawmidi_params()를 통해 버퍼 크기 등을 설정합니다.
- I/O (입출력): snd_rawmidi_read()로 들어오는 데이터를 읽거나, snd_rawmidi_write()로 데이터를 보냅니다.
- Drain (비우기): snd_rawmidi_drain()을 사용하여 버퍼에 남은 데이터가 모두 전송될 때까지 대기합니다.
- Close (닫기): snd_rawmidi_close()로 리소스를 해제합니다.
3. 주요 함수 및 플래그
| 함수 | 설명 |
| snd_rawmidi_open | 장치(예: "hw:1,0")에 대한 연결을 엽니다. |
| snd_rawmidi_write | MIDI 출력 포트에 원시 바이트를 씁니다. |
| snd_rawmidi_read | MIDI 입력 포트에서 원시 바이트를 읽습니다. |
| snd_rawmidi_drain | 출력 버퍼가 빌 때까지 프로세스를 차단(block)합니다. |
| snd_rawmidi_close | 핸들을 닫고 리소스를 반환합니다. |
주요 플래그:
- SND_RAWMIDI_NONBLOCK: 비차단 모드로 장치를 엽니다.
- SND_RAWMIDI_APPEND: (출력 전용) 여러 앱이 포트를 공유할 수 있게 시도합니다.
4. 코드 예시 (C 언어)
이 코드를 컴파일하려면 libasound2-dev 패키지가 필요하며, -lasound 옵션으로 링크해야 합니다.
5. 장치 식별 방법
터미널에서 다음 명령어를 입력하면 현재 시스템의 Raw MIDI 장치 목록을 확인할 수 있습니다.
주의사항
- "Device or resource busy": 다른 프로그램이나 ALSA Sequencer가 이미 해당 장치를 사용 중일 때 발생합니다. Raw MIDI는 대개 독점 접근이 필요합니다.
- 권한 문제: 유저가 audio 그룹에 속해 있어야 /dev/snd/midi* 장치에 직접 접근할 수 있습니다.
5.2 Code: ALSA Raw MIDI Device
아래 코드는 리눅스 커널 드라이버(Kernel-space) 수준에서 ALSA Raw MIDI 장치를 구현하는 전형적인 구조입니다. 앞서 설명드린 사용자 수준(User-space) API가 이 드라이버가 제공하는 기능을 호출하게 됩니다.
각 부분의 핵심 역할과 데이터 흐름을 정리해 드릴게요.
1. MIDI 디바이스 생성 및 초기화
- snd_rawmidi_new: 커널에 새로운 MIDI 장치를 등록합니다.
- 1, 1: 각각 출력(Output) 서브스트림 1개와 입력(Input) 서브스트림 1개를 생성한다는 뜻입니다.
- private_data: 드라이버 개발자가 정의한 구조체(chip)를 연결합니다. 이후 콜백 함수에서 하드웨어 레지스터에 접근할 때 이 포인터를 사용합니다.
2. 오퍼레이션(Ops) 설정
- ALSA 코어와 하드웨어 사이의 인터페이스를 정의합니다. 유저가 MIDI 데이터를 보낼 때(write), 커널은 여기에 등록된 trigger 함수를 실행합니다.
3. 출력 트리거 (my_midi_output_trigger)
이 함수는 데이터 전송의 '심장' 역할을 합니다.
- up 인자: 1이면 전송 시작, 0이면 중지를 의미합니다.
- snd_rawmidi_transmit_peek: ALSA 소프트웨어 버퍼에 있는 데이터를 삭제하지 않고 살짝 엿보는(Peek) 함수입니다.
- my_midi_tx_ready: 하드웨어(UART 등)의 송신 FIFO가 비어있어 데이터를 보낼 준비가 되었는지 확인합니다. (Flow Control)
- my_midi_write: 실제 하드웨어 레지스터에 1바이트를 씁니다.
- snd_rawmidi_transmit_ack: 하드웨어 전송이 성공했으므로, ALSA 버퍼에서 해당 바이트를 **실제로 제거(Acknowledge)**합니다.
Tip: 보통 트리거 함수 내에서 루프를 돌며 여러 바이트를 한꺼번에 보내거나, 하드웨어 인터럽트가 발생했을 때 다시 트리거를 호출하여 남은 데이터를 보냅니다.
4. 수신 인터럽트 (my_midi_rx_interrupt)
외부 장치(건반 등)로부터 데이터가 들어왔을 때 호출되는 인터럽트 핸들러 내의 로직입니다.
- my_midi_rx_avail: 하드웨어 수신 버퍼에 읽을 데이터가 있는지 확인합니다.
- snd_rawmidi_receive: 하드웨어에서 읽은 바이트를 ALSA 코어의 수신 버퍼로 밀어넣습니다. * 이렇게 전달된 데이터는 유저 프로그램이 snd_rawmidi_read()를 호출할 때 전달됩니다.
데이터 흐름 요약
| 방향 | 흐름 | 핵심 함수 |
| 송신 (TX) | 유저 앱 → ALSA 버퍼 → Trigger → 하드웨어 | peek & ack |
| 수신 (RX) | 하드웨어 → Interrupt → ALSA 버퍼 → 유저 앱 | receive |
주의할 점
- Spinlock: trigger 함수나 인터럽트 핸들러 내에서는 동기화를 위해 spin_lock을 적절히 사용해야 합니다. (코드에는 생략되어 있음)
- Atomic context: 트리거 함수는 원자적 문맥(Atomic context)에서 실행될 수 있으므로, 내부에서 잠자기(msleep 등)를 하면 안 됩니다.
이 드라이버를 실제 하드웨어(예: UART, SPI 등)에 이식하는 과정에서 특정 레지스터 설정이나 인터럽트 처리에 대해 더 궁금한 점이 있으신가요?
5.3 가상 MIDI (snd-virmidi)
snd-virmidi 모듈은 가상 MIDI 포트를 생성하여 ALSA Sequencer와 Raw MIDI 간 브리지 역할을 합니다.
snd-virmidi는 물리적인 하드웨어 장치 없이 소프트웨어적으로 MIDI 데이터를 주고받을 수 있게 해주는 ALSA의 가상 MIDI 카드 드라이버입니다.
가장 핵심적인 역할은 "Raw MIDI"와 "ALSA Sequencer"라는 두 세계를 이어주는 다리(Bridge) 역할을 하는 것입니다.
1. 주요 역할: API 간의 가교
리눅스 MIDI 시스템은 크게 두 가지 인터페이스로 나뉩니다.
- Raw MIDI: /dev/snd/midiC1D0 처럼 바이트 스트림을 직접 다루는 하드웨어 중심 방식.
- ALSA Sequencer: 타임스탬프와 포트 기반으로 이벤트를 다루는 고수준 방식 (가상 악기, DAW 등에서 사용).
snd-virmidi 모듈을 로드하면 가상의 MIDI 장치가 생성되는데, 이 장치의 Raw MIDI 포트로 데이터를 보내면 자동으로 Sequencer 포트의 이벤트로 변환되어 출력됩니다. 그 반대 방향도 마찬가지입니다.
2. 왜 사용하나요? (주요 용도)
- 구형 프로그램 지원: Raw MIDI 방식만 지원하는 오래된 소프트웨어를 현대적인 ALSA Sequencer 기반의 가상 악기나 DAW에 연결하고 싶을 때 사용합니다.
- 소프트웨어 간 라우팅: 하드웨어 미디 케이블 없이 한 프로그램의 MIDI 출력을 다른 프로그램의 입력으로 넣고 싶을 때 유용합니다.
- 개발 및 테스트: 실제 미디 건반이나 장비가 없는 환경에서 MIDI 드라이버 관련 코드를 테스트할 때 필수적입니다.
3. 사용 방법 (커널 모드 로드)
터미널에서 다음 명령어를 입력하여 가상 미디 장치를 활성화할 수 있습니다.
로드 후 amidi -l (Raw MIDI 목록)과 aconnect -lp (Sequencer 목록)를 실행해 보면, 동일한 이름의 가상 장치가 양쪽 목록에 모두 나타나는 것을 확인할 수 있습니다.
4. 요약
- 정체: 소프트웨어로 구현된 가상 MIDI 하드웨어.
- 특징: Raw MIDI 인터페이스와 Sequencer 인터페이스 간의 양방향 데이터 변환.
- 비유: "하드웨어 장치인 척하는 가짜 MIDI 카드".
5.3 ALSA Sequencer
Sequencer는 MIDI 이벤트의 고수준 라우팅과 스케줄링을 제공합니다. Client/Port 모델로 복잡한 MIDI 패치베이 구성이 가능합니다.
ALSA Sequencer는 리눅스 사운드 시스템(ALSA)에서 MIDI 데이터를 다루기 위한 고수준(High-level) 인터페이스입니다.
단순히 바이트를 주고받는 Raw MIDI와 달리, ALSA Sequencer는 "이벤트(Event)"와 "타이밍(Timing)"을 관리하는 일종의 'MIDI 허브' 역할을 합니다.
1. 주요 특징
- 이벤트 기반 (Event-based): 데이터를 바이트 스트림이 아닌 Note On, Note Off, Control Change 같은 구조화된 객체(Event)로 처리합니다.
- 멀티 클라이언트 (Multi-client): 하나의 MIDI 포트에 여러 개의 프로그램이 동시에 연결될 수 있습니다. (Raw MIDI의 독점 제어 한계를 극복)
- 타이밍과 큐 (Queues): 자체적인 타이머를 가지고 있어, "이 노트를 500ms 뒤에 연주해라"와 같은 스케줄링이 가능합니다.
- 동적 라우팅: aconnect 같은 도구를 사용해 소프트웨어와 하드웨어, 또는 소프트웨어끼리 MIDI 신호를 자유롭게 연결(Patch)할 수 있습니다.
2. Raw MIDI vs. Sequencer 차이점
| 구분 | Raw MIDI (snd_rawmidi) | ALSA Sequencer (snd_seq) |
| 데이터 형태 | 원시 바이트 (Raw Bytes) | 구조화된 이벤트 (Events) |
| 연결 방식 | 1:1 전용 연결 (독점) | N:N 다중 연결 (공유) |
| 타이밍 관리 | 사용자가 직접 처리해야 함 | 커널/라이브러리가 스케줄링 지원 |
| 주요 용도 | 시스템 익스클루시브(SysEx), 드라이버 제작 | DAW, 가상 악기, 미디 플레이어 |
3. 핵심 개념
- 클라이언트 (Client): Sequencer API를 사용하는 프로그램이나 하드웨어 드라이버입니다.
- 포트 (Port): 클라이언트 내에 존재하는 실제 '통로'입니다. (예: MIDI 입력 포트, 출력 포트)
- 구독 (Subscription): 한 포트의 출력을 다른 포트의 입력으로 연결하는 행위입니다.
- 큐 (Queue): 이벤트가 전송될 시간을 관리하는 타임라인입니다.
4. 유용한 터미널 명령어
• 포트 목록 확인:
• 포트 연결 (라우팅):
• 연결 해제:
aconnect -x
요약하자면
ALSA Sequencer는 리눅스에서 여러 음악 소프트웨어와 하드웨어를 마치 가상의 패치 케이블로 연결해주는 거대한 미디 라우팅 및 타이밍 센터라고 이해하시면 됩니다.
5.4 ALSA Sequencer API 예제
ALSA Sequencer API를 사용하여 MIDI 이벤트를 주고받는 코드는 Raw MIDI보다 조금 더 복잡하지만, 구조는 매우 체계적입니다. 코드를 실행하기 위해서는 libasound2-dev 패키지가 설치되어 있어야 하며, 컴파일 시 -lasound옵션을 붙여야 합니다.
1. MIDI 이벤트 전송 (Sender)
이 예제는 가상의 출력 포트를 생성하고, 실행 시 중앙 C(Note On) 신호를 즉시 전송합니다.
2. MIDI 이벤트 수신 (Receiver)
이 예제는 포트를 열고 대기하다가, 들어오는 MIDI 이벤트(Note On/Off)를 화면에 출력합니다.
3. 컴파일 및 실행 방법
컴파일
실행 및 연결
Sequencer는 포트를 생성만 한다고 바로 연결되지 않습니다. aconnect 도구를 사용해 두 프로그램을 다리 놓아줘야 합니다.
1. 두 프로그램을 각각 다른 터미널에서 실행합니다.
2. 새 터미널에서 aconnect -lp를 입력하여 포트 번호를 확인합니다. (예: Sender가 128:0, Receiver가 129:0)
3. 두 포트를 연결합니다:
4. Bash
aconnect 128:0 129:0 # 128번 장치의 0번 포트를 129번의 0번으로 연결
5. Sender 터미널을 확인하면 메시지가 전송되고, Receiver 터미널에 노트 정보가 찍히는 것을 볼 수 있습니다.
코드 포인트 요약
- snd_seq_ev_set_subs(): 이 함수를 쓰면 특정 목적지 주소를 몰라도 내 포트를 '구독'하고 있는 모든 대상에게 이벤트를 뿌려줍니다. 매우 편리하죠.
- snd_seq_event_output_direct(): 버퍼링 없이 즉시 커널로 이벤트를 보낼 때 사용합니다.
- 구조체 접근: MIDI 데이터는 ev->data.note.note 처럼 공용체(Union) 구조를 통해 접근하므로, 이벤트 타입에 맞는 필드를 참조해야 합니다.
5.5 ALSA Sequencer와 소켓 프로그래밍
ALSA Sequencer와 소켓(Socket) 프로그래밍은 데이터를 주고받는 방식에서 비슷한 면이 많지만, 그 목적과 작동 원리에서 뚜렷한 차이가 있습니다.
이해하기 쉽게 핵심적인 차이점을 비교해 드릴게요.
1. 개념적 비교
| 구분 | 소켓 (Socket) 프로그래밍 | ALSA Sequencer |
| 비유 | 전화기/우편: 주소(IP/Port)를 통해 상대방에게 직접 데이터를 보냄 | 방송국/믹서: 채널을 맞춘 모든 사람에게 신호를 보내고 연결(Patch)함 |
| 주 데이터 | 모든 종류의 바이너리/텍스트 데이터 | MIDI 이벤트 (노트, 컨트롤러 정보 등) |
| 핵심 목적 | 프로세스 간 또는 네트워크 간 통신 | 멀티미디어 장치/앱 간의 타이밍 기반 데이터 교환 |
| 연결 방식 | Point-to-Point (1:1 또는 서버/클라이언트) | Subscription (구독 기반 N:N 연결) |
2. 주요 차이점 상세 설명
1) 주소 체계 (Addressing)
- 소켓: IP 주소와 포트 번호를 사용하여 목적지를 명확히 지정합니다. 데이터를 보내기 위해 상대방의 위치를 정확히 알아야 합니다.
- Sequencer: Client ID와 Port ID를 사용합니다. 하지만 소켓과 달리 "구독(Subscription)" 모델을 씁니다. 내가 데이터를 보낼 때 수신자가 누구인지 몰라도, 내 포트를 구독 중인 모든 대상에게 데이터가 전달됩니다.
2) 타이밍 관리 (Timing)
- 소켓: 데이터가 최대한 빨리 도착하는 것이 목표입니다. 전송 시점의 정확한 타이밍보다는 신뢰성과 속도가 중요합니다.
- Sequencer: **"언제(Timestamp)"**가 매우 중요합니다. "이 메시지를 1초 뒤에 실행해라"와 같은 스케줄링 기능을 커널 수준에서 지원합니다.
3) 연결의 유연성
- 소켓: 소스 코드 내에서 연결(Connect/Accept)이 이루어져야 합니다.
- Sequencer: 프로그램 외부에서 aconnect 같은 도구로 실행 중에 연결을 마음대로 바꿀 수 있습니다. 마치 가상의 오디오 케이블을 뺐다 끼웠다 하는 것과 같습니다.
3. 코드 관점에서의 비유
- 소켓은 마치 전용 회선을 까는 것과 같습니다.
-
C
connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); write(sockfd, buffer, len); - ALSA Sequencer는 게시판에 글을 남기는 것과 같습니다.
-
C
snd_seq_ev_set_subs(&ev); // 이 게시판(포트)을 보는 사람 다 들어라! snd_seq_event_output_direct(seq, &ev);
4. 실무 선택 기준
- 소켓을 써야 할 때: 장치 간 일반적인 데이터 전송, 웹 통신, 파일 전송 등 범용적인 통신이 필요할 때.
- ALSA Sequencer를 써야 할 때: 리눅스 환경에서 MIDI 악기 연동, 음악 앱 간의 동기화, 정밀한 타이밍 제어가 필요한 오디오 시스템 제어 시.
요약하자면
소켓은 "데이터를 정확한 곳에 배달하기 위한 통로"이고, ALSA Sequencer는 "MIDI 이벤트를 여러 앱이 공유하고 정확한 시간에 실행하기 위한 이벤트 버스"입니다.

'Embedded : : Linux > : : ALSA' 카테고리의 다른 글
| [ALSA] 7. HW Dep (Hardware Dependent) (0) | 2026.03.11 |
|---|---|
| [ALSA] 6. Timer API (0) | 2026.03.11 |
| [ALSA] 4. Control (Mixer) 인터페이스 (0) | 2026.03.10 |
| [ALSA] 3. PCM 서브시스템 (0) | 2026.03.10 |
| [ALSA] ALSA 아키텍처 개요 (0) | 2026.03.09 |