├── .gitignore ├── .gitlab-ci.yml ├── AUTHORS ├── CONTRIBUTING ├── LICENSE ├── README.md └── v2 ├── .gitignore ├── TODO.md ├── cc.go ├── channel.go ├── channel_test.go ├── combined.go ├── combined_test.go ├── doc.go ├── drivers ├── .gitignore ├── README.md ├── driver.go ├── drivertest │ ├── drivers.go │ └── drivers_test.go ├── internal │ └── version │ │ ├── example_test.go │ │ ├── version.go │ │ └── version_test.go ├── midicat │ ├── midicat.go │ └── midicat_test.go ├── midicatdrv │ ├── LICENSE │ ├── README.md │ ├── doc.go │ ├── driver.go │ ├── driver_test.go │ ├── exec_unix.go │ ├── exec_windows.go │ ├── helpers.go │ ├── in.go │ └── out.go ├── port.go ├── reader.go ├── rtmididrv │ ├── .gitignore │ ├── .travis.yml │ ├── CONTRIBUTING │ ├── LICENSE │ ├── README.md │ ├── doc.go │ ├── driver.go │ ├── driver_test.go │ ├── imported │ │ ├── README.md │ │ └── rtmidi │ │ │ ├── LICENSE │ │ │ ├── RtMidi.cpp │ │ │ ├── RtMidi.h │ │ │ ├── rtmidi.go │ │ │ ├── rtmidi_c.cpp │ │ │ ├── rtmidi_c.h │ │ │ └── rtmidi_test.go │ ├── in.go │ └── out.go ├── testdrv │ ├── driver.go │ └── driver_test.go └── webmididrv │ ├── .gitignore │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── TODO.md │ ├── doc.go │ ├── driver.go │ ├── driver_test.go │ ├── helpers.go │ ├── in.go │ └── out.go ├── example_test.go ├── examples ├── .gitignore ├── logger │ └── main.go ├── looper │ └── main.go ├── main.go ├── simple │ └── main.go ├── smfplayer │ └── main.go ├── smfrecorder │ └── main.go ├── sysex │ └── main.go └── webmidi │ ├── Makefile │ ├── README.md │ ├── index.html │ ├── main.go │ ├── main_js.go │ └── wasm_exec.js ├── gm ├── doc.go ├── drumkits.go ├── instruments.go ├── percussion_keys.go └── reset.go ├── go.mod ├── helpers.go ├── internal ├── runningstatus │ ├── live_test.go │ └── runningstatus.go └── utils │ ├── midilib_test.go │ ├── utils.go │ └── vlq_test.go ├── io.go ├── listen.go ├── message.go ├── mmc ├── doc.go └── mmc.go ├── note.go ├── note_test.go ├── pitchbend_test.go ├── port.go ├── realtime.go ├── realtime_test.go ├── rpn_nrpn ├── handler.go ├── handler_test.go ├── helper.go ├── nrpn.go └── rpn.go ├── smf ├── chunk.go ├── doc.go ├── errors.go ├── example_test.go ├── helpers.go ├── json.go ├── json_test.go ├── key.go ├── key_test.go ├── message.go ├── meta.go ├── meta_test.go ├── meter_test.go ├── player │ ├── errors.go │ ├── player.go │ └── utils.go ├── reader.go ├── reader_test.go ├── smf.go ├── tempo_test.go ├── tempochanges.go ├── timeformat.go ├── track.go ├── writer.go └── writer_test.go ├── syscommon.go ├── syscommon_test.go ├── sysex.go ├── sysex ├── doc.go ├── manufacturer.go ├── nonrealtime.go ├── realtime.go └── sysex.go ├── sysex_test.go └── type.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | cover.html 17 | midi.debug 18 | midimessage/sysex/docs.md 19 | plan.md 20 | prepared-pull-request-for-awesome-go.txt 21 | realtime-midi-vs-midifiles.md 22 | todo.md 23 | specs 24 | *.wav 25 | examples/liverecord/liverecord 26 | examples/liverecord/recorded.mid 27 | examples/looper/looper 28 | recorder 29 | TODO.md 30 | *.sh 31 | slice-size.md 32 | .coverag.txt 33 | examples/filter 34 | examples/lowlevel 35 | examples/sysextest 36 | examples/tinygo 37 | .coverage.txt 38 | v1 39 | examples/examples 40 | tools/tools 41 | v2/drivers/portmididrv/imported/new/ 42 | v2/drivers/portmididrv/imported/pm_* 43 | v2/drivers/portmididrv/imported/porttime/ 44 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: golang:latest 2 | 3 | variables: 4 | REPO_NAME: gitlab.com/gomidi/midi/v2 5 | 6 | before_script: 7 | - go version 8 | - echo $CI_BUILD_REF 9 | - echo $CI_PROJECT_DIR 10 | - echo $GOPATH 11 | 12 | mingo: 13 | image: golang:1.24.2 14 | stage: test 15 | # when: manual 16 | script: 17 | - cd v2 18 | - apt-get update 19 | - apt-get install -y libasound2-dev 20 | # - apt-get install -y libportmidi-dev 21 | - go install gitlab.com/gomidi/tools/midicat@v1.0.4 22 | - go env 23 | - go fmt $(go list ./... | grep -v /vendor|imported/) 24 | - go vet $(go list ./... | grep -v /vendor|imported/) 25 | - go test $(go list ./... | grep -v /vendor/) 26 | 27 | format: 28 | stage: test 29 | # when: manual 30 | script: 31 | - cd v2 32 | - apt-get update 33 | - apt-get install -y libasound2-dev 34 | # - apt-get install -y libportmidi-dev 35 | - go install gitlab.com/gomidi/tools/midicat@v1.0.4 36 | - go env 37 | - go fmt $(go list ./... | grep -v /vendor|imported/) 38 | - go vet $(go list ./... | grep -v /vendor|imported/) 39 | - go test -race $(go list ./... | grep -v /vendor/) 40 | # - apt-cache search 'asound' 41 | # - apt-cache search 'portmidi' 42 | # - go fmt $(go list ./... | grep -v /drivers/) 43 | # - go vet $(go list ./... | grep -v /drivers/) 44 | # - go test -race $(go list ./... | grep -v /drivers/) 45 | 46 | stages: 47 | - test 48 | 49 | 50 | test-project: 51 | stage: test 52 | # when: manual 53 | script: 54 | - cd v2 55 | - apt-get update 56 | - apt-get install -y libasound2-dev 57 | # - apt-get install -y libportmidi-dev 58 | - go install gitlab.com/gomidi/tools/midicat@v1.0.4 59 | - go env 60 | - midicat ins 61 | - midicat outs 62 | - go test $(go list ./... | grep -v /vendor/) 63 | 64 | WindowsJob: 65 | stage: test 66 | # when: manual 67 | tags: 68 | # - windows 69 | - saas-windows-medium-amd64 70 | script: 71 | # we are in the powershell on windows 11, see https://stackoverflow.com/questions/74000780/echo-path-not-working-on-my-windows-11-instance 72 | # all env variables 73 | # - "gci env:" 74 | - md bin 75 | - "$env:Path = $env:Path + ';' + (Get-Location) + '\\bin'" 76 | - echo $env:Path 77 | # - bash -c "echo $PWD" 78 | - nuget install WGETWindows 79 | - nuget install 7-Zip.CommandLine 80 | - dir 7-Zip.CommandLine.18.1.0 81 | - dir WGETWindows.1.11.4 82 | - wget.exe --no-check-certificate --output-document=artifacts.zip https://gitlab.com/gomidi/tools/-/jobs/9861847349/artifacts/download?file_type=archive 83 | - 7z.exe e artifacts.zip 84 | - dir 85 | # - set PATH=%PATH%;%cd% 86 | - move midicat.exe bin/ 87 | - cd v2 88 | - go.exe test $(go list ./... | findstr /v "drivers") 89 | 90 | # no free runners 91 | #OSXJob: 92 | # stage: test 93 | # tags: 94 | # - osx 95 | # script: 96 | # - cd v2 97 | # - uname -a 98 | # - go test $(go list ./... | grep -v /drivers/) 99 | 100 | 101 | #OSXJob: 102 | # stage: test 103 | # tags: 104 | # - osx 105 | # script: 106 | # - uname -a 107 | # - go test ./... 108 | 109 | 110 | #build-project: 111 | # stage: build 112 | # script: 113 | # - OUTPUT="output" 114 | # - bash build-all.sh $OUTPUT $CI_PROJECT_DIR 115 | # 116 | # artifacts: 117 | # paths: 118 | # - artifacts/ 119 | 120 | #WindowsJob: 121 | # stage: test 122 | # tags: 123 | # - windows 124 | # script: 125 | # - go.exe test ./... 126 | 127 | #OSXJob: 128 | # stage: test 129 | # tags: 130 | # - osx 131 | # script: 132 | # - uname -a 133 | # - go test ./... 134 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Marc Rene Arns 2 | Joe Wass 3 | Zeal Wierslee 4 | William Sharkey 5 | Jonathan Thompson -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | All development activity takes place on Gitlab. 2 | 3 | If you think you've discovered a bug, or you would like 4 | to make a feature request please do this through Gitlab. 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marc Rene Arns 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # midi 2 | 3 | library for reading and writing of MIDI messages and SMF/MIDI files with Go. 4 | 5 | **Note: If you are reading this on Github, please note that the repo has moved to Gitlab (gitlab.com/gomidi/midi) and this is only a mirror.** 6 | 7 | - Go version: >= 1.24.2 8 | - OS/architectures: everywhere Go runs (tested on Linux and Windows). 9 | 10 | ## Installation 11 | 12 | ``` 13 | go get gitlab.com/gomidi/midi/v2@latest 14 | ``` 15 | 16 | ## Features 17 | 18 | This package provides a unified way to read and write "over the wire" MIDI data and MIDI files (SMF). 19 | 20 | - [x] implementation of complete MIDI standard ("cable" and SMF MIDI) 21 | - [x] unified Driver interface (see below) 22 | - [x] reading and optional writing with "running status" 23 | - [x] seamless integration with io.Reader and io.Writer 24 | - [x] no dependencies outside the standard library 25 | - [x] low overhead 26 | - [x] shortcuts for General MIDI, Sysex messages etc. 27 | - [x] CLI tools that use the library 28 | - [x] SMF files can be converted back and forth into a human readable JSON format 29 | 30 | ## Drivers 31 | 32 | For "cable" communication you need a `Driver` to connect with the MIDI system of your OS. 33 | Currently the following drivers available in the drivers subdirectory (all multi-platform): 34 | - rtmididrv based on rtmidi (requires CGO) 35 | - webmididrv based on the Web MIDI standard (produces webassembly) 36 | - midicatdrv based on the midicat binaries via piping (stdin / stdout) (no CGO needed) 37 | - testdrv for testing (no CGO needed) 38 | 39 | (there used to be a driver, based on portmidi, but this is not supported anymore) 40 | 41 | ### Examples 42 | 43 | ```go 44 | package main 45 | 46 | import ( 47 | "fmt" 48 | "time" 49 | 50 | "gitlab.com/gomidi/midi/v2" 51 | _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" // autoregisters driver 52 | ) 53 | 54 | func main() { 55 | defer midi.CloseDriver() 56 | 57 | in, err := midi.FindInPort("VMPK") 58 | if err != nil { 59 | fmt.Println("can't find VMPK") 60 | return 61 | } 62 | 63 | stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { 64 | var bt []byte 65 | var ch, key, vel uint8 66 | switch { 67 | case msg.GetSysEx(&bt): 68 | fmt.Printf("got sysex: % X\n", bt) 69 | case msg.GetNoteStart(&ch, &key, &vel): 70 | fmt.Printf("starting note %s on channel %v with velocity %v\n", midi.Note(key), ch, vel) 71 | case msg.GetNoteEnd(&ch, &key): 72 | fmt.Printf("ending note %s on channel %v\n", midi.Note(key), ch) 73 | default: 74 | // ignore 75 | } 76 | }, midi.UseSysEx()) 77 | 78 | if err != nil { 79 | fmt.Printf("ERROR: %s\n", err) 80 | return 81 | } 82 | 83 | time.Sleep(time.Second * 5) 84 | 85 | stop() 86 | } 87 | 88 | ``` 89 | 90 | ```go 91 | package main 92 | 93 | import ( 94 | "bytes" 95 | "fmt" 96 | 97 | "gitlab.com/gomidi/midi/v2" 98 | "gitlab.com/gomidi/midi/v2/gm" 99 | "gitlab.com/gomidi/midi/v2/smf" 100 | 101 | _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" // autoregisters driver 102 | ) 103 | 104 | func main() { 105 | defer midi.CloseDriver() 106 | 107 | fmt.Printf("outports:\n" + midi.GetOutPorts() + "\n") 108 | 109 | out, err := midi.FindOutPort("qsynth") 110 | if err != nil { 111 | fmt.Printf("can't find qsynth") 112 | return 113 | } 114 | 115 | // create a SMF 116 | rd := bytes.NewReader(mkSMF()) 117 | 118 | // read and play it 119 | smf.ReadTracksFrom(rd).Do(func(ev smf.TrackEvent) { 120 | fmt.Printf("track %v @%vms %s\n", ev.TrackNo, ev.AbsMicroSeconds/1000, ev.Message) 121 | }).Play(out) 122 | } 123 | 124 | // makes a SMF and returns the bytes 125 | func mkSMF() []byte { 126 | var ( 127 | bf bytes.Buffer 128 | clock = smf.MetricTicks(96) // resolution: 96 ticks per quarternote 960 is also common 129 | tr smf.Track 130 | ) 131 | 132 | // first track must have tempo and meter informations 133 | tr.Add(0, smf.MetaMeter(3, 4)) 134 | tr.Add(0, smf.MetaTempo(140)) 135 | tr.Add(0, smf.MetaInstrument("Brass")) 136 | tr.Add(0, midi.ProgramChange(0, gm.Instr_BrassSection.Value())) 137 | tr.Add(0, midi.NoteOn(0, midi.Ab(3), 120)) 138 | tr.Add(clock.Ticks8th(), midi.NoteOn(0, midi.C(4), 120)) 139 | // duration: a quarter note (96 ticks in our case) 140 | tr.Add(clock.Ticks4th()*2, midi.NoteOff(0, midi.Ab(3))) 141 | tr.Add(0, midi.NoteOff(0, midi.C(4))) 142 | tr.Close(0) 143 | 144 | // create the SMF and add the tracks 145 | s := smf.New() 146 | s.TimeFormat = clock 147 | s.Add(tr) 148 | s.WriteTo(&bf) 149 | return bf.Bytes() 150 | } 151 | 152 | ``` 153 | 154 | 155 | 156 | 157 | ## Documentation 158 | 159 | see https://pkg.go.dev/gitlab.com/gomidi/midi/v2 160 | 161 | ## License 162 | 163 | MIT (see LICENSE file) 164 | 165 | ## Credits 166 | 167 | Inspiration and low level code for MIDI reading (see internal midilib package) came from the http://github.com/afandian/go-midi package of Joe Wass which also helped as a starting point for the reading of SMF files. 168 | 169 | ## Alternatives 170 | 171 | Matt Aimonetti is also working on MIDI inside https://github.com/mattetti/audio but I didn't try it. 172 | -------------------------------------------------------------------------------- /v2/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /v2/TODO.md: -------------------------------------------------------------------------------- 1 | # sysex: implement 2 | 3 | SysEx commands carried on a MIDI 1.0 DIN cable may use the "dropped 4 | 0xF7" construction [MIDI]. In this coding method, the 0xF7 octet is 5 | dropped from the end of the SysEx command, and the status octet of 6 | the next MIDI command acts both to terminate the SysEx command and 7 | start the next command. 8 | 9 | Inbetween these two status bytes, any number of data bytes (all having bit #7 clear, ie, 0 to 127 value) may be sent. 10 | 11 | The purpose of the remaining data bytes, however many there may be, are determined by the manufacturer of a product. Typically, manufacturers follow the Manufacturer ID with a Model Number ID byte so that a device can not only determine that it's got a SysEx message for the correct manufacturer, but also has a SysEx message specifically for this model. Then, after the Model ID may be another byte that the device uses to determine what the SysEx message is supposed to be for, and therefore, how many more data bytes follow. Some manufacturers have a checksum byte, (usually right before the 0xF7) which is used to check the integrity of the message's transmission. 12 | 13 | The 0xF7 Status byte is dedicated to marking the end of a SysEx message. It should never occur without a preceding 0xF0 Status. In the event that a device experiences such a condition (ie, maybe the MIDI cable was connected during the transmission of a SysEx message), the device should ignore the 0xF7. 14 | 15 | Furthermore, although the 0xF7 is supposed to mark the end of a SysEx message, in fact, any status (except for Realtime Category messages) will cause a SysEx message to be considered "done" (ie, actually "aborted" is a better description since such a scenario indicates an abnormal MIDI condition). For example, if a 0x90 happened to be sent sometime after a 0xF0 (but before the 0xF7), then the SysEx message would be considered aborted at that point. It should be noted that, like all System Common messages, SysEx cancels any current running status. In other words, the next Voice Category message (after the SysEx message) must begin with a Status. 16 | 17 | # undefined syscommon and undefined Real-Time commands: implement 18 | 19 | [MIDI] reserves the undefined System Common commands 0xF4 and 0xF5 20 | and the undefined System Real-Time commands 0xF9 and 0xFD for future 21 | use. 22 | 23 | 24 | # make tests for midi channel messages 25 | 26 | 27 | 28 | # make transparent running status to explicit status reader; make it the default in listener, let it start listening at the first explicit status 29 | 30 | # make transparent running status writer 31 | 32 | # here is the question if it is not better to have some midi.Buffer that tracks status bytes for reading and writing. 33 | such a buffer could be used to convert to explicit status codes (which would be done inside a smf reader and a driver in port) 34 | or to compress with the help of running status (which would be done inside a smf writer and a driver out port) 35 | 36 | we need an interface with optional methods (=callbacks) for sysex messages (with are buffered by the drivers), 37 | realtime messages (with are send immediatly) and system common messages (which are also send immediatly). 38 | the default method receives channel messages (or could possibly receive any kind of message) 39 | 40 | # improve sysex 41 | 42 | # Pipelines / Builders 43 | 44 | # Test SMPTE in smf 45 | 46 | # Test midi clock etc. realtime and syscommon messages 47 | 48 | # Test sysex 49 | 50 | -------------------------------------------------------------------------------- /v2/cc.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | const ( 4 | Off uint8 = 0 // value meaning "off" 5 | On uint8 = 127 // value meaning "on" 6 | 7 | BankSelectMSB uint8 = 0 8 | ModulationWheelMSB uint8 = 1 9 | BreathControllerMSB uint8 = 2 10 | FootPedalMSB uint8 = 4 11 | PortamentoTimeMSB uint8 = 5 12 | DataEntryMSB uint8 = 6 13 | VolumeMSB uint8 = 7 14 | BalanceMSB uint8 = 8 15 | PanPositionMSB uint8 = 10 16 | ExpressionMSB uint8 = 11 17 | EffectControl1MSB uint8 = 12 18 | EffectControl2MSB uint8 = 13 19 | GeneralPurposeSlider1 uint8 = 16 20 | GeneralPurposeSlider2 uint8 = 17 21 | GeneralPurposeSlider3 uint8 = 18 22 | GeneralPurposeSlider4 uint8 = 19 23 | BankSelectLSB uint8 = 32 24 | ModulationWheelLSB uint8 = 33 25 | BreathControllerLSB uint8 = 34 26 | FootPedalLSB uint8 = 36 27 | PortamentoTimeLSB uint8 = 37 28 | DataEntryLSB uint8 = 38 29 | VolumeLSB uint8 = 39 30 | BalanceLSB uint8 = 40 31 | PanPositionLSB uint8 = 42 32 | ExpressionLSB uint8 = 43 33 | EffectControl1LSB uint8 = 44 34 | EffectControl2LSB uint8 = 45 35 | SoundVariation uint8 = 70 36 | SoundTimbre uint8 = 71 37 | SoundReleaseTime uint8 = 72 38 | SoundAttackTime uint8 = 73 39 | SoundBrightness uint8 = 74 40 | SoundControl6 uint8 = 75 41 | SoundControl7 uint8 = 76 42 | SoundControl8 uint8 = 77 43 | SoundControl9 uint8 = 78 44 | SoundControl10 uint8 = 79 45 | EffectsLevel uint8 = 91 46 | TremuloLevel uint8 = 92 47 | ChorusLevel uint8 = 93 48 | CelesteLevel uint8 = 94 49 | PhaserLevel uint8 = 95 50 | DataButtonIncrement uint8 = 96 51 | DataButtonDecrement uint8 = 97 52 | NonRegisteredParameterLSB uint8 = 98 53 | NonRegisteredParameterMSB uint8 = 99 54 | RegisteredParameterLSB uint8 = 100 55 | RegisteredParameterMSB uint8 = 101 56 | 57 | AllSoundOff uint8 = 120 // send it with value of 0/Off 58 | AllControllersOff uint8 = 121 // send it with value of 0/Off 59 | AllNotesOff uint8 = 123 // send it with value of 0/Off 60 | 61 | OmniModeOff uint8 = 124 // send it with value of 0/Off 62 | OmniModeOn uint8 = 125 // send it with value of 0 63 | 64 | MonoOperation uint8 = 126 65 | 66 | PolyOperation uint8 = 127 // send it with value of 0 67 | 68 | LocalKeyboardSwitch uint8 = 122 // send it with value of 127/On or 0/Off 69 | HoldPedalSwitch uint8 = 64 // send it with value of 127/On or 0/Off 70 | PortamentoSwitch uint8 = 65 // send it with value of 127/On or 0/Off 71 | SustenutoPedalSwitch uint8 = 66 // send it with value of 127/On or 0/Off 72 | 73 | SoftPedalSwitch uint8 = 67 // send it with value of 127/On or 0/Off 74 | LegatoPedalSwitch uint8 = 68 // send it with value of 127/On or 0/Off 75 | Hold2PedalSwitch uint8 = 69 // send it with value of 127/On or 0/Off 76 | GeneralPurposeButton1Switch uint8 = 80 // send it with value of 127/On or 0/Off 77 | GeneralPurposeButton2Switch uint8 = 81 // send it with value of 127/On or 0/Off 78 | GeneralPurposeButton3Switch uint8 = 82 // send it with value of 127/On or 0/Off 79 | GeneralPurposeButton4Switch uint8 = 83 // send it with value of 127/On or 0/Off 80 | ) 81 | 82 | // stolen from http://midi.teragonaudio.com/tech/midispec.htm 83 | var ControlChangeName = map[uint8]string{ 84 | 0: "Bank Select (MSB)", 85 | 1: "Modulation Wheel (MSB)", 86 | 2: "Breath controller (MSB)", 87 | 4: "Foot Pedal (MSB)", 88 | 5: "Portamento Time (MSB)", 89 | 6: "Data Entry (MSB)", 90 | 7: "Volume (MSB)", 91 | 8: "Balance (MSB)", 92 | 10: "Pan position (MSB)", 93 | 11: "Expression (MSB)", 94 | 12: "Effect Control 1 (MSB)", 95 | 13: "Effect Control 2 (MSB)", 96 | 16: "General Purpose Slider 1", 97 | 17: "General Purpose Slider 2", 98 | 18: "General Purpose Slider 3", 99 | 19: "General Purpose Slider 4", 100 | 32: "Bank Select (LSB)", 101 | 33: "Modulation Wheel (LSB)", 102 | 34: "Breath controller (LSB)", 103 | 36: "Foot Pedal (LSB)", 104 | 37: "Portamento Time (LSB)", 105 | 38: "Data Entry (LSB)", 106 | 39: "Volume (LSB)", 107 | 40: "Balance (LSB)", 108 | 42: "Pan position (LSB)", 109 | 43: "Expression (LSB)", 110 | 44: "Effect Control 1 (LSB)", 111 | 45: "Effect Control 2 (LSB)", 112 | 64: "Hold Pedal (on/off)", 113 | 65: "Portamento (on/off)", 114 | 66: "Sustenuto Pedal (on/off)", 115 | 67: "Soft Pedal (on/off)", 116 | 68: "Legato Pedal (on/off)", 117 | 69: "Hold 2 Pedal (on/off)", 118 | 70: "Sound Variation", 119 | 71: "Sound Timbre", 120 | 72: "Sound Release Time", 121 | 73: "Sound Attack Time", 122 | 74: "Sound Brightness", 123 | 75: "Sound Control 6", 124 | 76: "Sound Control 7", 125 | 77: "Sound Control 8", 126 | 78: "Sound Control 9", 127 | 79: "Sound Control 10", 128 | 80: "General Purpose Button 1 (on/off)", 129 | 81: "General Purpose Button 2 (on/off)", 130 | 82: "General Purpose Button 3 (on/off)", 131 | 83: "General Purpose Button 4 (on/off)", 132 | 91: "Effects Level", 133 | 92: "Tremulo Level", 134 | 93: "Chorus Level", 135 | 94: "Celeste Level", 136 | 95: "Phaser Level", 137 | 96: "Data Button increment", 138 | 97: "Data Button decrement", 139 | 98: "Non-registered Parameter (LSB)", 140 | 99: "Non-registered Parameter (MSB)", 141 | 100: "Registered Parameter (LSB)", 142 | 101: "Registered Parameter (MSB)", 143 | 120: "All Sound Off", 144 | 121: "All Controllers Off", 145 | 122: "Local Keyboard (on/off)", 146 | 123: "All Notes Off", 147 | 124: "Omni Mode Off", 148 | 125: "Omni Mode On", 149 | 126: "Mono Operation", 150 | 127: "Poly Operation", 151 | } 152 | -------------------------------------------------------------------------------- /v2/channel.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "gitlab.com/gomidi/midi/v2/internal/utils" 7 | ) 8 | 9 | const ( 10 | // PitchReset is the pitch bend value to reset the pitch wheel to zero 11 | PitchReset = 0 12 | 13 | // PitchLowest is the lowest possible value of the pitch bending 14 | PitchLowest = -8192 15 | 16 | // PitchHighest is the highest possible value of the pitch bending 17 | PitchHighest = 8191 18 | ) 19 | 20 | // Pitchbend returns a pitch bend message. 21 | // If value is > 8191 (max), it will be set to 8191. If value is < -8192, it will be set to -8192. 22 | // A value of 0 is considered as neutral position. 23 | func Pitchbend(channel uint8, value int16) Message { 24 | if channel > 15 { 25 | channel = 15 26 | } 27 | 28 | if value > PitchHighest { 29 | value = PitchHighest 30 | } 31 | 32 | if value < PitchLowest { 33 | value = PitchLowest 34 | } 35 | 36 | r := utils.MsbLsbSigned(value) 37 | 38 | var b = make([]byte, 2) 39 | 40 | binary.BigEndian.PutUint16(b, r) 41 | return channelMessage2(channel, 14, b[0], b[1]) 42 | } 43 | 44 | // PolyAfterTouch returns a polyphonic aftertouch message. 45 | func PolyAfterTouch(channel, key, pressure uint8) Message { 46 | if channel > 15 { 47 | channel = 15 48 | } 49 | 50 | if key > 127 { 51 | key = 127 52 | } 53 | if pressure > 127 { 54 | pressure = 127 55 | } 56 | return channelMessage2(channel, 10, key, pressure) 57 | } 58 | 59 | // NoteOn returns a note on message. 60 | func NoteOn(channel, key, velocity uint8) Message { 61 | if channel > 15 { 62 | channel = 15 63 | } 64 | 65 | if key > 127 { 66 | key = 127 67 | } 68 | if velocity > 127 { 69 | velocity = 127 70 | } 71 | return channelMessage2(channel, 9, key, velocity) 72 | } 73 | 74 | // NoteOffVelocity returns a note off message with velocity. 75 | func NoteOffVelocity(channel, key, velocity uint8) Message { 76 | if channel > 15 { 77 | channel = 15 78 | } 79 | 80 | if key > 127 { 81 | key = 127 82 | } 83 | if velocity > 127 { 84 | velocity = 127 85 | } 86 | return channelMessage2(channel, 8, key, velocity) 87 | } 88 | 89 | // NoteOff returns a note off message. 90 | func NoteOff(channel, key uint8) Message { 91 | if channel > 15 { 92 | channel = 15 93 | } 94 | 95 | if key > 127 { 96 | key = 127 97 | } 98 | return channelMessage2(channel, 8, key, 0) 99 | } 100 | 101 | // ProgramChange returns a program change message. 102 | func ProgramChange(channel, program uint8) Message { 103 | if channel > 15 { 104 | channel = 15 105 | } 106 | 107 | if program > 127 { 108 | program = 127 109 | } 110 | return channelMessage1(channel, 12, program) 111 | } 112 | 113 | // AfterTouch returns an aftertouch message. 114 | func AfterTouch(channel, pressure uint8) Message { 115 | if channel > 15 { 116 | channel = 15 117 | } 118 | 119 | if pressure > 127 { 120 | pressure = 127 121 | } 122 | return channelMessage1(channel, 13, pressure) 123 | } 124 | 125 | // ControlChange returns a control change message. 126 | func ControlChange(channel, controller, value uint8) Message { 127 | if channel > 15 { 128 | channel = 15 129 | } 130 | 131 | if controller > 127 { 132 | controller = 127 133 | } 134 | if value > 127 { 135 | value = 127 136 | } 137 | return channelMessage2(channel, 11, controller, value) 138 | } 139 | -------------------------------------------------------------------------------- /v2/channel_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestChannelString(t *testing.T) { 10 | 11 | tests := []struct { 12 | input []byte 13 | expected string 14 | }{ 15 | { 16 | AfterTouch(1, 120), 17 | "AfterTouch channel: 1 pressure: 120", 18 | }, 19 | { 20 | ControlChange(8, 7, 110), 21 | //"ControlChange channel: 8 controller: 7 (\"Volume (MSB)\") value 110", 22 | "ControlChange channel: 8 controller: 7 value: 110", 23 | }, 24 | { 25 | NoteOn(2, 100, 80), 26 | "NoteOn channel: 2 key: 100 velocity: 80", 27 | }, 28 | { 29 | NoteOff(3, 80), 30 | "NoteOff channel: 3 key: 80", 31 | }, 32 | { 33 | NoteOffVelocity(4, 80, 20), 34 | "NoteOff channel: 4 key: 80 velocity: 20", 35 | }, 36 | { 37 | Pitchbend(4, 300), 38 | "PitchBend channel: 4 pitch: 300 (8492)", 39 | }, 40 | { 41 | PolyAfterTouch(4, 86, 109), 42 | "PolyAfterTouch channel: 4 key: 86 pressure: 109", 43 | }, 44 | { 45 | ProgramChange(4, 83), 46 | "ProgramChange channel: 4 program: 83", 47 | }, 48 | 49 | // too high values 50 | { 51 | AfterTouch(1, 130), 52 | "AfterTouch channel: 1 pressure: 127", 53 | }, 54 | { 55 | ControlChange(8, 137, 130), 56 | //"ControlChange channel: 8 controller: 127 (\"Poly Operation\") value 127", 57 | "ControlChange channel: 8 controller: 127 value: 127", 58 | }, 59 | { 60 | NoteOn(2, 130, 130), 61 | "NoteOn channel: 2 key: 127 velocity: 127", 62 | }, 63 | { 64 | NoteOff(3, 180), 65 | "NoteOff channel: 3 key: 127", 66 | }, 67 | { 68 | NoteOffVelocity(4, 180, 220), 69 | "NoteOff channel: 4 key: 127 velocity: 127", 70 | }, 71 | { 72 | Pitchbend(4, 12300), 73 | "PitchBend channel: 4 pitch: 8191 (16383)", 74 | }, 75 | { 76 | PolyAfterTouch(4, 186, 190), 77 | "PolyAfterTouch channel: 4 key: 127 pressure: 127", 78 | }, 79 | { 80 | ProgramChange(4, 183), 81 | "ProgramChange channel: 4 program: 127", 82 | }, 83 | } 84 | 85 | for _, test := range tests { 86 | 87 | var bf bytes.Buffer 88 | 89 | bf.WriteString(Message(test.input).String()) 90 | 91 | if got, want := bf.String(), test.expected; got != want { 92 | t.Errorf("got: %#v; wanted %#v", got, want) 93 | } 94 | } 95 | 96 | } 97 | 98 | func TestChannelRaw(t *testing.T) { 99 | 100 | tests := []struct { 101 | input []byte 102 | expected string 103 | }{ 104 | { // 0 105 | AfterTouch(1, 120), 106 | "D1 78", 107 | }, 108 | { // 1 109 | ControlChange(8, 7, 110), 110 | "B8 07 6E", 111 | }, 112 | { // 2 113 | NoteOn(2, 100, 80), 114 | "92 64 50", 115 | }, 116 | { // 3 117 | NoteOff(3, 80), 118 | "83 50 00", 119 | }, 120 | { 121 | NoteOffVelocity(4, 80, 20), 122 | "84 50 14", 123 | }, 124 | { 125 | Pitchbend(4, 300), 126 | "E4 2C 42", 127 | }, 128 | { 129 | PolyAfterTouch(4, 86, 109), 130 | "A4 56 6D", 131 | }, 132 | { 133 | ProgramChange(4, 83), 134 | "C4 53", 135 | }, 136 | } 137 | 138 | for i, test := range tests { 139 | 140 | var bf bytes.Buffer 141 | 142 | bf.Write(test.input) 143 | 144 | if got, want := fmt.Sprintf("% X", bf.Bytes()), test.expected; got != want { 145 | t.Errorf("[%v] got: %#v; wanted %#v", i, got, want) 146 | } 147 | } 148 | 149 | } 150 | 151 | func TestGetChannel(t *testing.T) { 152 | 153 | tests := []struct { 154 | input []byte 155 | expected int 156 | }{ 157 | { // 0 158 | AfterTouch(1, 120), 159 | 1, 160 | }, 161 | { // 1 162 | ControlChange(8, 7, 110), 163 | 8, 164 | }, 165 | { // 2 166 | NoteOn(2, 100, 80), 167 | 2, 168 | }, 169 | { // 3 170 | NoteOff(3, 80), 171 | 3, 172 | }, 173 | { 174 | NoteOffVelocity(4, 80, 20), 175 | 4, 176 | }, 177 | { 178 | Pitchbend(4, 300), 179 | 4, 180 | }, 181 | { 182 | PolyAfterTouch(4, 86, 109), 183 | 4, 184 | }, 185 | { 186 | ProgramChange(4, 83), 187 | 4, 188 | }, 189 | } 190 | 191 | for i, test := range tests { 192 | 193 | var ch uint8 194 | Message(test.input).GetChannel(&ch) 195 | 196 | if got, want := int(ch), test.expected; got != want { 197 | t.Errorf("[%v] got: %v; wanted %v", i, got, want) 198 | } 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /v2/combined.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | // ResetChannel "resets" channel to some established defaults 4 | /* 5 | bank select -> bank 6 | program change -> prog 7 | cc all controllers off 8 | cc volume -> 100 9 | cc expression -> 127 10 | cc hold pedal -> off 11 | cc pan position -> 64 12 | cc RPN pitch bend sensitivity -> 2 (semitones) 13 | */ 14 | func ResetChannel(ch uint8, bank uint8, prog uint8) []Message { 15 | var msgs = []Message{ 16 | ControlChange(ch, BankSelectMSB, bank), 17 | ProgramChange(ch, prog), 18 | ControlChange(ch, AllControllersOff, 0), 19 | ControlChange(ch, VolumeMSB, 100), 20 | ControlChange(ch, ExpressionMSB, 127), 21 | ControlChange(ch, HoldPedalSwitch, 0), 22 | ControlChange(ch, PanPositionMSB, 64), 23 | } 24 | 25 | //err := PitchBendSensitivityRPN(w, 2, 0) 26 | 27 | return msgs 28 | } 29 | 30 | // SilenceChannel sends a note off message for every running note 31 | // on the given channel. If the channel is -1, every channel is affected. 32 | // If note consolidation is switched off, notes aren't tracked and therefor 33 | // every possible note will get a note off. This could also be enforced by setting 34 | // force to true. If force is true, additionally the cc messages AllNotesOff (123) and AllSoundOff (120) 35 | // are send. 36 | // If channel is > 15, the method panics. 37 | // The error or not error of the last write is returned. 38 | // (i.e. it does not return on the first error, but tries everything instead to make it silent) 39 | func SilenceChannel(ch int8) (out []Message) { 40 | if ch > 15 { 41 | panic("invalid channel number") 42 | } 43 | 44 | // single channel 45 | if ch >= 0 { 46 | out = silentium(uint8(ch)) 47 | return 48 | } 49 | 50 | // all channels 51 | for c := 0; c < 16; c++ { 52 | out = append(out, silentium(uint8(c))...) 53 | } 54 | return 55 | } 56 | 57 | // silentium for a single channel 58 | func silentium(ch uint8) (out []Message) { 59 | out = append(out, ControlChange(ch, AllNotesOff, 0)) 60 | out = append(out, ControlChange(ch, AllSoundOff, 0)) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /v2/combined_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestResetChannel(t *testing.T) { 10 | 11 | msgs := ResetChannel(2, 5, 7) 12 | 13 | var bd strings.Builder 14 | 15 | for _, msg := range msgs { 16 | bd.WriteString(fmt.Sprintf("%s\n", msg.String())) 17 | } 18 | 19 | expected := strings.TrimSpace(` 20 | ControlChange channel: 2 controller: 0 value: 5 21 | ProgramChange channel: 2 program: 7 22 | ControlChange channel: 2 controller: 121 value: 0 23 | ControlChange channel: 2 controller: 7 value: 100 24 | ControlChange channel: 2 controller: 11 value: 127 25 | ControlChange channel: 2 controller: 64 value: 0 26 | ControlChange channel: 2 controller: 10 value: 64 27 | `) 28 | 29 | got := strings.TrimSpace(bd.String()) 30 | 31 | if got != expected { 32 | t.Errorf("got: \n%s\nexpected:\n%s\n", got, expected) 33 | } 34 | 35 | } 36 | 37 | func TestSilenceChannelSingle(t *testing.T) { 38 | 39 | msgs := SilenceChannel(4) 40 | 41 | var bd strings.Builder 42 | 43 | for _, msg := range msgs { 44 | bd.WriteString(fmt.Sprintf("%s\n", msg.String())) 45 | } 46 | 47 | expected := strings.TrimSpace(` 48 | ControlChange channel: 4 controller: 123 value: 0 49 | ControlChange channel: 4 controller: 120 value: 0 50 | `) 51 | 52 | got := strings.TrimSpace(bd.String()) 53 | 54 | if got != expected { 55 | t.Errorf("got: \n%s\nexpected:\n%s\n", got, expected) 56 | } 57 | 58 | } 59 | 60 | func TestSilenceChannelAll(t *testing.T) { 61 | 62 | msgs := SilenceChannel(-1) 63 | 64 | var bd strings.Builder 65 | 66 | for _, msg := range msgs { 67 | bd.WriteString(fmt.Sprintf("%s\n", msg.String())) 68 | } 69 | 70 | expected := strings.TrimSpace(` 71 | ControlChange channel: 0 controller: 123 value: 0 72 | ControlChange channel: 0 controller: 120 value: 0 73 | ControlChange channel: 1 controller: 123 value: 0 74 | ControlChange channel: 1 controller: 120 value: 0 75 | ControlChange channel: 2 controller: 123 value: 0 76 | ControlChange channel: 2 controller: 120 value: 0 77 | ControlChange channel: 3 controller: 123 value: 0 78 | ControlChange channel: 3 controller: 120 value: 0 79 | ControlChange channel: 4 controller: 123 value: 0 80 | ControlChange channel: 4 controller: 120 value: 0 81 | ControlChange channel: 5 controller: 123 value: 0 82 | ControlChange channel: 5 controller: 120 value: 0 83 | ControlChange channel: 6 controller: 123 value: 0 84 | ControlChange channel: 6 controller: 120 value: 0 85 | ControlChange channel: 7 controller: 123 value: 0 86 | ControlChange channel: 7 controller: 120 value: 0 87 | ControlChange channel: 8 controller: 123 value: 0 88 | ControlChange channel: 8 controller: 120 value: 0 89 | ControlChange channel: 9 controller: 123 value: 0 90 | ControlChange channel: 9 controller: 120 value: 0 91 | ControlChange channel: 10 controller: 123 value: 0 92 | ControlChange channel: 10 controller: 120 value: 0 93 | ControlChange channel: 11 controller: 123 value: 0 94 | ControlChange channel: 11 controller: 120 value: 0 95 | ControlChange channel: 12 controller: 123 value: 0 96 | ControlChange channel: 12 controller: 120 value: 0 97 | ControlChange channel: 13 controller: 123 value: 0 98 | ControlChange channel: 13 controller: 120 value: 0 99 | ControlChange channel: 14 controller: 123 value: 0 100 | ControlChange channel: 14 controller: 120 value: 0 101 | ControlChange channel: 15 controller: 123 value: 0 102 | ControlChange channel: 15 controller: 120 value: 0 103 | `) 104 | 105 | got := strings.TrimSpace(bd.String()) 106 | 107 | if got != expected { 108 | t.Errorf("got: \n%s\nexpected:\n%s\n", got, expected) 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /v2/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Marc René Arns. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package midi helps with reading and writing of MIDI messages. 7 | 8 | A `Message` is a slice of bytes with some methods to get the data. 9 | Any complete midi message (i.e. without "running status") can be interpreted as a Message, by converting the type. 10 | The data can be retrieved with the corresponding Get* method. 11 | 12 | // the raw bytes for a noteon message on channel 1 (2nd channel) for the key 60 with velocity of 120 13 | var b = []byte{0x91, 0x3C, 0x78} 14 | 15 | // convert to Message type 16 | msg := midi.Message(b) 17 | 18 | var channel, key, velocity uint8 19 | if msg.GetNoteOn(&channel, &key, &velocity) { 20 | fmt.Printf("got %s: channel: %v key: %v, velocity: %v\n", msg.Type(), channel, key, velocity) 21 | } 22 | 23 | Received messages can be categorized by their type, e.g. 24 | 25 | switch msg.Type() { 26 | case midi.NoteOnMsg, midi.NoteOffMsg: 27 | // do something 28 | case midi.ControlChangeMsg: 29 | // do some other thing 30 | default: 31 | if msg.Is(midi.RealTimeMsg) || msg.Is(midi.SysCommonMsg) || msg.Is(midi.SysExMsg) { 32 | // ignore 33 | } 34 | } 35 | 36 | A new message is created by the corresponding function, e.g. 37 | 38 | msg := midi.NoteOn(1, 60, 120) // channel, key, velocity 39 | fmt.Printf("% X\n", []byte(msg)) // prints 91 3C 78 40 | 41 | Sending and retrieving of midi data is done via drivers. The drivers take care of converting a "running status" into a full status. 42 | Most of the time, a single driver is sufficient. 43 | Therefor it is handy, that the drivers autoregister themself. This also makes it easy to exchange a driver if needed (see the example). 44 | 45 | Different cross plattform implementations of the `Driver` interface can be found in the `drivers` subdirectory. 46 | 47 | The `smf` subpackage helps with writing to and reading from `Simple MIDI Files` (SMF) (see https://pkg.go.dev/gitlab.com/gomidi/midi/v2/smf). 48 | 49 | The `tools` subdirectory provides command line tools and libraries based on this library. 50 | */ 51 | package midi 52 | -------------------------------------------------------------------------------- /v2/drivers/.gitignore: -------------------------------------------------------------------------------- 1 | amididrv 2 | -------------------------------------------------------------------------------- /v2/drivers/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Drivers for gomidi 3 | 4 | All drivers must obey the following rules. If not, it is considered a bug. 5 | 6 | 1. Implement the midi.Driver, midi.In and midi.Out interfaces 7 | 2. Autoregister via midi.RegisterDriver(drv) within init function 8 | 3. The String() method must return a unique and indentifying string 9 | 4. The driver expects multiple goroutines to use its writing and reading methods and locks accordingly (to be threadsafe). 10 | 5. The midi.In ports respects any running status bytes when converting to midi.Message(s) 11 | 6. The midi.Out ports accept running status bytes. 12 | 7. The New function may take optional driver specific arguments. These are all organized as functional optional arguments. 13 | 8. The midi.In port is responsible for buffering of sysex data. Only complete sysex data is passed to the listener. 14 | 9. The midi In port must check the ListenConfig and act accordingly. 15 | 10. incomplete sysex data must be cached inside the sender and flushed, if the data is complete. 16 | 17 | ## Reason for the timestamp choice of the listener callback 18 | 19 | 1. we don't want floating point, but integers of small fractions of time (easier to calculate with). 20 | 2. not every driver tracks a delta timing. the ones that don't should indicate with a -1. also some underlying drivers 21 | use floats for delta timing, so we can't be sure to don't get negative values. 22 | 3. for the size, we find that int32 is large enough, if we take a reasonable resolution 23 | of 1 millisecond. Then we get 24 | max int32 = 2147483647 / 10 (ms) / 1000 (sec) / 60 (min) / 60 (hours) / 24 = 2,48 days 25 | of maximal duration when converting to absolute timing (starting from the first message), which should be 26 | long enough for a midi recording. 27 | int64 would double the needed ressources for no real benefit. 28 | -------------------------------------------------------------------------------- /v2/drivers/driver.go: -------------------------------------------------------------------------------- 1 | package drivers 2 | 3 | var ( 4 | firstDriver string 5 | 6 | // REGISTRY is the registry for MIDI drivers 7 | REGISTRY = map[string]Driver{} 8 | ) 9 | 10 | // RegisterDriver register a driver 11 | func Register(d Driver) { 12 | if len(REGISTRY) == 0 { 13 | firstDriver = d.String() 14 | } 15 | REGISTRY[d.String()] = d 16 | } 17 | 18 | // Get returns the first available driver 19 | func Get() Driver { 20 | if len(REGISTRY) == 0 { 21 | return nil 22 | } 23 | return REGISTRY[firstDriver] 24 | } 25 | 26 | // Close closes the first available driver 27 | func Close() { 28 | d := Get() 29 | if d != nil { 30 | d.Close() 31 | } 32 | } 33 | 34 | // Driver is a driver for MIDI connections. 35 | // It may provide the timing delta to the previous message in micro seconds. 36 | // It must send the given MIDI data immediately. 37 | type Driver interface { 38 | 39 | // Ins returns the available MIDI input ports. 40 | Ins() ([]In, error) 41 | 42 | // Outs returns the available MIDI output ports. 43 | Outs() ([]Out, error) 44 | 45 | // String returns the name of the driver. 46 | String() string 47 | 48 | // Close closes the driver. Must be called for cleanup at the end of a session. 49 | Close() error 50 | } 51 | -------------------------------------------------------------------------------- /v2/drivers/drivertest/drivers_test.go: -------------------------------------------------------------------------------- 1 | package drivertest 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestMkWithRunningStatus(t *testing.T) { 10 | messages := mkWithRunningStatus() 11 | bts := bytes.Join(messages, []byte{}) 12 | 13 | got := fmt.Sprintf("% X", bts) 14 | 15 | expected := `92 41 78 37 78 41 00 91 41 14 92 37 00 91 41 00` 16 | 17 | if got != expected { 18 | t.Errorf("\nexpected: %q\n got: %q", expected, got) 19 | } 20 | 21 | } 22 | 23 | func TestMkSysex(t *testing.T) { 24 | got := fmt.Sprintf("% X", mkSysex()) 25 | 26 | expected := `F0 7E 02 09 01 F7` 27 | 28 | if got != expected { 29 | t.Errorf("\nexpected: %q\n got: %q", expected, got) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /v2/drivers/internal/version/example_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func Example() { 8 | 9 | var v1 = "v1.1.0" 10 | var v2 = "1.0.4" 11 | var invalid = ".1.0" 12 | 13 | vers1, err := Parse(v1) 14 | 15 | if err != nil { 16 | fmt.Printf("could not parse version %q", v1) 17 | } 18 | 19 | vers2, err := Parse(v2) 20 | 21 | if err != nil { 22 | fmt.Printf("could not parse version %q", v1) 23 | } 24 | 25 | if vers2.Less(*vers1) { 26 | fmt.Printf("version %s < version %s\n", vers2, vers1) 27 | } 28 | 29 | vers2.Minor = 1 30 | 31 | if vers1.Less(*vers2) { 32 | fmt.Printf("version %s < version %s\n", vers1, vers2) 33 | } 34 | 35 | last := Versions{vers1, vers2}.Sort().Last() 36 | 37 | fmt.Printf("last version is %s\n", last) 38 | 39 | _, err = Parse(invalid) 40 | 41 | if err != nil { 42 | fmt.Printf("could not parse version %q", invalid) 43 | } 44 | 45 | // Output: 46 | // version 1.0.4 < version 1.1.0 47 | // version 1.1.0 < version 1.1.4 48 | // last version is 1.1.4 49 | // could not parse version ".1.0" 50 | } 51 | -------------------------------------------------------------------------------- /v2/drivers/internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Version represents a version (e.g. a release version) 11 | type Version struct { 12 | Major uint16 13 | Minor uint16 14 | Patch uint16 15 | } 16 | 17 | // String returns the string representation of the version (without a preceeding v 18 | func (v Version) String() string { 19 | return fmt.Sprintf("%v.%v.%v", v.Major, v.Minor, v.Patch) 20 | } 21 | 22 | // Equals is true when the version exactly equals the given version 23 | func (v Version) Equals(o Version) bool { 24 | return v.Major == o.Major && v.Minor == o.Minor && v.Patch == o.Patch 25 | } 26 | 27 | // EqualsMajor is true when the version equals the given version on the Major 28 | func (v Version) EqualsMajor(o Version) bool { 29 | return v.Major == o.Major 30 | } 31 | 32 | // EqualsMinor is true when the version equals the given version on the Major and Minor 33 | func (v Version) EqualsMinor(o Version) bool { 34 | return v.Major == o.Major && v.Minor == o.Minor 35 | } 36 | 37 | // Less returns true, if the Version is smaller than the given one. 38 | func (v Version) Less(o Version) bool { 39 | if v.Major != o.Major { 40 | return v.Major < o.Major 41 | } 42 | 43 | if v.Minor != o.Minor { 44 | return v.Minor < o.Minor 45 | } 46 | 47 | return v.Patch < o.Patch 48 | } 49 | 50 | // Parse parses the version out of the given string. Valid strings are "v0.0.1" or "1.0" or "12" etc. 51 | func Parse(v string) (*Version, error) { 52 | v = strings.TrimLeft(v, "v") 53 | nums := strings.Split(v, ".") 54 | var ns = make([]uint16, len(nums)) 55 | 56 | var ver Version 57 | 58 | for i, n := range nums { 59 | nn, err := strconv.Atoi(n) 60 | if err != nil { 61 | return nil, err 62 | } 63 | if nn < 0 { 64 | return nil, fmt.Errorf("number must not be < 0") 65 | } 66 | ns[i] = uint16(nn) 67 | } 68 | 69 | switch len(ns) { 70 | case 1: 71 | if ns[0] == 0 { 72 | return nil, fmt.Errorf("invalid version %q", v) 73 | } 74 | ver.Major = ns[0] 75 | case 2: 76 | if (ns[0] + ns[1]) == 0 { 77 | return nil, fmt.Errorf("invalid version %q", v) 78 | } 79 | ver.Major = ns[0] 80 | ver.Minor = ns[1] 81 | case 3: 82 | if (ns[0] + ns[1] + ns[2]) == 0 { 83 | return nil, fmt.Errorf("invalid version %q", v) 84 | } 85 | ver.Major = ns[0] 86 | ver.Minor = ns[1] 87 | ver.Patch = ns[2] 88 | default: 89 | return nil, fmt.Errorf("invalid version string %q", v) 90 | } 91 | 92 | return &ver, nil 93 | 94 | } 95 | 96 | // Versions is a sortable slice of *Version 97 | type Versions []*Version 98 | 99 | // Less returns true, if the version of index a is less than the version of index b 100 | func (v Versions) Less(a, b int) bool { 101 | return v[a].Less(*v[b]) 102 | } 103 | 104 | // Len returns the number of *Version inside the slice 105 | func (v Versions) Len() int { 106 | return len(v) 107 | } 108 | 109 | // Swap swaps the *Version of the index a with that of the index b 110 | func (v Versions) Swap(a, b int) { 111 | v[a], v[b] = v[b], v[a] 112 | } 113 | 114 | // Sort sorts the slice and returns it 115 | func (v Versions) Sort() Versions { 116 | sort.Sort(v) 117 | return v 118 | } 119 | 120 | // Last returns the last *Version of the slice. 121 | func (v Versions) Last() *Version { 122 | if len(v) == 0 { 123 | return nil 124 | } 125 | 126 | return v[len(v)-1] 127 | } 128 | 129 | // First returns the first *Version of the slice. 130 | func (v Versions) First() *Version { 131 | if len(v) == 0 { 132 | return nil 133 | } 134 | 135 | return v[0] 136 | } 137 | -------------------------------------------------------------------------------- /v2/drivers/midicat/midicat.go: -------------------------------------------------------------------------------- 1 | package midicat 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "gitlab.com/gomidi/midi/v2/drivers/internal/version" 8 | ) 9 | 10 | func read(rd io.Reader) (byte, error) { 11 | var b = make([]byte, 1) 12 | 13 | i, err := rd.Read(b) 14 | 15 | if err != nil { 16 | return 0, err 17 | } 18 | 19 | if i != 1 { 20 | return 0, err 21 | } 22 | 23 | return b[0], nil 24 | } 25 | 26 | func convert(b []byte) (out []byte, err error) { 27 | out = make([]byte, len(b)/2) 28 | 29 | _, err = fmt.Sscanf(string(b), "%X", &out) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return out, nil 35 | 36 | } 37 | 38 | func convertDelta(b []byte) (deltams int32, err error) { 39 | _, err = fmt.Sscanf(string(b), "%d", &deltams) 40 | if err != nil { 41 | return -1, err 42 | } 43 | 44 | return deltams, nil 45 | 46 | } 47 | 48 | const limit = byte('\n') 49 | 50 | func Read(rd io.Reader) (out []byte, deltams int32, err error) { 51 | var deltaRead bool 52 | var deltaBf []byte 53 | 54 | for { 55 | b, errRd := read(rd) 56 | if errRd != nil { 57 | return nil, -1, errRd 58 | } 59 | 60 | if b == ' ' { 61 | deltams, err = convertDelta(deltaBf) 62 | if err != nil { 63 | return 64 | } 65 | deltaRead = true 66 | continue 67 | } 68 | 69 | if b == limit { 70 | return out, deltams, err 71 | } 72 | 73 | if deltaRead { 74 | out = append(out, b) 75 | } else { 76 | deltaBf = append(deltaBf, b) 77 | } 78 | } 79 | } 80 | 81 | func ReadAndConvert(rd io.Reader) (out []byte, deltams int32, err error) { 82 | out, deltams, err = Read(rd) 83 | if err != nil { 84 | return 85 | } 86 | 87 | conv, err := convert(out) 88 | return conv, deltams, err 89 | } 90 | 91 | var Version = version.Version{Major: 0, Minor: 9, Patch: 5} 92 | -------------------------------------------------------------------------------- /v2/drivers/midicat/midicat_test.go: -------------------------------------------------------------------------------- 1 | package midicat 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestRead(t *testing.T) { 10 | var bf bytes.Buffer 11 | 12 | bf.WriteString("23 B0E4FF\n3455 B3EEF5\n") 13 | 14 | out1, delta1, err1 := ReadAndConvert(&bf) 15 | 16 | if err1 != nil { 17 | t.Errorf("error on first read: %s", err1.Error()) 18 | return 19 | } 20 | 21 | got1 := fmt.Sprintf("[%v] % X", delta1, out1) 22 | expected1 := "[23] B0 E4 FF" 23 | 24 | if got1 != expected1 { 25 | t.Errorf("read1: got %q, expected %q", got1, expected1) 26 | } 27 | 28 | out2, delta2, err2 := ReadAndConvert(&bf) 29 | 30 | if err2 != nil { 31 | t.Errorf("error on second read: %s", err2.Error()) 32 | return 33 | } 34 | 35 | got2 := fmt.Sprintf("[%v] % X", delta2, out2) 36 | expected2 := "[3455] B3 EE F5" 37 | 38 | if got2 != expected2 { 39 | t.Errorf("read2: got %q, expected %q", got2, expected2) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 gomidi - Go libraries for MIDI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/README.md: -------------------------------------------------------------------------------- 1 | # midicatdrv 2 | 3 | This driver is based on the slim midicat tool (see tools/midicat for more information). 4 | 5 | ## Installation 6 | 7 | This is driver uses the `midicat` binary. 8 | 9 | Download the binaries (for Linux and Windows) [here](https://gitlab.com/gomidi/tools/-/releases). 10 | 11 | Or install them via 12 | 13 | go install gitlab.com/gomidi/midi/tools/midicat@latest 14 | 15 | (When using windows, run the commands inside `cmd.exe`.) 16 | 17 | The `midicat` binary is based on the rtmidi project and connects MIDI ports to Stdin and Stdout. 18 | The idea is, to have just one binary that requires CGO (`midicat`) and for all the Go projects that need 19 | to connect to MIDI ports just pipe the MIDI data from and to this binary. 20 | 21 | This driver connects to the `midicat` binary via Stdin and Stdout while providing the same unified https://gitlab.com/gomidi/v2/drivers.Driver interface as `rtmididrv` and `portmididrv`. But projects importing this `midicatdrv` will not required CGO 22 | (like that would be the case otherwise). 23 | 24 | Download or compile the `midicat` binary and place it in your `PATH` before using this driver. 25 | **midicat version >= 0.6.8 is required**. 26 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Marc René Arns. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package midicatdrv provides a Driver to connect via the program midicat (from the tools). 7 | */ 8 | package midicatdrv 9 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/driver_test.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | 3 | package midicatdrv 4 | 5 | import ( 6 | "testing" 7 | 8 | "gitlab.com/gomidi/midi/v2" 9 | "gitlab.com/gomidi/midi/v2/drivers" 10 | "gitlab.com/gomidi/midi/v2/drivers/drivertest" 11 | ) 12 | 13 | func runTest(t *testing.T, fn func(*testing.T, drivers.In, drivers.Out)) func(*testing.T) { 14 | return func(*testing.T) { 15 | drv, err := New() 16 | if err != nil { 17 | t.Fatalf("ERROR: %s", err.Error()) 18 | } 19 | 20 | in, err := midi.FindInPort("Midi Through Port-0") 21 | if err != nil { 22 | //t.Skipf("could not find in port Midi Through Port-0") 23 | return 24 | } 25 | out, err := midi.FindOutPort("Midi Through Port-0") 26 | if err != nil { 27 | //t.Skipf("could not find out port Midi Through Port-0") 28 | return 29 | } 30 | 31 | fn(t, in, out) 32 | drv.Close() 33 | } 34 | } 35 | 36 | func TestSpec(t *testing.T) { 37 | drv, err := New() 38 | if err != nil { 39 | t.Fatalf("ERROR: %s", err.Error()) 40 | } 41 | 42 | drivertest.DriverInterfaceImplementationTest(t, drv) 43 | drivertest.AutoregisterTest(t, drv) 44 | 45 | tests := []struct { 46 | name string 47 | fn func(*testing.T, drivers.In, drivers.Out) 48 | }{ 49 | { 50 | "RunningStatus", 51 | drivertest.RunningStatusTest, 52 | }, 53 | { 54 | "FullStatus", 55 | drivertest.FullStatusTest, 56 | }, 57 | { 58 | "NoActiveSense", 59 | drivertest.NoActiveSenseTest, 60 | }, 61 | { 62 | "NoTimeCode", 63 | drivertest.NoTimeCodeTest, 64 | }, 65 | { 66 | "Sysex", 67 | drivertest.SysexTest, 68 | }, 69 | { 70 | "NoSysex", 71 | drivertest.NoSysexTest, 72 | }, 73 | } 74 | 75 | for _, test := range tests { 76 | f := runTest(t, test.fn) 77 | t.Run(test.name, f) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/exec_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !js 2 | // +build !windows,!js 3 | 4 | package midicatdrv 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | "syscall" 10 | ) 11 | 12 | func _execCommand(c string) *exec.Cmd { 13 | return exec.Command("sh", "-c", "exec "+c) 14 | } 15 | 16 | func midiCatOutCmd(index int) *exec.Cmd { 17 | cmd := exec.Command("midicat", "out", fmt.Sprintf("--index=%v", index)) 18 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0} 19 | return cmd 20 | } 21 | 22 | func midiCatVersionCmd() *exec.Cmd { 23 | return exec.Command("midicat", "version", "-s") 24 | } 25 | 26 | func midiCatInCmd(index int) *exec.Cmd { 27 | cmd := exec.Command("midicat", "in", fmt.Sprintf("--index=%v", index)) 28 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0} 29 | return cmd 30 | } 31 | 32 | func midiCatCmd(args string) *exec.Cmd { 33 | cmd := _execCommand("midicat " + args) 34 | // important! prevents that signals such as interrupt send to the main program gets passed 35 | // to midicat (which would not allow us to shutdown properly, e.g. stop hanging notes) 36 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0} 37 | return cmd 38 | } 39 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/exec_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package midicatdrv 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | "strings" 10 | "syscall" 11 | ) 12 | 13 | /* 14 | func execCommand(c string) *exec.Cmd { 15 | //return exec.Command("powershell.exe", "/Command", `$Process = [Diagnostics.Process]::Start("` + c + `") ; echo $Process.Id `) 16 | //return exec.Command("powershell.exe", "/Command", `$Process = [Diagnostics.Process]::Start("fluidsynth.exe", "-i -q -n $_file") ; echo $Process.Id `) 17 | fmt.Println(c) 18 | return exec.Command("lib.exe", "/C", c) 19 | } 20 | */ 21 | 22 | func midiCatOutCmd(index int) *exec.Cmd { 23 | cmd := exec.Command("midicat.exe", "out", fmt.Sprintf("--index=%v", index)) 24 | cmd.SysProcAttr = &syscall.SysProcAttr{ 25 | CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, 26 | // CREATE_NEW_CONSOLE 27 | } 28 | return cmd 29 | } 30 | 31 | func midiCatVersionCmd() *exec.Cmd { 32 | return exec.Command("midicat.exe", "version", "-s") 33 | } 34 | 35 | func midiCatInCmd(index int) *exec.Cmd { 36 | cmd := exec.Command("midicat.exe", "in", fmt.Sprintf("--index=%v", index)) 37 | cmd.SysProcAttr = &syscall.SysProcAttr{ 38 | CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, 39 | } 40 | return cmd 41 | } 42 | 43 | func midiCatCmd(args string) *exec.Cmd { 44 | //return execCommand("midicat.exe " + args) 45 | //fmt.Println("midicat.exe " + args) 46 | a := strings.Split(args, " ") 47 | cmd := exec.Command("midicat.exe", a...) 48 | cmd.SysProcAttr = &syscall.SysProcAttr{ 49 | CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, 50 | } 51 | return cmd 52 | } 53 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/helpers.go: -------------------------------------------------------------------------------- 1 | package midicatdrv 2 | 3 | import ( 4 | "gitlab.com/gomidi/midi/v2/drivers" 5 | ) 6 | 7 | type inPorts []drivers.In 8 | 9 | func (i inPorts) Len() int { 10 | return len(i) 11 | } 12 | 13 | func (i inPorts) Swap(a, b int) { 14 | i[a], i[b] = i[b], i[a] 15 | } 16 | 17 | func (i inPorts) Less(a, b int) bool { 18 | return i[a].Number() < i[b].Number() 19 | } 20 | 21 | type outPorts []drivers.Out 22 | 23 | func (i outPorts) Len() int { 24 | return len(i) 25 | } 26 | 27 | func (i outPorts) Swap(a, b int) { 28 | i[a], i[b] = i[b], i[a] 29 | } 30 | 31 | func (i outPorts) Less(a, b int) bool { 32 | return i[a].Number() < i[b].Number() 33 | } 34 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/in.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | 3 | package midicatdrv 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "runtime" 9 | "sync" 10 | 11 | "gitlab.com/gomidi/midi/v2" 12 | "gitlab.com/gomidi/midi/v2/drivers" 13 | lib "gitlab.com/gomidi/midi/v2/drivers/midicat" 14 | ) 15 | 16 | type in struct { 17 | number int 18 | sync.RWMutex 19 | driver *Driver 20 | name string 21 | shouldStopListening chan bool 22 | didStopListening chan bool 23 | shouldKill chan bool 24 | wasKilled chan bool 25 | hasProc bool 26 | listener func(data []byte, deltamillisecs int32) 27 | } 28 | 29 | func (o *in) fireCmd() error { 30 | o.Lock() 31 | if o.hasProc { 32 | o.Unlock() 33 | return fmt.Errorf("already running") 34 | } 35 | o.shouldStopListening = make(chan bool, 1) 36 | o.didStopListening = make(chan bool, 1) 37 | o.shouldKill = make(chan bool, 1) 38 | o.wasKilled = make(chan bool, 1) 39 | o.hasProc = true 40 | cmd := midiCatInCmd(o.number) 41 | rd, wr := io.Pipe() 42 | cmd.Stdout = wr 43 | err := cmd.Start() 44 | if err != nil { 45 | o.Lock() 46 | o.hasProc = false 47 | o.Unlock() 48 | return err 49 | } 50 | o.Unlock() 51 | go func() { 52 | for { 53 | data, abstime, err := lib.ReadAndConvert(rd) 54 | if err != nil { 55 | return 56 | } 57 | o.RLock() 58 | if !o.hasProc { 59 | o.RUnlock() 60 | return 61 | } 62 | 63 | if o.listener != nil { 64 | o.listener(data, abstime) 65 | } 66 | o.RUnlock() 67 | runtime.Gosched() 68 | } 69 | }() 70 | 71 | go func(shouldStopListening <-chan bool, didStopListening chan<- bool, shouldKill <-chan bool, wasKilled chan<- bool) { 72 | defer rd.Close() 73 | defer wr.Close() 74 | 75 | for { 76 | select { 77 | case <-shouldKill: 78 | if cmd.Process != nil { 79 | /* 80 | rd.Close() 81 | wr.Close() 82 | */ 83 | cmd.Process.Kill() 84 | } 85 | o.Lock() 86 | o.hasProc = false 87 | o.Unlock() 88 | wasKilled <- true 89 | return 90 | case <-shouldStopListening: 91 | o.Lock() 92 | o.listener = nil 93 | o.Unlock() 94 | didStopListening <- true 95 | default: 96 | runtime.Gosched() 97 | } 98 | } 99 | }(o.shouldStopListening, o.didStopListening, o.shouldKill, o.wasKilled) 100 | 101 | return nil 102 | } 103 | 104 | // IsOpen returns wether the MIDI in port is open 105 | func (o *in) IsOpen() (open bool) { 106 | o.RLock() 107 | open = o.hasProc 108 | o.RUnlock() 109 | return 110 | } 111 | 112 | // String returns the name of the MIDI in port. 113 | func (i *in) String() string { 114 | return i.name 115 | } 116 | 117 | // Underlying returns the underlying driver. Here returns nil. 118 | func (i *in) Underlying() interface{} { 119 | return nil 120 | } 121 | 122 | // Number returns the number of the MIDI in port. 123 | // Note that with rtmidi, out and in ports are counted separately. 124 | // That means there might exists out ports and an in ports that share the same number. 125 | func (i *in) Number() int { 126 | return i.number 127 | } 128 | 129 | // Close closes the MIDI in port, after it has stopped listening. 130 | func (i *in) Close() (err error) { 131 | if !i.IsOpen() { 132 | return nil 133 | } 134 | 135 | //i.shouldStopReading 136 | go func() { 137 | i.shouldStopListening <- true 138 | }() 139 | <-i.didStopListening 140 | 141 | i.shouldKill <- true 142 | <-i.wasKilled 143 | return 144 | } 145 | 146 | // Open opens the MIDI in port 147 | func (i *in) Open() (err error) { 148 | if i.IsOpen() { 149 | return nil 150 | } 151 | 152 | err = i.fireCmd() 153 | if err != nil { 154 | i.Close() 155 | return fmt.Errorf("can't open MIDI in port %v (%s): %v", i.number, i, err) 156 | } 157 | 158 | i.driver.Lock() 159 | i.driver.opened = append(i.driver.opened, i) 160 | i.driver.Unlock() 161 | 162 | return nil 163 | } 164 | 165 | func newIn(driver *Driver, number int, name string) drivers.In { 166 | return &in{driver: driver, number: number, name: name} 167 | } 168 | 169 | func (i *in) Listen(onMsg func(msg []byte, absmilliseconds int32), conf drivers.ListenConfig) (stopFn func(), err error) { 170 | stopFn = func() { 171 | if !i.IsOpen() { 172 | return 173 | } 174 | i.shouldStopListening <- true 175 | <-i.didStopListening 176 | } 177 | 178 | if !i.IsOpen() { 179 | return nil, drivers.ErrPortClosed 180 | } 181 | 182 | if onMsg == nil { 183 | return nil, fmt.Errorf("onMsg callback must not be nil") 184 | } 185 | 186 | i.RLock() 187 | if i.listener != nil { 188 | i.RUnlock() 189 | return nil, fmt.Errorf("listener already set") 190 | } 191 | i.RUnlock() 192 | 193 | //var rd = drivers.NewReader(config, onMsg) 194 | i.Lock() 195 | i.listener = func(data []byte, absmilliseconds int32) { 196 | //rd.EachMessage(data, deltamillisecs) 197 | //rd.EachMessage(data, -1) 198 | msg := midi.Message(data) 199 | 200 | if msg.Is(midi.ActiveSenseMsg) && !conf.ActiveSense { 201 | return 202 | } 203 | 204 | if msg.Is(midi.TimingClockMsg) && !conf.TimeCode { 205 | return 206 | } 207 | 208 | if msg.Is(midi.SysExMsg) && !conf.SysEx { 209 | return 210 | } 211 | onMsg(data, absmilliseconds) 212 | } 213 | i.Unlock() 214 | 215 | return stopFn, nil 216 | } 217 | 218 | /* 219 | // SendTo makes the listener listen to the in port 220 | func (i *in) StartListening(cb func([]byte, int32)) (err error) { 221 | if !i.IsOpen() { 222 | return drivers.ErrPortClosed 223 | } 224 | 225 | i.RLock() 226 | if i.listener != nil { 227 | i.RUnlock() 228 | return fmt.Errorf("listener already set") 229 | } 230 | i.RUnlock() 231 | i.Lock() 232 | i.listener = cb 233 | i.Unlock() 234 | 235 | return nil 236 | } 237 | 238 | // StopListening cancels the listening 239 | func (i *in) StopListening() (err error) { 240 | if !i.IsOpen() { 241 | return drivers.ErrPortClosed 242 | } 243 | 244 | i.shouldStopListening <- true 245 | <-i.didStopListening 246 | return 247 | } 248 | */ 249 | -------------------------------------------------------------------------------- /v2/drivers/midicatdrv/out.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | 3 | package midicatdrv 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "os/exec" 9 | "sync" 10 | 11 | "gitlab.com/gomidi/midi/v2/drivers" 12 | ) 13 | 14 | func newOut(driver *Driver, number int, name string) drivers.Out { 15 | o := &out{driver: driver, number: number, name: name} 16 | return o 17 | } 18 | 19 | type out struct { 20 | number int 21 | sync.RWMutex 22 | driver *Driver 23 | name string 24 | wr *io.PipeWriter 25 | rd *io.PipeReader 26 | cmd *exec.Cmd 27 | } 28 | 29 | func (o *out) fireCmd() error { 30 | o.Lock() 31 | defer o.Unlock() 32 | if o.cmd != nil { 33 | return fmt.Errorf("already running") 34 | } 35 | 36 | o.cmd = midiCatOutCmd(o.number) 37 | o.rd, o.wr = io.Pipe() 38 | o.cmd.Stdin = o.rd 39 | 40 | err := o.cmd.Start() 41 | if err != nil { 42 | o.rd = nil 43 | o.wr = nil 44 | o.cmd = nil 45 | return err 46 | } 47 | 48 | return err 49 | } 50 | 51 | // IsOpen returns wether the port is open 52 | func (o *out) IsOpen() (open bool) { 53 | o.RLock() 54 | open = o.cmd != nil 55 | o.RUnlock() 56 | return 57 | } 58 | 59 | // Send sends a MIDI message to the MIDI output port 60 | // If the output port is closed, it returns midi.ErrClosed 61 | func (o *out) Send(b []byte) error { 62 | o.Lock() 63 | defer o.Unlock() 64 | if o.cmd == nil { 65 | fmt.Println("port closed") 66 | return drivers.ErrPortClosed 67 | } 68 | //fmt.Printf("% X\n", b) 69 | _, err := fmt.Fprintf(o.wr, "%d %X\n", 0, b) 70 | //_, err := fmt.Fprintf(o.wr, "%X\n", b) 71 | if err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | 77 | // Underlying returns the underlying driver. Here it returns nil 78 | func (o *out) Underlying() interface{} { 79 | return nil 80 | } 81 | 82 | // Number returns the number of the MIDI out port. 83 | // Note that with rtmidi, out and in ports are counted separately. 84 | // That means there might exists out ports and an in ports that share the same number 85 | func (o *out) Number() int { 86 | return o.number 87 | } 88 | 89 | // String returns the name of the MIDI out port. 90 | func (o *out) String() string { 91 | return o.name 92 | } 93 | 94 | // Close closes the MIDI out port 95 | func (o *out) Close() (err error) { 96 | if !o.IsOpen() { 97 | return nil 98 | } 99 | 100 | o.Lock() 101 | defer o.Unlock() 102 | o.wr.Close() 103 | err = o.cmd.Process.Kill() 104 | o.cmd = nil 105 | o.rd.Close() 106 | o.wr = nil 107 | o.rd = nil 108 | return err 109 | } 110 | 111 | // Open opens the MIDI out port 112 | func (o *out) Open() (err error) { 113 | if o.IsOpen() { 114 | return nil 115 | } 116 | 117 | err = o.fireCmd() 118 | 119 | if err != nil { 120 | return fmt.Errorf("can't open MIDI out port %v (%s): %v", o.number, o, err) 121 | } 122 | 123 | o.driver.Lock() 124 | o.driver.opened = append(o.driver.opened, o) 125 | o.driver.Unlock() 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | example/example 15 | example/virtual/virtual 16 | example/check/check -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.11.x" 4 | - tip 5 | 6 | sudo: false 7 | #before_install: 8 | # - go get github.com/mattn/goveralls 9 | #script: 10 | # - $HOME/gopath/bin/goveralls -service=travis-ci 11 | 12 | before_install: 13 | - sudo apt-get install libasound2-dev 14 | env: 15 | - GO111MODULE=on -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/CONTRIBUTING: -------------------------------------------------------------------------------- 1 | All development activity takes place on Gitlab. 2 | 3 | If you think you've discovered a bug, or you would like 4 | to make a feature request please do this through Gitlab. 5 | 6 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 gomidi - Go libraries for MIDI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/README.md: -------------------------------------------------------------------------------- 1 | # rtmididrv 2 | 3 | This driver is based on the rtmidi project (see https://github.com/thestk/rtmidi for more information). 4 | 5 | 6 | ## Linux / Debian 7 | 8 | install the headers of alsa somehow before using this driver, e.g. sudo apt-get install libasound2-dev 9 | 10 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Marc René Arns. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !js 6 | 7 | /* 8 | Package rtmididrv provides a Driver to connect via rtmidi. 9 | */ 10 | package rtmididrv 11 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/driver.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package rtmididrv 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "gitlab.com/gomidi/midi/v2/drivers" 11 | "gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi" 12 | ) 13 | 14 | func init() { 15 | drv, err := New() 16 | if err != nil { 17 | panic(fmt.Sprintf("could not register rtmididrv: %s", err.Error())) 18 | } 19 | drivers.Register(drv) 20 | } 21 | 22 | type Driver struct { 23 | opened []drivers.Port 24 | //ignoreSysex bool 25 | //ignoreTimeCode bool 26 | //ignoreActiveSense bool 27 | //sync.RWMutex 28 | } 29 | 30 | func (d *Driver) String() string { 31 | return "rtmididrv" 32 | } 33 | 34 | // Close closes all open ports. It must be called at the end of a session. 35 | func (d *Driver) Close() (err error) { 36 | // d.Lock() 37 | var e CloseErrors 38 | 39 | for _, p := range d.opened { 40 | err = p.Close() 41 | if err != nil { 42 | e = append(e, err) 43 | } 44 | } 45 | 46 | // d.Unlock() 47 | 48 | if len(e) == 0 { 49 | return nil 50 | } 51 | 52 | return e 53 | } 54 | 55 | /* 56 | type Option func(*Driver) 57 | 58 | func IgnoreSysex() Option { 59 | return func(d *Driver) { 60 | d.ignoreSysex = true 61 | } 62 | } 63 | 64 | func IgnoreTimeCode() Option { 65 | return func(d *Driver) { 66 | d.ignoreTimeCode = true 67 | } 68 | } 69 | 70 | func IgnoreActiveSense() Option { 71 | return func(d *Driver) { 72 | d.ignoreActiveSense = true 73 | } 74 | } 75 | */ 76 | 77 | // New returns a driver based on the default rtmidi in and out 78 | func New() (*Driver, error) { 79 | d := &Driver{} 80 | return d, nil 81 | } 82 | 83 | // OpenVirtualIn opens and returns a virtual MIDI in. We can't get the port number, so set it to -1. 84 | func (d *Driver) OpenVirtualIn(name string) (drivers.In, error) { 85 | _in, err := rtmidi.NewMIDIInDefault() 86 | if err != nil { 87 | return nil, fmt.Errorf("can't open default MIDI in: %v", err) 88 | } 89 | 90 | err = _in.OpenVirtualPort(name) 91 | 92 | if err != nil { 93 | return nil, fmt.Errorf("can't open virtual in port: %s", err.Error()) 94 | } 95 | 96 | // d.Lock() 97 | //defer d.Unlock() 98 | //_in.IgnoreTypes(d.ignoreSysex, d.ignoreTimeCode, d.ignoreActiveSense) 99 | inPort := &in{driver: d, number: -1, name: name, midiIn: _in} 100 | d.opened = append(d.opened, inPort) 101 | return inPort, nil 102 | } 103 | 104 | // OpenVirtualOut opens and returns a virtual MIDI out. We can't get the port number, so set it to -1. 105 | func (d *Driver) OpenVirtualOut(name string) (drivers.Out, error) { 106 | _out, err := rtmidi.NewMIDIOutDefault() 107 | if err != nil { 108 | return nil, fmt.Errorf("can't open default MIDI out: %v", err) 109 | } 110 | 111 | err = _out.OpenVirtualPort(name) 112 | 113 | if err != nil { 114 | return nil, fmt.Errorf("can't open virtual out port: %s", err.Error()) 115 | } 116 | 117 | //d.Lock() 118 | //defer d.Unlock() 119 | outPort := &out{driver: d, number: -1, name: name, midiOut: _out} 120 | d.opened = append(d.opened, outPort) 121 | return outPort, nil 122 | } 123 | 124 | // Ins returns the available MIDI input ports 125 | func (d *Driver) Ins() (ins []drivers.In, err error) { 126 | var in rtmidi.MIDIIn 127 | in, err = rtmidi.NewMIDIInDefault() 128 | if err != nil { 129 | return nil, fmt.Errorf("can't open default MIDI in: %v", err) 130 | } 131 | 132 | ports, err := in.PortCount() 133 | if err != nil { 134 | return nil, fmt.Errorf("can't get number of in ports: %s", err.Error()) 135 | } 136 | 137 | for i := 0; i < ports; i++ { 138 | name, err := in.PortName(i) 139 | if err != nil { 140 | name = "" 141 | } 142 | ins = append(ins, newIn(d, i, name)) 143 | } 144 | 145 | // don't destroy, destroy just panics 146 | // in.Destroy() 147 | err = in.Close() 148 | return 149 | } 150 | 151 | // Outs returns the available MIDI output ports 152 | func (d *Driver) Outs() (outs []drivers.Out, err error) { 153 | var out rtmidi.MIDIOut 154 | out, err = rtmidi.NewMIDIOutDefault() 155 | if err != nil { 156 | return nil, fmt.Errorf("can't open default MIDI out: %v", err) 157 | } 158 | 159 | ports, err := out.PortCount() 160 | if err != nil { 161 | return nil, fmt.Errorf("can't get number of out ports: %s", err.Error()) 162 | } 163 | 164 | for i := 0; i < ports; i++ { 165 | name, err := out.PortName(i) 166 | if err != nil { 167 | name = "" 168 | } 169 | outs = append(outs, newOut(d, i, name)) 170 | } 171 | 172 | err = out.Close() 173 | return 174 | } 175 | 176 | // CloseErrors collects error from closing multiple MIDI ports 177 | type CloseErrors []error 178 | 179 | func (c CloseErrors) Error() string { 180 | if len(c) == 0 { 181 | return "no errors" 182 | } 183 | 184 | var bd strings.Builder 185 | 186 | bd.WriteString("the following closing errors occured:\n") 187 | 188 | for _, e := range c { 189 | bd.WriteString(e.Error() + "\n") 190 | } 191 | 192 | return bd.String() 193 | } 194 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/driver_test.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package rtmididrv 5 | 6 | import ( 7 | "testing" 8 | 9 | "gitlab.com/gomidi/midi/v2" 10 | "gitlab.com/gomidi/midi/v2/drivers" 11 | "gitlab.com/gomidi/midi/v2/drivers/drivertest" 12 | ) 13 | 14 | func runTest(t *testing.T, fn func(*testing.T, drivers.In, drivers.Out)) func(*testing.T) { 15 | return func(*testing.T) { 16 | drv, err := New() 17 | if err != nil { 18 | t.Fatalf("ERROR: %s", err.Error()) 19 | } 20 | 21 | in, err := midi.FindInPort("Midi Through Port-0") 22 | if err != nil { 23 | //t.Skipf("could not find in port Midi Through Port-0") 24 | return 25 | } 26 | out, err := midi.FindOutPort("Midi Through Port-0") 27 | if err != nil { 28 | //t.Skipf("could not find out port Midi Through Port-0") 29 | return 30 | } 31 | 32 | fn(t, in, out) 33 | drv.Close() 34 | } 35 | } 36 | 37 | func TestSpec(t *testing.T) { 38 | drv, err := New() 39 | if err != nil { 40 | t.Fatalf("ERROR: %s", err.Error()) 41 | } 42 | 43 | drivertest.DriverInterfaceImplementationTest(t, drv) 44 | drivertest.AutoregisterTest(t, drv) 45 | 46 | tests := []struct { 47 | name string 48 | fn func(*testing.T, drivers.In, drivers.Out) 49 | }{ 50 | { 51 | "RunningStatus", 52 | drivertest.RunningStatusTest, 53 | }, 54 | { 55 | "FullStatus", 56 | drivertest.FullStatusTest, 57 | }, 58 | { 59 | "NoActiveSense", 60 | drivertest.NoActiveSenseTest, 61 | }, 62 | { 63 | "NoTimeCode", 64 | drivertest.NoTimeCodeTest, 65 | }, 66 | { 67 | "Sysex", 68 | drivertest.SysexTest, 69 | }, 70 | { 71 | "NoSysex", 72 | drivertest.NoSysexTest, 73 | }, 74 | } 75 | 76 | for _, test := range tests { 77 | f := runTest(t, test.fn) 78 | t.Run(test.name, f) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/imported/README.md: -------------------------------------------------------------------------------- 1 | 2 | Please note that the files inside rtmidi are copies from the rtmidi repository at https://github.com/thestk/rtmidi 3 | and https://github.com/thestk/rtmidi/contrib/go/rtmidi. 4 | 5 | The LICENSE file is a copy of https://github.com/thestk/rtmidi/LICENSE -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/imported/rtmidi/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | RtMidi: realtime MIDI i/o C++ classes 3 | Copyright (c) 2003-2023 Gary P. Scavone 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation files 7 | (the "Software"), to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, 10 | and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | Any person wishing to distribute modifications to the Software is 17 | asked to send the modifications to the original developer so that 18 | they can be incorporated into the canonical version. This is, 19 | however, not a binding provision of this license. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 25 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 26 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/imported/rtmidi/rtmidi_test.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package rtmidi 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "testing" 10 | ) 11 | 12 | func TestMidiIn(t *testing.T) { 13 | _, err := NewMIDIInDefault() 14 | if err != nil { 15 | //return nil, fmt.Errorf("can't open default MIDI in: %v", err) 16 | //t.Errorf("can't open default MIDI in: %v", err) 17 | fmt.Printf("can't open default MIDI in: %v", err) 18 | } 19 | } 20 | 21 | func ExampleCompiledAPI() { 22 | for _, api := range CompiledAPI() { 23 | log.Println("Compiled API: ", api) 24 | } 25 | } 26 | 27 | func ExampleMIDIIn_Message() { 28 | in, err := NewMIDIInDefault() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer in.Destroy() 33 | if err := in.OpenPort(0, "RtMidi"); err != nil { 34 | log.Fatal(err) 35 | } 36 | defer in.Close() 37 | 38 | for { 39 | m, t, err := in.Message() 40 | if len(m) > 0 { 41 | log.Println(m, t, err) 42 | } 43 | } 44 | } 45 | 46 | func ExampleMIDIIn_SetCallback() { 47 | in, err := NewMIDIInDefault() 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | defer in.Destroy() 52 | if err := in.OpenPort(0, "RtMidi"); err != nil { 53 | log.Fatal(err) 54 | } 55 | defer in.Close() 56 | in.SetCallback(func(m MIDIIn, msg []byte, t float64) { 57 | log.Println(msg, t) 58 | }) 59 | <-make(chan struct{}) 60 | } 61 | -------------------------------------------------------------------------------- /v2/drivers/rtmididrv/out.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package rtmididrv 5 | 6 | import ( 7 | "fmt" 8 | 9 | "gitlab.com/gomidi/midi/v2/drivers" 10 | "gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi" 11 | ) 12 | 13 | func newOut(driver *Driver, number int, name string) drivers.Out { 14 | o := &out{driver: driver, number: number, name: name} 15 | return o 16 | } 17 | 18 | type out struct { 19 | number int 20 | //sync.RWMutex 21 | driver *Driver 22 | name string 23 | midiOut rtmidi.MIDIOut 24 | } 25 | 26 | // IsOpen returns wether the port is open 27 | func (o *out) IsOpen() (open bool) { 28 | // o.RLock() 29 | open = o.midiOut != nil 30 | // o.RUnlock() 31 | return 32 | } 33 | 34 | // Send writes a MIDI sysex message to the outut port 35 | func (o *out) SendSysEx(data []byte) error { 36 | //fmt.Printf("try to send sysex\n") 37 | 38 | if o.midiOut == nil { 39 | //o.RUnlock() 40 | return drivers.ErrPortClosed 41 | } 42 | //o.mx.RUnlock() 43 | 44 | // since we always open the outputstream with a latency of 0 45 | // the timestamp is ignored 46 | //var ts portmidi.Timestamp // or portmidi.Time() 47 | 48 | //o.mx.Lock() 49 | // defer o.mx.Unlock() 50 | //fmt.Printf("sending sysex % X\n", data) 51 | //err := o.stream.WriteSysExBytes(ts, data) 52 | err := o.midiOut.SendMessage(data) 53 | if err != nil { 54 | return fmt.Errorf("could not send sysex message to MIDI out %v (%s): %v", o.Number(), o, err) 55 | } 56 | return nil 57 | } 58 | 59 | func (o *out) Send(b []byte) error { 60 | if o.midiOut == nil { 61 | //o.RUnlock() 62 | return drivers.ErrPortClosed 63 | } 64 | // o.RUnlock() 65 | 66 | //fmt.Printf("send % X\n", m.Data) 67 | /* 68 | var bt []byte 69 | 70 | switch { 71 | case b[2] == 0 && b[1] == 0: 72 | bt = []byte{b[0]} 73 | // case b[2] == 0: 74 | // bt = []byte{b[0], b[1]} 75 | default: 76 | bt = []byte{b[0], b[1], b[2]} 77 | } 78 | 79 | //bt := []byte{b[0], b[1], b[2]} 80 | err := o.midiOut.SendMessage(bt) 81 | */ 82 | err := o.midiOut.SendMessage(b) 83 | if err != nil { 84 | return fmt.Errorf("could not send message to MIDI out %v (%s): %v", o.number, o, err) 85 | } 86 | return nil 87 | } 88 | 89 | /* 90 | // Send writes a MIDI message to the MIDI output port 91 | // If the output port is closed, it returns midi.ErrClosed 92 | func (o *out) send(bt []byte) error { 93 | //o.RLock() 94 | o.Lock() 95 | defer o.Unlock() 96 | if o.midiOut == nil { 97 | //o.RUnlock() 98 | return drivers.ErrPortClosed 99 | } 100 | // o.RUnlock() 101 | 102 | //fmt.Printf("send % X\n", m.Data) 103 | err := o.midiOut.SendMessage(bt) 104 | if err != nil { 105 | return fmt.Errorf("could not send message to MIDI out %v (%s): %v", o.number, o, err) 106 | } 107 | return nil 108 | } 109 | */ 110 | 111 | // Underlying returns the underlying rtmidi.MIDIOut. Use it with type casting: 112 | // 113 | // rtOut := o.Underlying().(rtmidi.MIDIOut) 114 | func (o *out) Underlying() interface{} { 115 | return o.midiOut 116 | } 117 | 118 | // Number returns the number of the MIDI out port. 119 | // Note that with rtmidi, out and in ports are counted separately. 120 | // That means there might exists out ports and an in ports that share the same number 121 | func (o *out) Number() int { 122 | return o.number 123 | } 124 | 125 | // String returns the name of the MIDI out port. 126 | func (o *out) String() string { 127 | return o.name 128 | } 129 | 130 | // Close closes the MIDI out port 131 | func (o *out) Close() (err error) { 132 | if !o.IsOpen() { 133 | return nil 134 | } 135 | //o.Lock() 136 | //defer o.Unlock() 137 | 138 | err = o.midiOut.Close() 139 | o.midiOut = nil 140 | 141 | if err != nil { 142 | err = fmt.Errorf("can't close MIDI out %v (%s): %v", o.number, o, err) 143 | } 144 | 145 | return 146 | } 147 | 148 | // Open opens the MIDI out port 149 | func (o *out) Open() (err error) { 150 | if o.IsOpen() { 151 | return nil 152 | } 153 | // o.Lock() 154 | //defer o.Unlock() 155 | o.midiOut, err = rtmidi.NewMIDIOutDefault() 156 | if err != nil { 157 | o.midiOut = nil 158 | return fmt.Errorf("can't open default MIDI out: %v", err) 159 | } 160 | 161 | err = o.midiOut.OpenPort(o.number, "") 162 | if err != nil { 163 | o.midiOut = nil 164 | return fmt.Errorf("can't open MIDI out port %v (%s): %v", o.number, o, err) 165 | } 166 | 167 | // o.driver.Lock() 168 | o.driver.opened = append(o.driver.opened, o) 169 | // o.driver.Unlock() 170 | 171 | return nil 172 | } 173 | -------------------------------------------------------------------------------- /v2/drivers/testdrv/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Marc René Arns. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package testdrv provides a Driver for testing. 7 | */ 8 | package testdrv 9 | 10 | import ( 11 | //"sync" 12 | 13 | "time" 14 | 15 | "gitlab.com/gomidi/midi/v2" 16 | "gitlab.com/gomidi/midi/v2/drivers" 17 | ) 18 | 19 | func init() { 20 | drv := New("testdrv") 21 | drivers.Register(drv) 22 | } 23 | 24 | type Driver struct { 25 | in *in 26 | out *out 27 | name string 28 | last time.Time 29 | now time.Time 30 | stopListening bool 31 | rd *drivers.Reader 32 | //wg sync.WaitGroup 33 | } 34 | 35 | func New(name string) *Driver { 36 | d := &Driver{name: name} 37 | d.in = &in{name: name + "-in", Driver: d, number: 0} 38 | d.out = &out{name: name + "-out", Driver: d, number: 0} 39 | d.last = time.Now() 40 | d.now = d.last 41 | return d 42 | } 43 | 44 | func (f *Driver) Sleep(d time.Duration) { 45 | f.now = f.now.Add(d) 46 | } 47 | 48 | // wait until all messages are handled 49 | /* 50 | func (f *Driver) Wait() { 51 | f.wg.Wait() 52 | } 53 | */ 54 | 55 | func (f *Driver) String() string { return f.name } 56 | func (f *Driver) Close() error { return nil } 57 | func (f *Driver) Ins() ([]drivers.In, error) { return []drivers.In{f.in}, nil } 58 | func (f *Driver) Outs() ([]drivers.Out, error) { return []drivers.Out{f.out}, nil } 59 | 60 | type in struct { 61 | number int 62 | name string 63 | isOpen bool 64 | *Driver 65 | } 66 | 67 | func (f *in) String() string { return f.name } 68 | func (f *in) Number() int { return f.number } 69 | func (f *in) IsOpen() bool { return f.isOpen } 70 | func (f *in) Underlying() interface{} { return nil } 71 | 72 | func (f *in) Listen(onMsg func(msg []byte, milliseconds int32), conf drivers.ListenConfig) (stopFn func(), err error) { 73 | //fmt.Printf("listeining from in port of %s\n", f.Driver.name) 74 | 75 | f.last = time.Now() 76 | 77 | stopFn = func() { 78 | f.stopListening = true 79 | } 80 | 81 | f.rd = drivers.NewReader(conf, func(m []byte, ms int32) { 82 | msg := midi.Message(m) 83 | 84 | if msg.Is(midi.ActiveSenseMsg) && !conf.ActiveSense { 85 | return 86 | } 87 | 88 | if msg.Is(midi.TimingClockMsg) && !conf.TimeCode { 89 | return 90 | } 91 | 92 | if msg.Is(midi.SysExMsg) && !conf.SysEx { 93 | return 94 | } 95 | 96 | //fmt.Printf("handle message % X at [%v] in driver %q\n", m, ms, f.Driver.name) 97 | onMsg(m, ms) 98 | // f.wg.Done() 99 | //fmt.Println("msg handled") 100 | }) 101 | f.rd.Reset() 102 | return stopFn, nil 103 | } 104 | 105 | func (f *in) Close() error { 106 | if !f.isOpen { 107 | return nil 108 | } 109 | f.isOpen = false 110 | return nil 111 | } 112 | 113 | func (f *in) Open() error { 114 | if f.isOpen { 115 | return nil 116 | } 117 | f.isOpen = true 118 | return nil 119 | } 120 | 121 | type out struct { 122 | number int 123 | name string 124 | isOpen bool 125 | *Driver 126 | } 127 | 128 | func (f *out) Number() int { return f.number } 129 | func (f *out) IsOpen() bool { return f.isOpen } 130 | func (f *out) String() string { return f.name } 131 | func (f *out) Underlying() interface{} { return nil } 132 | 133 | func (f *out) Close() error { 134 | if !f.isOpen { 135 | return nil 136 | } 137 | f.isOpen = false 138 | return nil 139 | } 140 | 141 | func (f *out) Send(bt []byte) error { 142 | if !f.isOpen { 143 | return drivers.ErrPortClosed 144 | } 145 | 146 | if f.stopListening { 147 | return nil 148 | } 149 | 150 | dur := f.now.Sub(f.last) 151 | ts_ms := int32(dur.Milliseconds()) 152 | f.last = f.now 153 | //f.wg.Add(1) 154 | //fmt.Printf("message added % X (len %v) at [%v] in driver %q\n", bt, len(bt), ts_ms, f.Driver.name) 155 | f.rd.EachMessage(bt, ts_ms) 156 | /* 157 | f.rd.SetDelta(ts_ms) 158 | for _, b := range bt { 159 | f.rd.EachByte(b) 160 | } 161 | */ 162 | return nil 163 | } 164 | 165 | func (f *out) Open() error { 166 | if f.isOpen { 167 | return nil 168 | } 169 | f.isOpen = true 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /v2/drivers/testdrv/driver_test.go: -------------------------------------------------------------------------------- 1 | package testdrv 2 | 3 | import ( 4 | "testing" 5 | 6 | "gitlab.com/gomidi/midi/v2/drivers" 7 | "gitlab.com/gomidi/midi/v2/drivers/drivertest" 8 | ) 9 | 10 | func runTest(t *testing.T, fn func(*testing.T, drivers.In, drivers.Out)) func(*testing.T) { 11 | return func(*testing.T) { 12 | drv := New("testdrv") 13 | ins, _ := drv.Ins() 14 | outs, _ := drv.Outs() 15 | fn(t, ins[0], outs[0]) 16 | drv.Close() 17 | } 18 | } 19 | 20 | func TestSpec(t *testing.T) { 21 | drv := New("testdrv") 22 | 23 | drivertest.DriverInterfaceImplementationTest(t, drv) 24 | drivertest.AutoregisterTest(t, drv) 25 | 26 | tests := []struct { 27 | name string 28 | fn func(*testing.T, drivers.In, drivers.Out) 29 | }{ 30 | { 31 | "RunningStatus", 32 | drivertest.RunningStatusTest, 33 | }, 34 | { 35 | "FullStatus", 36 | drivertest.FullStatusTest, 37 | }, 38 | { 39 | "NoActiveSense", 40 | drivertest.NoActiveSenseTest, 41 | }, 42 | { 43 | "NoTimeCode", 44 | drivertest.NoTimeCodeTest, 45 | }, 46 | { 47 | "Sysex", 48 | drivertest.SysexTest, 49 | }, 50 | { 51 | "NoSysex", 52 | drivertest.NoSysexTest, 53 | }, 54 | } 55 | 56 | for _, test := range tests { 57 | f := runTest(t, test.fn) 58 | t.Run(test.name, f) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/.gitignore: -------------------------------------------------------------------------------- 1 | example/example 2 | main.wasm 3 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 gomidi - Go libraries for MIDI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: 3 | GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o main.wasm main_js.go 4 | 5 | test: 6 | GOOS=js GOARCH=wasm WASM_HEADLESS=off go test -exec="/home/benny/go/bin/wasmbrowsertest" 7 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/README.md: -------------------------------------------------------------------------------- 1 | # webmididrv 2 | 3 | This is driver uses the js/wasm compilation target and runs in the browsers that support the webmidi standard 4 | (all browsers based on Chrome). 5 | 6 | ## Example 7 | 8 | see the example directory 9 | 10 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/TODO.md: -------------------------------------------------------------------------------- 1 | 2 | navigator.requestMIDIAccess() 3 | .then(onMIDISuccess, onMIDIFailure); 4 | 5 | function onMIDISuccess(midiAccess) { 6 | console.log(midiAccess); 7 | 8 | var inputs = midiAccess.inputs; 9 | var outputs = midiAccess.outputs; 10 | 11 | for (var input of midiAccess.inputs.values()) 12 | input.onmidimessage = getMIDIMessage; 13 | } 14 | 15 | inputs.forEach((midiInput) => { 16 | // Do something with the MIDI input device 17 | }); 18 | 19 | // Iterate through each connected MIDI output device 20 | outputs.forEach((midioutput) => { 21 | // Do something with the MIDI output device 22 | }); 23 | 24 | midiInput.addEventListener('midimessage', (event) => { 25 | // the `event` object will have a `data` property 26 | // that contains an array of 3 numbers. For examples: 27 | // [144, 63, 127] 28 | }) 29 | 30 | outputsend([144, 63, 127]); 31 | } 32 | 33 | function onMIDIFailure() { 34 | console.log('Could not access your MIDI devices.'); 35 | } 36 | 37 | function getMIDIMessage(message) { 38 | var command = message.data[0]; 39 | var note = message.data[1]; 40 | var velocity = (message.data.length > 2) ? message.data[2] : 0; // a velocity value might not be included with a noteOff command 41 | 42 | switch (command) { 43 | case 144: // noteOn 44 | if (velocity > 0) { 45 | noteOn(note, velocity); 46 | } else { 47 | noteOff(note); 48 | } 49 | break; 50 | case 128: // noteOff 51 | noteOff(note); 52 | break; 53 | // we could easily expand this switch statement to cover other types of commands such as controllers or sysex 54 | } 55 | } 56 | 57 | func jsonWrapper() js.Func { 58 | jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 59 | if len(args) != 1 { 60 | return "Invalid no of arguments passed" 61 | } 62 | inputJSON := args[0].String() 63 | fmt.Printf("input %s\n", inputJSON) 64 | pretty, err := prettyJson(inputJSON) 65 | if err != nil { 66 | fmt.Printf("unable to convert to json %s\n", err) 67 | return err.Error() 68 | } 69 | return pretty 70 | }) 71 | return jsonFunc 72 | } 73 | 74 | func main() { 75 | fmt.Println("Go Web Assembly") 76 | js.Global().Set("formatJSON", jsonWrapper()) 77 | <-make(chan bool) 78 | } 79 | 80 | jsDoc := js.Global().Get("document") 81 | if !jsDoc.Truthy() { 82 | return "Unable to get document object" 83 | } 84 | jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput") 85 | if !jsonOuputTextArea.Truthy() { 86 | return "Unable to get output text area" 87 | } 88 | inputJSON := args[0].String() 89 | 90 | 91 | 92 | 93 | /* 94 | For all other browsers that don’t support it natively, Chris Wilson’s WebMIDIAPIShim library (https://github.com/cwilso/WebMIDIAPIShim) is a polyfill for the Web MIDI API, of which Chris is a co-author. Simply including the shim script on your page will enable everything we’ve covered so far. 95 | 96 | 97 | 100 | 101 | This shim also requires Jazz-Soft.net’s Jazz-Plugin (https://jazz-soft.net/) to work, unfortunately, which means it’s an OK option for developers who want the flexibility to work in multiple browsers, but an extra barrier to mainstream adoption. Hopefully, within time, other browsers will adopt the Web MIDI API natively. 102 | */ 103 | 104 | /* 105 | The MIDIMessageEvent object we get back contains a lot of information, but what we’re most interested in is the data array. This array typically contains three values (e.g. [144, 72, 64]). The first value tells us what type of command was sent, the second is the note value, and the third is velocity. The command type could be either “note on,” “note off,” controller (such as pitch bend or piano pedal), or some other kind of system exclusive (“sysex”) event unique to that device/manufacturer. 106 | 107 | A command value of 144 signifies a “note on” event, and 128 typically signifies a “note off” event. 108 | Note values are on a range from 0–127, lowest to highest. For example, the lowest note on an 88-key piano has a value of 21, and the highest note is 108. A “middle C” is 60. 109 | Velocity values are also given on a range from 0–127 (softest to loudest). The softest possible “note on” velocity is 1. 110 | A velocity of 0 is sometimes used in conjunction with a command value of 144 (which typically represents “note on”) to indicate a “note off” message, so it’s helpful to check if the given velocity is 0 as an alternate way of interpreting a “note off” message. 111 | */ 112 | 113 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Marc René Arns. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package webmididrv provides a Driver to connect to MIDI ports in the browser (via webmidi). 7 | See the example to get an idea how to use it. 8 | */ 9 | package webmididrv 10 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/driver.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm && !windows && !linux && !darwin 2 | // +build js,wasm,!windows,!linux,!darwin 3 | 4 | package webmididrv 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "sync" 10 | "syscall/js" 11 | 12 | "gitlab.com/gomidi/midi/v2/drivers" 13 | ) 14 | 15 | func init() { 16 | drv, err := New() 17 | if err != nil { 18 | panic(fmt.Sprintf("could not register webmididrv: %s", err.Error())) 19 | } 20 | drivers.Register(drv) 21 | } 22 | 23 | type Driver struct { 24 | opened []drivers.Port 25 | sync.RWMutex 26 | inputsJS js.Value 27 | outputsJS js.Value 28 | wg sync.WaitGroup 29 | Err error 30 | } 31 | 32 | func (d *Driver) String() string { 33 | return "webmididrv" 34 | } 35 | 36 | // Close closes all open ports. It must be called at the end of a session. 37 | func (d *Driver) Close() (err error) { 38 | d.Lock() 39 | var e CloseErrors 40 | 41 | for _, p := range d.opened { 42 | err = p.Close() 43 | if err != nil { 44 | e = append(e, err) 45 | } 46 | } 47 | 48 | d.Unlock() 49 | 50 | if len(e) == 0 { 51 | return nil 52 | } 53 | 54 | return e 55 | } 56 | 57 | // New returns a driver based on the js webmidi standard 58 | func New() (*Driver, error) { 59 | jsDoc := js.Global().Get("navigator") 60 | if !jsDoc.Truthy() { 61 | return nil, fmt.Errorf("Unable to get navigator object") 62 | } 63 | 64 | // currently sysex messages are not allowed in the browser implementations 65 | var opts = map[string]interface{}{ 66 | "sysex": "false", 67 | } 68 | 69 | jsOpts := js.ValueOf(opts) 70 | 71 | midiaccess := jsDoc.Call("requestMIDIAccess", jsOpts) 72 | if !midiaccess.Truthy() { 73 | return nil, fmt.Errorf("unable to get requestMIDIAccess") 74 | } 75 | 76 | drv := &Driver{} 77 | drv.wg.Add(1) 78 | midiaccess.Call("then", drv.onMIDISuccess(), drv.onMIDIFailure()) 79 | drv.wg.Wait() 80 | return drv, nil 81 | } 82 | 83 | func (d *Driver) onMIDISuccess() js.Func { 84 | return js.FuncOf(func(this js.Value, args []js.Value) interface{} { 85 | if len(args) != 1 { 86 | return "Invalid no of arguments passed" 87 | } 88 | 89 | d.inputsJS = args[0].Get("inputs") 90 | d.outputsJS = args[0].Get("outputs") 91 | d.wg.Done() 92 | return nil 93 | }) 94 | } 95 | 96 | func (d *Driver) onMIDIFailure() js.Func { 97 | return js.FuncOf(func(this js.Value, args []js.Value) interface{} { 98 | d.Err = fmt.Errorf("Could not access the MIDI devices.") 99 | d.wg.Done() 100 | return nil 101 | }) 102 | } 103 | 104 | // Ins returns the available MIDI input ports 105 | func (d *Driver) Ins() (ins []drivers.In, err error) { 106 | if d.Err != nil { 107 | return nil, err 108 | } 109 | 110 | if !d.inputsJS.Truthy() { 111 | return nil, fmt.Errorf("no inputs") 112 | } 113 | 114 | var i = 0 115 | 116 | eachIn := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 117 | jsport := args[0] 118 | var name = jsport.Get("name").String() 119 | ins = append(ins, newIn(d, i, name, jsport)) 120 | i++ 121 | return nil 122 | }) 123 | 124 | d.inputsJS.Call("forEach", eachIn) 125 | return ins, nil 126 | } 127 | 128 | // Outs returns the available MIDI output ports 129 | func (d *Driver) Outs() (outs []drivers.Out, err error) { 130 | if d.Err != nil { 131 | return nil, err 132 | } 133 | 134 | if !d.outputsJS.Truthy() { 135 | return nil, fmt.Errorf("no outputs") 136 | } 137 | 138 | var i = 0 139 | 140 | eachOut := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 141 | jsport := args[0] 142 | var name = jsport.Get("name").String() 143 | outs = append(outs, newOut(d, i, name, jsport)) 144 | i++ 145 | return nil 146 | }) 147 | 148 | d.outputsJS.Call("forEach", eachOut) 149 | 150 | return outs, nil 151 | } 152 | 153 | // CloseErrors collects error from closing multiple MIDI ports 154 | type CloseErrors []error 155 | 156 | func (c CloseErrors) Error() string { 157 | if len(c) == 0 { 158 | return "no errors" 159 | } 160 | 161 | var bd strings.Builder 162 | 163 | bd.WriteString("the following closing errors occured:\n") 164 | 165 | for _, e := range c { 166 | bd.WriteString(e.Error() + "\n") 167 | } 168 | 169 | return bd.String() 170 | } 171 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/driver_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm && !windows && !linux && !darwin 2 | // +build js,wasm,!windows,!linux,!darwin 3 | 4 | package webmididrv 5 | 6 | import ( 7 | "testing" 8 | 9 | "gitlab.com/gomidi/midi/v2" 10 | "gitlab.com/gomidi/midi/v2/drivers" 11 | "gitlab.com/gomidi/midi/v2/drivers/drivertest" 12 | ) 13 | 14 | func runTest(t *testing.T, fn func(*testing.T, drivers.In, drivers.Out)) func(*testing.T) { 15 | return func(*testing.T) { 16 | drv, err := New() 17 | if err != nil { 18 | t.Fatalf("ERROR: %s", err.Error()) 19 | } 20 | 21 | //fmt.Println(midi.GetInPorts().String()) 22 | 23 | in, err := midi.FindInPort("Midi Through Port-0") 24 | if err != nil { 25 | t.Skipf("could not find in port Midi Through Port-0") 26 | return 27 | } 28 | out, err := midi.FindOutPort("Midi Through Port-0") 29 | if err != nil { 30 | t.Skipf("could not find out port Midi Through Port-0") 31 | return 32 | } 33 | 34 | fn(t, in, out) 35 | drv.Close() 36 | } 37 | } 38 | 39 | func TestSpec(t *testing.T) { 40 | drv, err := New() 41 | if err != nil { 42 | t.Fatalf("ERROR: %s", err.Error()) 43 | } 44 | 45 | drivertest.DriverInterfaceImplementationTest(t, drv) 46 | drivertest.AutoregisterTest(t, drv) 47 | 48 | tests := []struct { 49 | name string 50 | fn func(*testing.T, drivers.In, drivers.Out) 51 | }{ 52 | { 53 | "RunningStatus", 54 | drivertest.RunningStatusTest, 55 | }, 56 | { 57 | "FullStatus", 58 | drivertest.FullStatusTest, 59 | }, 60 | { 61 | "NoActiveSense", 62 | drivertest.NoActiveSenseTest, 63 | }, 64 | { 65 | "NoTimeCode", 66 | drivertest.NoTimeCodeTest, 67 | }, 68 | /* 69 | { 70 | "Sysex", 71 | drivertest.SysexTest, 72 | }, 73 | { 74 | "NoSysex", 75 | drivertest.NoSysexTest, 76 | }, 77 | */ 78 | } 79 | 80 | for _, test := range tests { 81 | f := runTest(t, test.fn) 82 | t.Run(test.name, f) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/helpers.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm && !windows && !linux && !darwin 2 | // +build js,wasm,!windows,!linux,!darwin 3 | 4 | package webmididrv 5 | 6 | import ( 7 | "syscall/js" 8 | 9 | "gitlab.com/gomidi/midi/v2/drivers" 10 | ) 11 | 12 | func log(s string) { 13 | jsConsole := js.Global().Get("console") 14 | 15 | if !jsConsole.Truthy() { 16 | return 17 | } 18 | 19 | jsConsole.Call("log", js.ValueOf(s)) 20 | } 21 | 22 | type inPorts []drivers.In 23 | 24 | func (i inPorts) Len() int { 25 | return len(i) 26 | } 27 | 28 | func (i inPorts) Swap(a, b int) { 29 | i[a], i[b] = i[b], i[a] 30 | } 31 | 32 | func (i inPorts) Less(a, b int) bool { 33 | return i[a].Number() < i[b].Number() 34 | } 35 | 36 | type outPorts []drivers.Out 37 | 38 | func (i outPorts) Len() int { 39 | return len(i) 40 | } 41 | 42 | func (i outPorts) Swap(a, b int) { 43 | i[a], i[b] = i[b], i[a] 44 | } 45 | 46 | func (i outPorts) Less(a, b int) bool { 47 | return i[a].Number() < i[b].Number() 48 | } 49 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/in.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm && !windows && !linux && !darwin 2 | // +build js,wasm,!windows,!linux,!darwin 3 | 4 | package webmididrv 5 | 6 | import ( 7 | "math" 8 | "sync" 9 | "sync/atomic" 10 | "syscall/js" 11 | 12 | "gitlab.com/gomidi/midi/v2" 13 | "gitlab.com/gomidi/midi/v2/drivers" 14 | ) 15 | 16 | type in struct { 17 | number int 18 | sync.RWMutex 19 | driver *Driver 20 | name string 21 | isOpen bool 22 | jsport js.Value 23 | listener func(data []byte, timestamp int32) 24 | } 25 | 26 | // IsOpen returns wether the MIDI in port is open 27 | func (o *in) IsOpen() (open bool) { 28 | o.RLock() 29 | open = o.isOpen 30 | o.RUnlock() 31 | return 32 | } 33 | 34 | // String returns the name of the MIDI in port. 35 | func (i *in) String() string { 36 | return i.name 37 | } 38 | 39 | // Underlying returns the underlying driver. Here returns the js midi port. 40 | func (i *in) Underlying() interface{} { 41 | return i.jsport 42 | } 43 | 44 | // Number returns the number of the MIDI in port. 45 | // Note that with rtmidi, out and in ports are counted separately. 46 | // That means there might exists out ports and an in ports that share the same number. 47 | func (i *in) Number() int { 48 | return i.number 49 | } 50 | 51 | // Close closes the MIDI in port, after it has stopped listening. 52 | func (i *in) Close() (err error) { 53 | if !i.IsOpen() { 54 | return nil 55 | } 56 | 57 | i.Lock() 58 | i.isOpen = false 59 | i.jsport.Call("close") 60 | i.Unlock() 61 | return 62 | } 63 | 64 | // Open opens the MIDI in port 65 | func (i *in) Open() (err error) { 66 | if i.IsOpen() { 67 | return nil 68 | } 69 | i.Lock() 70 | i.isOpen = true 71 | i.jsport.Call("open") 72 | i.Unlock() 73 | 74 | i.driver.Lock() 75 | i.driver.opened = append(i.driver.opened, i) 76 | i.driver.Unlock() 77 | 78 | return nil 79 | } 80 | 81 | func newIn(driver *Driver, number int, name string, jsport js.Value) drivers.In { 82 | return &in{driver: driver, number: number, name: name, jsport: jsport} 83 | } 84 | 85 | /* 86 | i.Lock() 87 | //i.listener = recv.Receive 88 | 89 | jsCallback := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 90 | jsdata := args[0].Get("data") 91 | jstime := args[0].Get("receivedTime") 92 | 93 | var data = make([]byte, 3) 94 | data[0] = byte(jsdata.Index(0).Int()) 95 | data[1] = byte(jsdata.Index(1).Int()) 96 | data[2] = byte(jsdata.Index(2).Int()) 97 | var t = int32(-1) 98 | if jstime.Truthy() { 99 | // round to milliseconds 100 | t = int32(math.Round(jstime.Float())) 101 | } 102 | //i.listener(data, t) 103 | cb(data, t) 104 | return nil 105 | }) 106 | 107 | i.jsport.Call("addEventListener", "midimessage", jsCallback) 108 | i.Unlock() 109 | 110 | */ 111 | 112 | func (i *in) Listen(onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) (stopFn func(), err error) { 113 | 114 | var stop int32 115 | 116 | //stopWait := i.driver.sleepingTime * 2 117 | stopFn = func() { 118 | // lockless sync 119 | atomic.StoreInt32(&stop, 1) 120 | //time.Sleep(stopWait) 121 | } 122 | 123 | i.Lock() 124 | 125 | var stopped int32 126 | 127 | jsCallback := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 128 | // lockless sync 129 | stopped = atomic.LoadInt32(&stop) 130 | 131 | if stopped == 1 { 132 | return nil 133 | } 134 | 135 | jsdata := args[0].Get("data") 136 | jstime := args[0].Get("receivedTime") 137 | 138 | //var data = make([]byte, 3) 139 | var data []byte 140 | if !jsdata.Index(0).IsUndefined() { 141 | //data[0] = byte(jsdata.Index(0).Int()) 142 | data = append(data, byte(jsdata.Index(0).Int())) 143 | } 144 | if !jsdata.Index(1).IsUndefined() { 145 | //data[1] = byte(jsdata.Index(1).Int()) 146 | data = append(data, byte(jsdata.Index(1).Int())) 147 | } 148 | if !jsdata.Index(2).IsUndefined() { 149 | //data[2] = byte(jsdata.Index(2).Int()) 150 | data = append(data, byte(jsdata.Index(2).Int())) 151 | } 152 | var t = int32(-1) 153 | if jstime.Truthy() { 154 | // round to milliseconds 155 | t = int32(math.Round(jstime.Float())) 156 | } 157 | 158 | msg := midi.Message(data) 159 | 160 | if msg.Is(midi.ActiveSenseMsg) && !config.ActiveSense { 161 | return nil 162 | } 163 | 164 | if msg.Is(midi.TimingClockMsg) && !config.TimeCode { 165 | return nil 166 | } 167 | 168 | if msg.Is(midi.SysExMsg) && !config.SysEx { 169 | return nil 170 | } 171 | 172 | onMsg(data, t) 173 | return nil 174 | }) 175 | 176 | go i.jsport.Call("addEventListener", "midimessage", jsCallback) 177 | i.Unlock() 178 | 179 | return 180 | } 181 | 182 | /* 183 | // SendTo 184 | func (i *in) StartListening(cb func(data []byte, timestamp int32)) (err error) { 185 | if !i.IsOpen() { 186 | return drivers.ErrPortClosed 187 | } 188 | 189 | i.RLock() 190 | if i.listener != nil { 191 | i.RUnlock() 192 | return fmt.Errorf("listener already set") 193 | } 194 | i.RUnlock() 195 | i.Lock() 196 | //i.listener = recv.Receive 197 | 198 | jsCallback := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 199 | jsdata := args[0].Get("data") 200 | jstime := args[0].Get("receivedTime") 201 | 202 | var data = make([]byte, 3) 203 | data[0] = byte(jsdata.Index(0).Int()) 204 | data[1] = byte(jsdata.Index(1).Int()) 205 | data[2] = byte(jsdata.Index(2).Int()) 206 | var t = int32(-1) 207 | if jstime.Truthy() { 208 | // round to milliseconds 209 | t = int32(math.Round(jstime.Float())) 210 | } 211 | //i.listener(data, t) 212 | cb(data, t) 213 | return nil 214 | }) 215 | 216 | i.jsport.Call("addEventListener", "midimessage", jsCallback) 217 | i.Unlock() 218 | 219 | return nil 220 | } 221 | 222 | // StopListening cancels the listening 223 | func (i *in) StopListening() (err error) { 224 | if !i.IsOpen() { 225 | return drivers.ErrPortClosed 226 | } 227 | 228 | // TODO 229 | return 230 | } 231 | */ 232 | -------------------------------------------------------------------------------- /v2/drivers/webmididrv/out.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm && !windows && !linux && !darwin 2 | // +build js,wasm,!windows,!linux,!darwin 3 | 4 | package webmididrv 5 | 6 | import ( 7 | "bytes" 8 | "sync" 9 | "syscall/js" 10 | 11 | "gitlab.com/gomidi/midi/v2/drivers" 12 | ) 13 | 14 | func newOut(driver *Driver, number int, name string, jsport js.Value) drivers.Out { 15 | o := &out{driver: driver, number: number, name: name, jsport: jsport} 16 | return o 17 | } 18 | 19 | type out struct { 20 | number int 21 | sync.RWMutex 22 | driver *Driver 23 | name string 24 | jsport js.Value 25 | isOpen bool 26 | bf bytes.Buffer 27 | running *drivers.Reader 28 | } 29 | 30 | // IsOpen returns wether the port is open 31 | func (o *out) IsOpen() (open bool) { 32 | o.RLock() 33 | open = o.isOpen 34 | o.RUnlock() 35 | return 36 | } 37 | 38 | // Send writes a MIDI message to the MIDI output port 39 | // If the output port is closed, it returns midi.ErrClosed 40 | func (o *out) Send(b []byte) error { 41 | o.RLock() 42 | if !o.isOpen { 43 | o.RUnlock() 44 | return drivers.ErrPortClosed 45 | } 46 | o.RUnlock() 47 | 48 | o.running.EachMessage(b, 0) 49 | b = o.bf.Bytes() 50 | o.bf.Reset() 51 | 52 | var arr = make([]interface{}, len(b)) 53 | for i, bt := range b { 54 | arr[i] = bt 55 | } 56 | 57 | o.jsport.Call("send", js.ValueOf(arr)) 58 | return nil 59 | } 60 | 61 | // Underlying returns the underlying driver. Here it returns the js output port. 62 | func (o *out) Underlying() interface{} { 63 | return o.jsport 64 | } 65 | 66 | // Number returns the number of the MIDI out port. 67 | // Note that with rtmidi, out and in ports are counted separately. 68 | // That means there might exists out ports and an in ports that share the same number 69 | func (o *out) Number() int { 70 | return o.number 71 | } 72 | 73 | // String returns the name of the MIDI out port. 74 | func (o *out) String() string { 75 | return o.name 76 | } 77 | 78 | // Close closes the MIDI out port 79 | func (o *out) Close() (err error) { 80 | if !o.IsOpen() { 81 | return nil 82 | } 83 | 84 | o.Lock() 85 | defer o.Unlock() 86 | o.isOpen = false 87 | o.jsport.Call("close") 88 | return err 89 | } 90 | 91 | // Open opens the MIDI out port 92 | func (o *out) Open() (err error) { 93 | if o.IsOpen() { 94 | return nil 95 | } 96 | 97 | o.driver.Lock() 98 | o.bf = bytes.Buffer{} 99 | //o.running = runningstatus.NewLiveWriter(&o.bf) 100 | var conf drivers.ListenConfig 101 | conf.ActiveSense = true 102 | conf.SysEx = false 103 | conf.TimeCode = true 104 | o.running = drivers.NewReader(conf, func(b []byte, ms int32) { 105 | o.bf.Write(b) 106 | }) 107 | o.isOpen = true 108 | o.jsport.Call("open") 109 | o.driver.opened = append(o.driver.opened, o) 110 | o.driver.Unlock() 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /v2/example_test.go: -------------------------------------------------------------------------------- 1 | package midi_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | . "gitlab.com/gomidi/midi/v2" 8 | "gitlab.com/gomidi/midi/v2/gm" 9 | 10 | // testdrv has one in port and one out port which is connected to the in port 11 | _ "gitlab.com/gomidi/midi/v2/drivers/testdrv" 12 | //_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" 13 | // when using rtmidi ("for real"), replace with the line above 14 | ) 15 | 16 | func Example() { 17 | 18 | var eachMessage = func(msg Message, timestampms int32) { 19 | if msg.Is(RealTimeMsg) { 20 | // ignore realtime messages 21 | return 22 | } 23 | var channel, key, velocity, cc, val, prog uint8 24 | switch { 25 | 26 | // is better, than to use GetNoteOn (handles note on messages with velocity of 0 as expected) 27 | case msg.GetNoteStart(&channel, &key, &velocity): 28 | fmt.Printf("note started channel: %v key: %v (%s) velocity: %v\n", channel, key, Note(key), velocity) 29 | 30 | // is better, than to use GetNoteOff (handles note on messages with velocity of 0 as expected) 31 | case msg.GetNoteEnd(&channel, &key): 32 | fmt.Printf("note ended channel: %v key: %v (%s)\n", channel, key, Note(key)) 33 | 34 | case msg.GetControlChange(&channel, &cc, &val): 35 | fmt.Printf("control change %v (%s) channel: %v value: %v\n", cc, ControlChangeName[cc], channel, val) 36 | 37 | case msg.GetProgramChange(&channel, &prog): 38 | fmt.Printf("program change %v (%s) channel: %v\n", prog, gm.Instr(prog), channel) 39 | 40 | default: 41 | fmt.Printf("%s\n", msg) 42 | } 43 | } 44 | 45 | // always good to close the driver at the end 46 | defer CloseDriver() 47 | 48 | // allows you to get the ports when using "real" drivers like rtmididrv or portmididrv 49 | if len(os.Args) == 2 && os.Args[1] == "list" { 50 | fmt.Printf("MIDI IN Ports\n") 51 | fmt.Println(GetInPorts()) 52 | fmt.Printf("\n\nMIDI OUT Ports\n") 53 | fmt.Println(GetOutPorts()) 54 | fmt.Printf("\n\n") 55 | return 56 | } 57 | 58 | var out, _ = OutPort(0) 59 | // takes the first out port, for real, consider 60 | // var out = OutByName("my synth") 61 | 62 | // creates a sender function to the out port 63 | send, _ := SendTo(out) 64 | 65 | var in, _ = InPort(0) 66 | // here we take first in port, for real, consider 67 | // var in = InByName("my midi keyboard") 68 | 69 | // listens to the in port and calls eachMessage for every message. 70 | // any running status bytes are converted and only complete messages are passed to the eachMessage. 71 | stop, _ := ListenTo(in, eachMessage) 72 | 73 | { // send some messages 74 | send(NoteOn(0, Db(5), 100)) 75 | send(NoteOff(0, Db(5))) 76 | send(Pitchbend(0, -12)) 77 | send(ProgramChange(1, gm.Instr_AcousticBass.Value())) 78 | send(ControlChange(2, FootPedalMSB, On)) 79 | } 80 | 81 | // stops listening 82 | stop() 83 | 84 | // Output: 85 | // note started channel: 0 key: 61 (Db5) velocity: 100 86 | // note ended channel: 0 key: 61 (Db5) 87 | // PitchBend channel: 0 pitch: -12 (8180) 88 | // program change 32 (AcousticBass) channel: 1 89 | // control change 4 (Foot Pedal (MSB)) channel: 2 value: 127 90 | 91 | } 92 | -------------------------------------------------------------------------------- /v2/examples/.gitignore: -------------------------------------------------------------------------------- 1 | logger/logger 2 | simple/simple 3 | sysex/sysex 4 | *.mid 5 | *.MID 6 | smfplayer/smfplayer 7 | smfrecorder/smfrecorder 8 | webmidi/main 9 | webmidi/main.wasm 10 | webmidi/webmidi 11 | *.png -------------------------------------------------------------------------------- /v2/examples/logger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "gitlab.com/gomidi/midi/v2" 8 | _ "gitlab.com/gomidi/midi/v2/drivers/midicatdrv" 9 | ) 10 | 11 | func main() { 12 | defer midi.CloseDriver() 13 | 14 | in, err := midi.FindInPort("VMPK") 15 | if err != nil { 16 | fmt.Println("can't find VMPK") 17 | return 18 | } 19 | 20 | stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { 21 | var bt []byte 22 | var ch, key, vel uint8 23 | switch { 24 | case msg.GetSysEx(&bt): 25 | fmt.Printf("got sysex: % X\n", bt) 26 | case msg.GetNoteStart(&ch, &key, &vel): 27 | fmt.Printf("starting note %s on channel %v with velocity %v\n", midi.Note(key), ch, vel) 28 | case msg.GetNoteEnd(&ch, &key): 29 | fmt.Printf("ending note %s on channel %v\n", midi.Note(key), ch) 30 | default: 31 | // ignore 32 | } 33 | }, midi.UseSysEx()) 34 | 35 | if err != nil { 36 | fmt.Printf("ERROR: %s\n", err) 37 | return 38 | } 39 | 40 | time.Sleep(time.Second * 5) 41 | 42 | stop() 43 | } 44 | -------------------------------------------------------------------------------- /v2/examples/looper/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | "gitlab.com/gomidi/midi/v2" 11 | "gitlab.com/gomidi/midi/v2/drivers" 12 | _ "gitlab.com/gomidi/midi/v2/drivers/midicatdrv" // autoregisters driver 13 | "gitlab.com/gomidi/midi/v2/smf" 14 | ) 15 | 16 | func record(in drivers.In, ticks smf.MetricTicks, bpm float64, send func(msg midi.Message) error) (stop func() smf.Track) { 17 | var tr smf.Track 18 | var absmillisec int32 19 | 20 | _stop, err := midi.ListenTo(in, func(msg midi.Message, absms int32) { 21 | send(msg) 22 | deltams := absms - absmillisec 23 | absmillisec = absms 24 | delta := ticks.Ticks(bpm, time.Duration(deltams)*time.Millisecond) 25 | tr.Add(delta, msg) 26 | }) 27 | 28 | if err != nil { 29 | fmt.Printf("ERROR: %s\n", err) 30 | return nil 31 | } 32 | 33 | return func() smf.Track { 34 | _stop() 35 | tr.Close(0) 36 | return tr 37 | } 38 | } 39 | 40 | func main() { 41 | defer midi.CloseDriver() 42 | 43 | in, err := midi.FindInPort("VMPK") 44 | if err != nil { 45 | fmt.Println("can't find in port") 46 | return 47 | } 48 | out, err := midi.FindOutPort("qsynth") 49 | if err != nil { 50 | fmt.Println("can't find out port") 51 | return 52 | } 53 | 54 | send, _ := midi.SendTo(out) 55 | 56 | var s smf.SMF 57 | var ticks = smf.MetricTicks(960) 58 | s.TimeFormat = ticks 59 | var bpm float64 = 120.00 60 | 61 | var tempoTrack smf.Track 62 | var tick = midi.NoteOn(9, 60, 120) 63 | var tickOff = midi.NoteOff(9, 60) 64 | var tock = midi.NoteOn(9, 50, 100) 65 | var tockOff = midi.NoteOff(9, 50) 66 | 67 | // 2 bars ticks and tocks on quarter notes 68 | tempoTrack.Add(0, tick) 69 | for i := 0; i < 7; i++ { 70 | if (i+1)%4 == 0 { 71 | tempoTrack.Add(ticks.Ticks4th(), tockOff) 72 | tempoTrack.Add(9, tick) 73 | } else { 74 | tempoTrack.Add(ticks.Ticks4th(), tickOff) 75 | tempoTrack.Add(9, tock) 76 | } 77 | } 78 | 79 | tempoTrack.Add(ticks.Ticks4th(), tockOff) 80 | tempoTrack.Close(0) 81 | s.Add(tempoTrack) 82 | 83 | var bf bytes.Buffer 84 | s.WriteTo(&bf) 85 | 86 | player := smf.ReadTracksFrom(bytes.NewReader(bf.Bytes())) 87 | 88 | sigchan := make(chan os.Signal, 10) 89 | 90 | // listen for ctrl+c 91 | go signal.Notify(sigchan, os.Interrupt) 92 | 93 | go func() { 94 | for { 95 | stop := record(in, ticks, bpm, send) 96 | player.Play(out) 97 | rec := stop() 98 | if !rec.IsEmpty() { 99 | s.Add(rec) 100 | print(".") 101 | } 102 | send(midi.ControlChange(0, midi.AllNotesOff, midi.On)) 103 | send(midi.ControlChange(9, midi.AllNotesOff, midi.On)) 104 | bf.Reset() 105 | s.WriteTo(&bf) 106 | player = smf.ReadTracksFrom(bytes.NewReader(bf.Bytes())) 107 | } 108 | }() 109 | 110 | // interrupt has happend 111 | <-sigchan 112 | fmt.Println("\n--interrupted!") 113 | send(midi.ControlChange(0, midi.AllNotesOff, midi.On)) 114 | send(midi.ControlChange(9, midi.AllNotesOff, midi.On)) 115 | s.WriteFile("recorded.mid") 116 | } 117 | -------------------------------------------------------------------------------- /v2/examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | // nothing to see here 5 | } 6 | -------------------------------------------------------------------------------- /v2/examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "gitlab.com/gomidi/midi/v2" 8 | "gitlab.com/gomidi/midi/v2/gm" 9 | "gitlab.com/gomidi/midi/v2/smf" 10 | 11 | _ "gitlab.com/gomidi/midi/v2/drivers/midicatdrv" 12 | ) 13 | 14 | func main() { 15 | defer midi.CloseDriver() 16 | 17 | fmt.Println("out ports: \n" + midi.GetOutPorts().String()) 18 | 19 | out, err := midi.FindOutPort("qsynth") 20 | if err != nil { 21 | fmt.Printf("can't find qsynth") 22 | return 23 | } 24 | 25 | // create a SMF 26 | rd := bytes.NewReader(mkSMF()) 27 | 28 | // read and play it 29 | smf.ReadTracksFrom(rd).Do(func(ev smf.TrackEvent) { 30 | fmt.Printf("track %v @%vms %s\n", ev.TrackNo, ev.AbsMicroSeconds/1000, ev.Message) 31 | }).Play(out) 32 | } 33 | 34 | // makes a SMF and returns the bytes 35 | func mkSMF() []byte { 36 | var ( 37 | bf bytes.Buffer 38 | clock = smf.MetricTicks(96) // resolution: 96 ticks per quarternote 960 is also common 39 | tr smf.Track 40 | ) 41 | 42 | // first track must have tempo and meter informations 43 | tr.Add(0, smf.MetaMeter(3, 4)) 44 | tr.Add(0, smf.MetaTempo(140)) 45 | tr.Add(0, smf.MetaInstrument("Brass")) 46 | tr.Add(0, midi.ProgramChange(0, gm.Instr_BrassSection.Value())) 47 | tr.Add(0, midi.NoteOn(0, midi.Ab(3), 120)) 48 | tr.Add(clock.Ticks8th(), midi.NoteOn(0, midi.C(4), 120)) 49 | // duration: a quarter note (96 ticks in our case) 50 | tr.Add(clock.Ticks4th()*2, midi.NoteOff(0, midi.Ab(3))) 51 | tr.Add(0, midi.NoteOff(0, midi.C(4))) 52 | tr.Close(0) 53 | 54 | // create the SMF and add the tracks 55 | s := smf.New() 56 | s.TimeFormat = clock 57 | s.Add(tr) 58 | s.WriteTo(&bf) 59 | return bf.Bytes() 60 | } 61 | -------------------------------------------------------------------------------- /v2/examples/smfplayer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gitlab.com/gomidi/midi/v2" 7 | "gitlab.com/gomidi/midi/v2/smf" 8 | 9 | _ "gitlab.com/gomidi/midi/v2/drivers/midicatdrv" 10 | ) 11 | 12 | func printPorts() { 13 | fmt.Println(midi.GetOutPorts()) 14 | } 15 | 16 | func run() error { 17 | printPorts() 18 | out, err := midi.FindOutPort("qsynth") 19 | if err != nil { 20 | return fmt.Errorf("can't find qsynth") 21 | } 22 | 23 | //return smf.ReadTracksFrom(bytes.NewReader(prelude4)). 24 | //return smf.ReadTracksFrom(bytes.NewReader(voyager)). 25 | return smf.ReadTracks("Prelude4.mid"). 26 | //result := smf.ReadTracks("VOYAGER.MID", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20). 27 | //Only(midi.NoteOnMsg, midi.NoteOffMsg). 28 | //Only(midi.NoteOnMsg, midi.NoteOffMsg, midi.MetaMsgType). 29 | //Only(midi.NoteMsg, midi.ControlChangeMsg, midi.ProgramChangeMsg). 30 | //Only(midi.NoteOnMsg, midi.NoteOffMsg, midi.ControlChangeMsg, midi.ProgramChangeMsg, smf.MetaTrackNameMsg). 31 | //Only(midi.ProgramChangeMsg, smf.MetaTrackNameMsg, smf.MetaTempoMsg, smf.MetaTimeSigMsg). 32 | //Only(smf.MetaMsg). 33 | Do( 34 | func(te smf.TrackEvent) { 35 | if te.Message.IsMeta() { 36 | fmt.Printf("[%v] @%vms %s\n", te.TrackNo, te.AbsMicroSeconds/1000, te.Message.String()) 37 | /* 38 | var t string 39 | if mm.Text(&t) { 40 | //fmt.Printf("[%v] %s %s (%s): %q\n", te.TrackNo, msg.Type().Kind(), msg.String(), msg.Type(), t) 41 | fmt.Printf("[%v] %s: %q\n", te.TrackNo, te.Type, t) 42 | //fmt.Printf("[%v] %s %s (%s): %q\n", te.TrackNo, mm.Type().Kind(), mm.String(), mm.Type(), t) 43 | } 44 | var bpm float64 45 | if mm.Tempo(&bpm) { 46 | fmt.Printf("[%v] %s: %v\n", te.TrackNo, te.Type, math.Round(bpm)) 47 | } 48 | */ 49 | } else { 50 | //fmt.Printf("[%v] %s\n", te.TrackNo, te.Message) 51 | } 52 | }, 53 | ).Play(out) 54 | } 55 | 56 | func main() { 57 | defer midi.CloseDriver() 58 | err := run() 59 | 60 | if err != nil { 61 | fmt.Printf("ERROR: %s\n", err.Error()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /v2/examples/smfrecorder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "gitlab.com/gomidi/midi/v2" 8 | "gitlab.com/gomidi/midi/v2/smf" 9 | 10 | _ "gitlab.com/gomidi/midi/v2/drivers/midicatdrv" 11 | ) 12 | 13 | func main() { 14 | err := run() 15 | 16 | if err != nil { 17 | fmt.Printf("ERROR: %s\n", err.Error()) 18 | } 19 | } 20 | 21 | func run() error { 22 | 23 | defer midi.CloseDriver() 24 | in, err := midi.FindInPort("VMPK") 25 | 26 | if err != nil { 27 | return fmt.Errorf("can't find MIDI in port %q", "VMPK") 28 | } 29 | 30 | stop, err := smf.RecordTo(in, 120, "recordedx.mid") 31 | 32 | if err != nil { 33 | return err 34 | } 35 | 36 | time.Sleep(5 * time.Second) 37 | 38 | stop() 39 | 40 | out, err := midi.FindOutPort("qsynth") 41 | if err != nil { 42 | return fmt.Errorf("can't find MIDI in port %q", "qsynth") 43 | } 44 | 45 | return smf.ReadTracks("recordedx.mid").Play(out) 46 | } 47 | -------------------------------------------------------------------------------- /v2/examples/sysex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "gitlab.com/gomidi/midi/v2" 8 | _ "gitlab.com/gomidi/midi/v2/drivers/testdrv" // autoregisters driver 9 | "gitlab.com/gomidi/midi/v2/sysex" 10 | ) 11 | 12 | func main() { 13 | defer midi.CloseDriver() 14 | 15 | in, _ := midi.InPort(0) 16 | 17 | stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { 18 | var bt []byte 19 | switch { 20 | case msg.GetSysEx(&bt): 21 | fmt.Printf("got sysex: % X\n", bt) 22 | default: 23 | // ignore 24 | } 25 | }, midi.UseSysEx()) 26 | 27 | if err != nil { 28 | fmt.Printf("ERROR: %s\n", err) 29 | return 30 | } 31 | 32 | out, _ := midi.OutPort(0) 33 | 34 | send, err := midi.SendTo(out) 35 | 36 | if err != nil { 37 | fmt.Printf("ERROR: %s\n", err) 38 | return 39 | } 40 | 41 | reset := sysex.GMReset.SysEx() 42 | fmt.Printf("sending reset:\n% X,\n", reset) 43 | 44 | send(reset) 45 | 46 | time.Sleep(time.Second * 1) 47 | 48 | stop() 49 | } 50 | -------------------------------------------------------------------------------- /v2/examples/webmidi/Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: 3 | GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o main.wasm main_js.go 4 | 5 | test: 6 | go test ./... -v -coverprofile .coverage.txt 7 | go tool cover -func .coverage.txt 8 | 9 | coverage: test 10 | go tool cover -html=.coverage.txt 11 | -------------------------------------------------------------------------------- /v2/examples/webmidi/README.md: -------------------------------------------------------------------------------- 1 | # Example for webmididrv 2 | 3 | Build the `main.wasm` file with 4 | 5 | ``` 6 | GOOS=js GOARCH=wasm go build -o main.wasm main_js.go 7 | ``` 8 | 9 | Start the webserver with 10 | 11 | ``` 12 | go run main.go 13 | ``` 14 | 15 | And then point your browser to `http://localhost:8080`. 16 | 17 | If you need a fresh `wasm_exec.js`, you can get it with 18 | 19 | ``` 20 | cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . 21 | ``` 22 | 23 | If you create your own html file, make sure, it contains the lines 24 | 25 | 26 | ```html 27 | 28 | 34 | ``` 35 | -------------------------------------------------------------------------------- /v2/examples/webmidi/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 11 | 12 | 13 |