본문 바로가기
코딩/Flutter

Flutter로 Midi 입력(피아노 등)을 받는 앱 만들기

by jsjin 2023. 8. 17.
728x90

맨 밑에 주의사항있어요!

 

Midi에 대한 설명은 -> https://jsjin.tistory.com/entry/Midi

 

Midi 데이터를 받기 위해서 flutter midi command라는 패키지를 사용할 것이다.

https://pub.dev/packages/flutter_midi_command

 

flutter_midi_command | Flutter Package

A Flutter plugin for sending and receiving MIDI messages between Flutter and physical and virtual MIDI devices. Wraps CoreMIDI and android.media.midi in a thin dart/flutter layer.

pub.dev

 

 

main_page에서 연결된 Midi 디바이스를 찾고 연결하면

Midi 디바이스의 값을 보여주는 화면인 midi_page로 넘어가는 간단한 앱이다.

파일 위치

main.dart

import 'package:flutter/material.dart';
import 'pages/main_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Midi Input Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MainPage(),
    );
  }
}

 

main_page.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_midi_command/flutter_midi_command.dart';
import 'midi_page.dart';

class MainPage extends StatefulWidget {
  const MainPage({
    super.key,
  });
  final String title = 'Flutter Midi Input Demo';

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  //Midi 데이터들을 담는 리스트들 입니다.
  List<String> stringofDeviceNames = [];
  List<String> stringofDeviceIds = [];
  List<MidiDevice>? devices = [];

  //시작시 연결된 디바이스들을 불러옵니다.
  @override
  void initState() {
    super.initState();
    bringMidiDevice();
  }
  
  //연결된 디바이스들을 불러오는 함수입니다.
  Future<void> bringMidiDevice() async {
    devices = await MidiCommand().devices;

    stringofDeviceNames =
        devices?.map((device) => device.name).toList() ?? []; //디바이스의 이름을 저장합니다.
    stringofDeviceIds =
        devices?.map((device) => device.id).toList() ?? []; //디바이스의 ID를 저장합니다.
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          const SizedBox(
            height: 24,
          ),
          
          //새로고침 버튼입니다.
          ElevatedButton(
            onPressed: () {
              setState(() {
                bringMidiDevice();
              });
            },
            child: const Text("Refresh"),
          ),
          
          //연결된 디바이스를 보여주는 화면입니다.
          Expanded(
            child: ListView.builder(
              itemCount: stringofDeviceNames.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(stringofDeviceNames[index]),
                  subtitle: Text(stringofDeviceIds[index]),
                  
                  //연결된 디바이스를 선택하면 해당 디바이스의 데이터와 함께 MidiPage로 이동합니다.
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => MidiPage(device: devices![index]),
                      ),
                    );
                  },
                );
              },
            ),
          ),
          //만약 연결된 디바이스가 없으면 아무것도 표시하지 않습니다.
          if (stringofDeviceNames.isEmpty)
            const Center(
              child: Text("None"),
            ),
        ],
      ),
    );
  }
}

 

midi_page.dart

import 'dart:async';
import 'dart:typed_data';
import 'package:automatic_sheet_music_page_turner/function/change_to_note_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_midi_command/flutter_midi_command.dart';

class MidiPage extends StatefulWidget {
  final MidiDevice device;

  const MidiPage({
    super.key,
    required this.device,
  });

  @override
  MidiPageState createState() => MidiPageState();
}

class MidiPageState extends State<MidiPage> {
  StreamSubscription<MidiPacket>? _midiSubscription;
  List<Uint8List> midiValues = [];
  late MidiDevice device;
  List<String> stringofNoteInfos = [];

  //시작시 MIDI 입력을 구독하여 MIDI 메시지 수신을 시작합니다.
  @override
  void initState() {
    super.initState();
    device = widget.device;
    subscribeToMidiInput();
  }

  void subscribeToMidiInput() {
    MidiCommand().connectToDevice(device);
    _midiSubscription =
        MidiCommand().onMidiDataReceived?.listen((MidiPacket packet) {
      setState(() {
        final Uint8List data =
            packet.data; // 수신된 MIDI 데이터를 Uint8List 형식으로 추출합니다.
        midiValues.add(data);
        stringofNoteInfos.add(
            convertToNoteInfo(data)); // 수신된 MIDI 데이터를 알기 쉬운 형식으로 변환하여 저장합니다.
        //convertToNoteInfo는 change_to_note_info.dart의 함수입니다.
      });
    });
  }

