diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1fd2495 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${fileDirname}", + "env": {}, + "args": [], + "showLog": true + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 400d682..8c45ae1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ Arremi ===== -Arremi is a simple router from serial to MIDI +![Screen Shot](ScreenShot.png) + +Arremi is a simple driver/router from serial to MIDI Now only for OSX diff --git a/ScreenShot.png b/ScreenShot.png new file mode 100644 index 0000000..9dde4d6 Binary files /dev/null and b/ScreenShot.png differ diff --git a/backend/backend.go b/backend/backend.go new file mode 100644 index 0000000..c4ab429 --- /dev/null +++ b/backend/backend.go @@ -0,0 +1,55 @@ +package backend + +import ( + "io" + + "github.com/jacobsa/go-serial/serial" + "github.com/tonychee7000/Arremi/consts" +) + +var ( + // MidiDev is a global midi device. + MidiDev *MIDIDevice + + // MIDIError check this. if not nil, go exit + MIDIError error +) + +func init() { + MidiDev, MIDIError = NewMIDIDevice() +} + +// Run called by frontend. +func Run(chSerialName chan string, ch chan int, errCh chan error) { + serialName := <-chSerialName + + sPort, err := serial.Open(serial.OpenOptions{ + PortName: "/dev/" + serialName, + BaudRate: consts.SerialBaudrate, + DataBits: 8, + StopBits: 1, + ParityMode: serial.PARITY_NONE, + MinimumReadSize: 3, + }) + if err != nil { + errCh <- err + return + } + defer MidiDev.AllNoteOff() + defer sPort.Close() + + go func() { + _, err := io.Copy(MidiDev, sPort) + if err != nil { + errCh <- err + } + ch <- 1 + }() + + for { + select { + case <-ch: + return + } + } +} diff --git a/backend/backend_test.go b/backend/backend_test.go new file mode 100644 index 0000000..bb1c64d --- /dev/null +++ b/backend/backend_test.go @@ -0,0 +1,7 @@ +package backend + +import "testing" + +func TestBackendRun(t *testing.T) { + +} diff --git a/backend/midiDevice.go b/backend/midiDevice.go new file mode 100644 index 0000000..cfbd7da --- /dev/null +++ b/backend/midiDevice.go @@ -0,0 +1,50 @@ +package backend + +import ( + "github.com/tonychee7000/Arremi/consts" + midi "github.com/youpy/go-coremidi" +) + +// MIDIDevice implies a Writer interface. +type MIDIDevice struct { + client midi.Client + source midi.Source + Signal chan midi.Packet +} + +// NewMIDIDevice construction func +func NewMIDIDevice() (*MIDIDevice, error) { + var mididev = new(MIDIDevice) + err := mididev.Init() + return mididev, err +} + +// Init the client and source +func (midiDev *MIDIDevice) Init() error { + var err error + + midiDev.Signal = make(chan midi.Packet, 4096) + midiDev.client, err = midi.NewClient(consts.ClientName) + if err != nil { + return err + } + + midiDev.source, err = midi.NewSource(midiDev.client, consts.SourceName) + return err +} + +func (midiDev *MIDIDevice) Write(p []byte) (int, error) { + var pack = midi.NewPacket(p, 0) + midiDev.Signal <- pack + err := pack.Received(&(midiDev.source)) + return len(p), err +} + +// AllNoteOff I don't want panic! +func (midiDev *MIDIDevice) AllNoteOff() { + for i := 0; i < 16; i++ { + for j := 0; j < 128; j++ { + midiDev.Write([]byte{byte(0x90 + i), byte(j), 0}) + } + } +} diff --git a/consts/consts.go b/consts/consts.go new file mode 100644 index 0000000..040e0a1 --- /dev/null +++ b/consts/consts.go @@ -0,0 +1,15 @@ +package consts + +const ( + // WindowTitle for display + WindowTitle = "Arremi" + // ClientName for make a client + ClientName = "Arremi" + // SourceName for register Source + SourceName = "Arremi" + + MIDISignalOn = "⚪️" + MIDISignalOff = "⚫️" + + SerialBaudrate = 31250 +) diff --git a/debug b/debug new file mode 100755 index 0000000..4a96bee Binary files /dev/null and b/debug differ diff --git a/frontend/window.go b/frontend/window.go new file mode 100644 index 0000000..2caf2dd --- /dev/null +++ b/frontend/window.go @@ -0,0 +1,154 @@ +package frontend + +import ( + "fmt" + "time" + + "regexp" + + "github.com/andlabs/ui" + "github.com/tonychee7000/Arremi/consts" + "github.com/tonychee7000/Arremi/serialPort" + "github.com/tonychee7000/arremi/backend" +) + +// WindowMain is to show the UI +func WindowMain() { + // Let backend stop + //var ch = make(chan int) + + var errCh = make(chan error, 0) + var chCon = make(chan int, 1) + var chSerialName = make(chan string, 1) + var serialList []string + + window := ui.NewWindow(consts.WindowTitle, 300, 100, false) + + if backend.MIDIError != nil { + ui.MsgBox(window, "MIDI Failure", + fmt.Sprint("MIDI driver initialize failed: ", backend.MIDIError)) + ui.Quit() + } + + labelSerialSelection := ui.NewLabel("Serial Port") + serialSelection := ui.NewCombobox() + buttonRefresh := ui.NewButton("Refresh") + buttonRun := ui.NewCheckbox("Run") + labelMidiActive := ui.NewLabel("MIDI Active ") + labelMidiSignal := ui.NewLabel(consts.MIDISignalOff) + labelHint := ui.NewLabel( + fmt.Sprint( + "HINT:\nBaudrate of Arduino USB serial port should be set to ", + consts.SerialBaudrate, " by using\n\n\tSerial.begin(", consts.SerialBaudrate, ")", + ), + ) + + serialList, err := loadSerial(serialSelection, buttonRun) + + if err != nil { + ui.MsgBox(window, "Serial Failure", + fmt.Sprint("Cannot list serial ports for reason: ", err)) + ui.Quit() + } + + buttonRefresh.OnClicked(func(*ui.Button) { + serialList, err = loadSerial(serialSelection, buttonRun) + if err != nil { + ui.MsgBox(window, "Serial Failure", + fmt.Sprint("Cannot list serial ports for reason: ", err)) + ui.Quit() + } + }) + + buttonRun.OnToggled(func(*ui.Checkbox) { + if buttonRun.Checked() { + serialSelection.Disable() + buttonRefresh.Disable() + chSerialName <- serialList[serialSelection.Selected()] + } else { + serialSelection.Enable() + buttonRefresh.Enable() + chCon <- 1 + } + }) + + mainBox := ui.NewVerticalBox() + topBox := ui.NewHorizontalBox() + buttomBox := ui.NewHorizontalBox() + + topBox.Append(serialSelection, true) + topBox.Append(buttonRefresh, false) + topBox.Append(ui.NewLabel(" "), false) + topBox.Append(buttonRun, false) + + buttomBox.Append(labelSerialSelection, false) + buttomBox.Append(ui.NewHorizontalSeparator(), true) + buttomBox.Append(labelMidiActive, false) + buttomBox.Append(labelMidiSignal, false) + + mainBox.Append(ui.NewHorizontalBox(), true) + mainBox.Append(buttomBox, false) + mainBox.Append(topBox, false) + mainBox.Append(labelHint, true) + mainBox.Append(ui.NewHorizontalBox(), true) + + window.SetMargined(true) + window.SetChild(mainBox) + window.OnClosing(func(*ui.Window) bool { + if buttonRun.Checked() { + return false + } + ui.Quit() + return true + }) + + go func() { + for { + select { + case err := <-errCh: + ui.QueueMain(func() { + if err != nil && + !regexp.MustCompile("file already closed").MatchString(err.Error()) { + ui.MsgBox(window, "Backend Failure", fmt.Sprint(err)) + } + serialSelection.Enable() + buttonRefresh.Enable() + buttonRun.SetChecked(false) + labelMidiSignal.SetText(consts.MIDISignalOff) + }) + case <-backend.MidiDev.Signal: + ui.QueueMain(func() { + labelMidiSignal.SetText(consts.MIDISignalOn) + }) + case <-time.After(100 * time.Millisecond): + ui.QueueMain(func() { + labelMidiSignal.SetText(consts.MIDISignalOff) + }) + } + } + }() + + go func() { + for { + backend.Run(chSerialName, chCon, errCh) + } + }() + + window.Show() +} + +func loadSerial(serialSelection *ui.Combobox, buttonRun *ui.Checkbox) ([]string, error) { + serialList, err := serialPort.GetSerialPorts() + + if len(serialList) == 0 { + buttonRun.Disable() + serialSelection.Append("-- No Arduino Found --") + serialSelection.Disable() + } else { + for _, seiral := range serialList { + serialSelection.Append(seiral) + } + } + serialSelection.SetSelected(0) + return serialList, err +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..420ad8c --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + + "github.com/andlabs/ui" + "github.com/tonychee7000/Arremi/frontend" +) + +func main() { + defer func() { + fmt.Println("Arremi Exit.") + }() + err := ui.Main(frontend.WindowMain) + if err != nil { + fmt.Print("Got error:", err, "\n") + } +} diff --git a/serialPort/get_serial.go b/serialPort/get_serial.go new file mode 100644 index 0000000..5109b85 --- /dev/null +++ b/serialPort/get_serial.go @@ -0,0 +1,23 @@ +package serialPort + +import ( + "io/ioutil" + "regexp" +) + +// GetSerialPorts is to list all serial port for Arduino device. +func GetSerialPorts() ([]string, error) { + f, err := ioutil.ReadDir("/dev") + if err != nil { + return nil, err + } + + var fileList []string + for _, file := range f { + if regexp.MustCompile("^ttyACM([0-9]+)|^cu.usbmodem").MatchString(file.Name()) { + fileList = append(fileList, file.Name()) + } + } + + return fileList, nil +} diff --git a/serialPort/serialPort_test.go b/serialPort/serialPort_test.go new file mode 100644 index 0000000..fe25336 --- /dev/null +++ b/serialPort/serialPort_test.go @@ -0,0 +1,17 @@ +package serialPort + +import ( + "testing" +) + +func TestGetSerialPorts(T *testing.T) { + portList, err := GetSerialPorts() + if err != nil { + T.Error("ERROR: ", err, "\n") + } + T.Log("Show all serial ports") + for _, p := range portList { + T.Log("\t", p) + } +} +