  //이전에 구독한 MIDI 이벤트의 구독을 취소합니다.
  @override
  void dispose() {
    _midiSubscription?.cancel();
    MidiCommand midiCommand = MidiCommand();
    midiCommand.disconnectDevice(device);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('MIDI Input Screen'),
      ),
      body: Column(
        children: [
        
          //수신된 가장 최근의 MIDI 메시지를 출력합니다.
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: Container(
              decoration: BoxDecoration(
                color: Colors.grey[200],
                borderRadius: BorderRadius.circular(12),
              ),
              alignment: Alignment.center,
              height: 150,
              child: stringofNoteInfos.isNotEmpty
                  ? Text(
                      stringofNoteInfos.last,
                      style: const TextStyle(fontSize: 20),
                    )
                  : Container(),
            ),
          ),
          
          //수신된 모든 MIDI 메시지를 출력합니다.
          Expanded(
            child: ListView.builder(
              reverse: true,
              itemCount: stringofNoteInfos.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(stringofNoteInfos[index]),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

 

change_to_note_info.dart

import 'dart:typed_data';

String convertToNoteInfo(Uint8List data) {
  String status = '';
  int syllable, octave;

  // 노트 상태와 채널 정보 추출
  int firstByte = data[0];
  status = (firstByte & 0xF0).toRadixString(16); // 첫 번째 바이트의 상위 4비트 추출

  syllable = data[1];

  // status에 따른 노트 누름 여부
  if (status == '90') {
    status = 'Note ON';
  } else if (status == '80') {
    status = 'Note OFF';
  } else {
    status = 'Unknown';
  }

  // syllable에 따른 옥타브 위치 변환
  octave = (syllable ~/ 12) - 1;

  // syllable에 따른 계이름 한국어 변환
  String syllableName = '';
  switch (syllable % 12) {
    case 0:
      syllableName = '도';
      break;
    case 1:
      syllableName = '도#';
      break;
    case 2:
      syllableName = '레';
      break;
    case 3:
      syllableName = '레#';
      break;
    case 4:
      syllableName = '미';
      break;
    case 5:
      syllableName = '파';
      break;
    case 6:
      syllableName = '파#';
      break;
    case 7:
      syllableName = '솔';
      break;
    case 8:
      syllableName = '솔#';
      break;
    case 9:
      syllableName = '라';
      break;
    case 10:
      syllableName = '라#';
      break;
    case 11:
      syllableName = '시';
      break;
    default:
      syllableName = 'Unknown';
      break;
  }

  return '상태: $status, 계이름: $syllableName, 옥타브: $octave';
}

 

앱 화면

 

핸드폰으로 Midi 입력을 받는 방법은 -> https://jsjin.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%95%B8%EB%93%9C%ED%8F%B0%EC%9C%BC%EB%A1%9C-Midi-%EC%9E%85%EC%B6%9C%EB%A0%A5-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

 

안드로이드 핸드폰으로 Midi 입출력 하는 방법

Midi 입출력이 필요한 작업이 있지만 전자 피아노 같은 Midi 장치가 없어 이를 대체할 방법을 찾아봤다. 안드로이드 공식 사이트에 따르면 Android 13부터 USB에 MIDI 2.0을 지원한다. (Midi 2.0은 최신 Midi

jsjin.tistory.com

 

 

주의사항

1. 패키지의 변동사항이 있으면 코드가 불안정해질 수 있습니다.

2. 안드로이드 스튜디오와 같이 에뮬레이터 사용하는 경우 midi 장치가 연결되지 않을 수 있습니다.

(실제 핸드폰에 연결하여 실행하는 것을 추천드립니다)

3. 안드로이드 환경에서만 테스트하였습니다.

728x90

'코딩 > Flutter' 카테고리의 다른 글

TextField 사용 시 Overflow 발생 해결 방안  (0) 2023.09.13
Flutter로 키보드(텍스트) 입력 받기  (0) 2023.09.13