├── .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 |

Demo of gomidi/webmididrv

14 | 15 | -------------------------------------------------------------------------------- /v2/examples/webmidi/main.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | //DownloadWasmExec() 15 | fileServer := http.FileServer(http.Dir(".")) 16 | http.Handle("/", fileServer) 17 | println("Listening on port 8080...") 18 | err := http.ListenAndServe(":8080", nil) 19 | if err != nil { 20 | fmt.Printf("ERROR: %s\n", err.Error()) 21 | } 22 | } 23 | 24 | //const wasmExecURL = "https://raw.githubusercontent.com/golang/go/release-branch.go1.12/misc/wasm/wasm_exec.js" 25 | const wasmExecURL = "https://raw.githubusercontent.com/golang/go/master/misc/wasm/wasm_exec.js" 26 | const wasmExecFile = "wasm_exec.js" 27 | 28 | func DownloadWasmExec() { 29 | if _, err := os.Stat(wasmExecFile); err == nil { 30 | return 31 | } 32 | println("Downloading wasm_exec.js...") 33 | out, err := os.Create(wasmExecFile) 34 | if err != nil { 35 | panic(err) 36 | } 37 | defer out.Close() 38 | resp, err := http.Get(wasmExecURL) 39 | if err != nil { 40 | panic(err) 41 | } 42 | defer resp.Body.Close() 43 | _, err = io.Copy(out, resp.Body) 44 | if err != nil { 45 | panic(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /v2/examples/webmidi/main_js.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm,!windows,!linux,!darwin 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "syscall/js" 9 | "time" 10 | 11 | "gitlab.com/gomidi/midi/v2" 12 | _ "gitlab.com/gomidi/midi/v2/drivers/webmididrv" 13 | ) 14 | 15 | /* 16 | to build, run 17 | 18 | GOOS=js GOARCH=wasm go build -o main.wasm main_js.go 19 | */ 20 | 21 | func log(message string) { 22 | document := js.Global().Get("document") 23 | p := document.Call("createElement", "p") 24 | p.Set("innerHTML", message) 25 | document.Get("body").Call("appendChild", p) 26 | } 27 | 28 | func main() { 29 | defer midi.CloseDriver() 30 | var bf bytes.Buffer 31 | 32 | for i, in := range midi.GetInPorts() { 33 | fmt.Fprintf(&bf, "found MIDI in port: %v: %s
", i, in) 34 | } 35 | 36 | fmt.Fprintf(&bf, "

") 37 | 38 | for i, out := range midi.GetOutPorts() { 39 | fmt.Fprintf(&bf, "found MIDI out port: %v: %s
", i, out) 40 | } 41 | 42 | log(bf.String()) 43 | 44 | in, err := midi.InPort(0) 45 | e(err) 46 | 47 | stop, err := midi.ListenTo(in, func(msg midi.Message, timestamp int32) { 48 | log(fmt.Sprintf("got: %s
", msg)) 49 | }) 50 | e(err) 51 | 52 | out, err := midi.OutPort(0) 53 | e(err) 54 | 55 | send, err := midi.SendTo(out) 56 | e(err) 57 | 58 | log(fmt.Sprintf("send: NoteOn key: %v veloctiy: %v on channel %v
", 60, 120, 3)) 59 | 60 | // do some writing: if you are using a loopback midi device on your os, you will see 61 | // this messages in the browser window 62 | send(midi.NoteOn(3, 60, 120)) 63 | time.Sleep(time.Second) 64 | log(fmt.Sprintf("send: NoteOff key: %v on channel %v
", 60, 3)) 65 | send(midi.NoteOff(3, 60)) 66 | 67 | qsynth, err := midi.FindOutPort("qsynth") 68 | 69 | if err == nil { 70 | qsend, err := midi.SendTo(qsynth) 71 | e(err) 72 | 73 | qsend(midi.NoteOn(0, 60, 120)) 74 | time.Sleep(time.Millisecond * 500) 75 | qsend(midi.NoteOff(0, 60)) 76 | } 77 | 78 | // stay alive 79 | ch := make(chan bool) 80 | <-ch 81 | stop() 82 | } 83 | 84 | func e(err error) { 85 | if err != nil { 86 | log(fmt.Sprintf("ERROR: %s", err.Error())) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /v2/gm/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 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 gm provides constants for instruments, drumkits and percussion keys based on the General MIDI standard. 7 | */ 8 | package gm 9 | -------------------------------------------------------------------------------- /v2/gm/drumkits.go: -------------------------------------------------------------------------------- 1 | package gm 2 | 3 | type DrumKit uint8 4 | 5 | func (d DrumKit) Value() uint8 { 6 | return uint8(d) 7 | } 8 | 9 | const ( 10 | DrumKit_Standard DrumKit = 0 11 | DrumKit_Standard1 DrumKit = 1 12 | DrumKit_Standard2 DrumKit = 2 13 | DrumKit_Standard3 DrumKit = 3 14 | DrumKit_Standard4 DrumKit = 4 15 | DrumKit_Standard5 DrumKit = 5 16 | DrumKit_Standard6 DrumKit = 6 17 | DrumKit_Standard7 DrumKit = 7 18 | DrumKit_Room DrumKit = 8 19 | DrumKit_Room1 DrumKit = 9 20 | DrumKit_Room2 DrumKit = 10 21 | DrumKit_Room3 DrumKit = 11 22 | DrumKit_Room4 DrumKit = 12 23 | DrumKit_Room5 DrumKit = 13 24 | DrumKit_Room6 DrumKit = 14 25 | DrumKit_Room7 DrumKit = 15 26 | DrumKit_Power DrumKit = 16 27 | DrumKit_Power1 DrumKit = 17 28 | DrumKit_Power2 DrumKit = 18 29 | DrumKit_Power3 DrumKit = 19 30 | DrumKit_Power4 DrumKit = 20 31 | DrumKit_Power5 DrumKit = 21 32 | DrumKit_Power6 DrumKit = 22 33 | DrumKit_Power7 DrumKit = 23 34 | DrumKit_Electronic DrumKit = 24 35 | DrumKit_Electronic1 DrumKit = 25 36 | DrumKit_Electronic2 DrumKit = 26 37 | DrumKit_Electronic3 DrumKit = 27 38 | DrumKit_Electronic4 DrumKit = 28 39 | DrumKit_Electronic5 DrumKit = 29 40 | DrumKit_Electronic6 DrumKit = 30 41 | DrumKit_Electronic7 DrumKit = 31 42 | DrumKit_Tr808 DrumKit = 25 43 | DrumKit_Jazz DrumKit = 32 44 | DrumKit_Jazz1 DrumKit = 33 45 | DrumKit_Jazz2 DrumKit = 34 46 | DrumKit_Jazz3 DrumKit = 35 47 | DrumKit_Jazz4 DrumKit = 36 48 | DrumKit_Jazz5 DrumKit = 37 49 | DrumKit_Jazz6 DrumKit = 38 50 | DrumKit_Jazz7 DrumKit = 39 51 | DrumKit_Brush DrumKit = 40 52 | DrumKit_Brush1 DrumKit = 41 53 | DrumKit_Brush2 DrumKit = 42 54 | DrumKit_Brush3 DrumKit = 43 55 | DrumKit_Brush4 DrumKit = 44 56 | DrumKit_Brush5 DrumKit = 45 57 | DrumKit_Brush6 DrumKit = 46 58 | DrumKit_Brush7 DrumKit = 47 59 | DrumKit_Orchestra DrumKit = 48 60 | DrumKit_Orchestra1 DrumKit = 49 61 | DrumKit_Orchestra2 DrumKit = 50 62 | DrumKit_Orchestra3 DrumKit = 51 63 | DrumKit_Orchestra4 DrumKit = 52 64 | DrumKit_Orchestra5 DrumKit = 53 65 | DrumKit_Orchestra6 DrumKit = 54 66 | DrumKit_Orchestra7 DrumKit = 55 67 | DrumKit_SoundFX DrumKit = 56 68 | DrumKit_SoundFX1 DrumKit = 57 69 | DrumKit_SoundFX2 DrumKit = 58 70 | DrumKit_SoundFX3 DrumKit = 59 71 | DrumKit_SoundFX4 DrumKit = 60 72 | DrumKit_SoundFX5 DrumKit = 61 73 | DrumKit_SoundFX6 DrumKit = 62 74 | DrumKit_SoundFX7 DrumKit = 63 75 | ) 76 | -------------------------------------------------------------------------------- /v2/gm/percussion_keys.go: -------------------------------------------------------------------------------- 1 | package gm 2 | 3 | //General MIDI Percussion Key Map 4 | 5 | type DrumKey uint8 6 | 7 | func (d DrumKey) Key() uint8 { 8 | return uint8(d) + 1 9 | } 10 | 11 | const ( 12 | DrumKey_AcousticBassDrum DrumKey = 34 13 | DrumKey_BassDrum1 DrumKey = 35 14 | DrumKey_SideStick DrumKey = 36 15 | DrumKey_AcousticSnare DrumKey = 37 16 | DrumKey_HandClap DrumKey = 38 17 | DrumKey_ElectricSnare DrumKey = 39 18 | DrumKey_LowFloorTom DrumKey = 40 19 | DrumKey_ClosedHiHat DrumKey = 41 20 | DrumKey_HighFloorTom DrumKey = 42 21 | DrumKey_PedalHiHat DrumKey = 43 22 | DrumKey_LowTom DrumKey = 44 23 | DrumKey_OpenHiHat DrumKey = 45 24 | DrumKey_LowMidTom DrumKey = 46 25 | DrumKey_HiMidTom DrumKey = 47 26 | DrumKey_CrashCymbal1 DrumKey = 48 27 | DrumKey_HighTom DrumKey = 49 28 | DrumKey_RideCymbal1 DrumKey = 50 29 | DrumKey_ChineseCymbal DrumKey = 51 30 | DrumKey_RideBell DrumKey = 52 31 | DrumKey_Tambourine DrumKey = 53 32 | DrumKey_SplashCymbal DrumKey = 54 33 | DrumKey_Cowbell DrumKey = 55 34 | DrumKey_CrashCymbal2 DrumKey = 56 35 | DrumKey_Vibraslap DrumKey = 57 36 | DrumKey_RideCymbal2 DrumKey = 58 37 | DrumKey_HiBongo DrumKey = 59 38 | DrumKey_LowBongo DrumKey = 60 39 | DrumKey_MuteHiConga DrumKey = 61 40 | DrumKey_OpenHiConga DrumKey = 62 41 | DrumKey_LowConga DrumKey = 63 42 | DrumKey_HighTimbale DrumKey = 64 43 | DrumKey_LowTimbale DrumKey = 65 44 | DrumKey_HighAgogo DrumKey = 66 45 | DrumKey_LowAgogo DrumKey = 67 46 | DrumKey_Cabasa DrumKey = 68 47 | DrumKey_Maracas DrumKey = 69 48 | DrumKey_ShortWhistle DrumKey = 70 49 | DrumKey_LongWhistle DrumKey = 71 50 | DrumKey_ShortGuiro DrumKey = 72 51 | DrumKey_LongGuiro DrumKey = 73 52 | DrumKey_Claves DrumKey = 74 53 | DrumKey_HiWoodBlock DrumKey = 75 54 | DrumKey_LowWoodBlock DrumKey = 76 55 | DrumKey_MuteCuica DrumKey = 77 56 | DrumKey_OpenCuica DrumKey = 78 57 | DrumKey_MuteTriangle DrumKey = 79 58 | DrumKey_OpenTriangle DrumKey = 80 59 | ) 60 | -------------------------------------------------------------------------------- /v2/gm/reset.go: -------------------------------------------------------------------------------- 1 | package gm 2 | 3 | import ( 4 | "gitlab.com/gomidi/midi/v2" 5 | ) 6 | 7 | // GMProgram is a shortcut to write GM bank select control change message followed 8 | // by a program change. 9 | func GMProgram(ch, prog uint8) (msgs []midi.Message) { 10 | //c := channel.Channel(ch) 11 | msgs = append(msgs, midi.ControlChange(ch, midi.BankSelectMSB, 0)) 12 | msgs = append(msgs, midi.ProgramChange(ch, prog)) 13 | return 14 | } 15 | 16 | // Reset writes a kind of somewhat homegrown GM/GS reset message. 17 | // The idea is inspired by http://www.artandscienceofsound.com/article/standardmidifiles. 18 | // The following messages will be written to the writer on the given channel: 19 | /* 20 | cc bank select 0 21 | program change prog 22 | cc all controllers off 23 | cc volume 100 24 | cc expression 127 25 | cc hold pedal 0 26 | cc pan position 64 27 | */ 28 | func Reset(ch, prog uint8) []midi.Message { 29 | return []midi.Message{ 30 | midi.ControlChange(ch, midi.BankSelectMSB, 0), 31 | midi.ProgramChange(ch, prog), 32 | midi.ControlChange(ch, midi.AllControllersOff, 0), 33 | midi.ControlChange(ch, midi.VolumeMSB, 100), 34 | midi.ControlChange(ch, midi.ExpressionMSB, 127), 35 | midi.ControlChange(ch, midi.HoldPedalSwitch, 0), 36 | midi.ControlChange(ch, midi.PanPositionMSB, 64), 37 | } 38 | } 39 | 40 | /* 41 | default of 2 semitone 42 | Pitch Bend Range can be set by sending MIDI controller messages. Specifically, you do it with Registered Parameters (cc# 100 and 101). 43 | 44 | On the MIDI channel in question, you need to send: 45 | MIDI cc100 = 0 46 | MIDI cc101 = 0 47 | MIDI cc6 = value of desired bend range (in semitones) 48 | 49 | Example: Lets say you want to set the bend range to 2 semi-tones. First you send cc# 100 with a value of 0; then cc#101 with a value of 0. This turns on reception for setting pitch bend with the Data controller (#6). Then you send cc# 6 with a value of 2 (in semitones; this will give you a whole step up and a whole step down from the center). 50 | 51 | Once you have set the bend range the way you want, then you send controller 100 or 101 with a value of 127 so that any further messages of controller 6 (which you might be using for other stuff) won't change the bend range. 52 | */ 53 | 54 | /* 55 | from http://www.artandscienceofsound.com/article/standardmidifiles 56 | 57 | Depending upon the application you are using to create the file in the first place, header information may automatically be saved from within parameters set in the application, or may need to be placed in a ‘set-up’ bar before the music data commences. 58 | 59 | Either way, information that should be considered includes: 60 | 61 | GM/GS Reset message 62 | 63 | Per MIDI Channel 64 | Bank Select (0=GM) / Program Change # 65 | Reset All Controllers (not all devices may recognize this command so you may prefer to zero out or reset individual controllers) 66 | Initial Volume (CC7) (standard level = 100) 67 | Expression (CC11) (initial level set to 127) 68 | Hold pedal (0 = off) 69 | Pan (Center = 64) 70 | Modulation (0) 71 | Pitch bend range 72 | Reverb (0 = off) 73 | Chorus level (0 = off) 74 | */ 75 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module gitlab.com/gomidi/midi/v2 2 | 3 | go 1.24.2 4 | -------------------------------------------------------------------------------- /v2/helpers.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "io" 5 | 6 | "gitlab.com/gomidi/midi/v2/internal/utils" 7 | ) 8 | 9 | // channelMessage1 returns the bytes for a single byte channel message 10 | func channelMessage1(c uint8, status, msg byte) Message { 11 | cm := &channelMessage{channel: c, status: status} 12 | cm.data[0] = msg 13 | return cm.bytes() 14 | } 15 | 16 | // channelMessage2 returns the bytes for a two bytes channel message 17 | func channelMessage2(c uint8, status, msg1 byte, msg2 byte) Message { 18 | cm := &channelMessage{channel: c, status: status} 19 | cm.data[0] = msg1 20 | cm.data[1] = msg2 21 | cm.twoBytes = true 22 | return cm.bytes() 23 | } 24 | 25 | type channelMessage struct { 26 | status uint8 27 | channel uint8 28 | twoBytes bool 29 | data [2]byte 30 | } 31 | 32 | func (m *channelMessage) getCompleteStatus() uint8 { 33 | s := m.status << 4 34 | s = utils.ClearBitU8(s, 0) 35 | s = utils.ClearBitU8(s, 1) 36 | s = utils.ClearBitU8(s, 2) 37 | s = utils.ClearBitU8(s, 3) 38 | s = s | m.channel 39 | return s 40 | } 41 | 42 | func (m *channelMessage) bytes() []byte { 43 | if m.twoBytes { 44 | return []byte{m.getCompleteStatus(), m.data[0], m.data[1]} 45 | } 46 | return []byte{m.getCompleteStatus(), m.data[0]} 47 | } 48 | 49 | const ( 50 | byteProgramChange = 0xC 51 | byteChannelPressure = 0xD 52 | byteNoteOff = 0x8 53 | byteNoteOn = 0x9 54 | bytePolyphonicKeyPressure = 0xA 55 | byteControlChange = 0xB 56 | bytePitchWheel = 0xE 57 | ) 58 | 59 | // ReadChannelMessage reads a channel message for the given status byte from the given reader. 60 | // Don't use this function as a user, it is only internal to the library. 61 | func ReadChannelMessage(status byte, arg1 byte, rd io.Reader) (m Message, err error) { 62 | typ, channel := utils.ParseStatus(status) 63 | 64 | if err != nil { 65 | return 66 | } 67 | 68 | switch typ { 69 | 70 | // one argument only 71 | case byteProgramChange, byteChannelPressure: 72 | m = channelMessage1(channel, typ, arg1) 73 | 74 | // two Arguments needed 75 | default: 76 | var arg2 byte 77 | arg2, err = utils.ReadByte(rd) 78 | 79 | if err != nil { 80 | return 81 | } 82 | m = channelMessage2(channel, typ, arg1, arg2) 83 | } 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /v2/internal/runningstatus/runningstatus.go: -------------------------------------------------------------------------------- 1 | package runningstatus 2 | 3 | import ( 4 | "gitlab.com/gomidi/midi/v2" 5 | 6 | "io" 7 | ) 8 | 9 | // Reader is a running status reader 10 | type Reader interface { 11 | // Read reads the status byte off the canary and returns 12 | // if it has changed compared to the previous read 13 | Read(canary byte) (status byte, changed bool) 14 | } 15 | 16 | type reader struct { 17 | status byte 18 | } 19 | 20 | func (r *reader) read(canary byte) (status byte, changed bool) { 21 | 22 | // channel/Voice Category Status 23 | if canary >= 0x80 && canary <= 0xEF { 24 | r.status = canary 25 | changed = true 26 | } 27 | 28 | return r.status, changed 29 | } 30 | 31 | type livereader struct { 32 | reader 33 | } 34 | 35 | /* 36 | his (http://midi.teragonaudio.com/tech/midispec.htm) take on running status buffer 37 | A recommended approach for a receiving device is to maintain its "running status buffer" as so: 38 | 39 | Buffer is cleared (ie, set to 0) at power up. 40 | Buffer stores the status when a Voice Category Status (ie, 0x80 to 0xEF) is received. 41 | Buffer is cleared when a System Common Category Status (ie, 0xF0 to 0xF7) is received. 42 | Nothing is done to the buffer when a RealTime Category message is received. 43 | Any data bytes are ignored when the buffer is 0. (I think that only holds for realtime midi) 44 | */ 45 | 46 | // Read reads the status byte from the given canary, while respecting 47 | // running status and returns whether the status has changed 48 | func (r *livereader) Read(canary byte) (status byte, changed bool) { 49 | 50 | // here we clear for System Common Category messages 51 | if canary >= 0xF0 && canary <= 0xF7 { 52 | r.status = 0 53 | return r.status, true 54 | } 55 | 56 | return r.read(canary) 57 | } 58 | 59 | type smfreader struct { 60 | reader 61 | } 62 | 63 | // Read reads the status byte from the given canary, while respecting 64 | // running status and returns whether the status has changed 65 | func (r *smfreader) Read(canary byte) (status byte, changed bool) { 66 | 67 | // here we clear for meta messages 68 | if canary == 0xFF || canary == 0xF0 || canary == 0xF7 { 69 | r.status = 0 70 | return r.status, true 71 | } 72 | 73 | return r.read(canary) 74 | } 75 | 76 | // NewLiveReader returns a new Reader for reading of live MIDI data 77 | func NewLiveReader() Reader { 78 | return &livereader{} 79 | } 80 | 81 | // NewSMFReader returns a new Reader for reading of SMF MIDI data 82 | func NewSMFReader() Reader { 83 | return &smfreader{} 84 | } 85 | 86 | // Writer writes messages with running status byte 87 | type Writer interface { 88 | io.Writer 89 | runningstatus() 90 | } 91 | 92 | // NewSMFWriter returns a new SMFWriter 93 | func NewSMFWriter() SMFWriter { 94 | return &smfwriter{0} 95 | } 96 | 97 | // SMFWriter is a writer for writing messages with running status byte in SMF files 98 | type SMFWriter interface { 99 | Write([]byte) []byte 100 | ResetStatus() 101 | } 102 | 103 | // NewLiveWriter returns a new Writer for live writing of messages with running status byte 104 | func NewLiveWriter(output io.Writer) Writer { 105 | return &liveWriter{output, 0} 106 | } 107 | 108 | type smfwriter struct { 109 | status byte 110 | } 111 | 112 | func (w *smfwriter) ResetStatus() { 113 | w.status = 0 114 | } 115 | 116 | // Write writes the given message with running status 117 | func (w *smfwriter) Write(raw []byte) []byte { 118 | // raw := m.Data 119 | // fmt.Printf("should write %s (% X)\n", msg, raw) 120 | firstByte := raw[0] 121 | /* 122 | var b1, b2 byte = raw[0], 0 123 | if len(raw) > 1 { 124 | b2 = raw[1] 125 | } 126 | */ 127 | // for non channel messages, reset status and write whole message 128 | //if !midilib.IsChannelMessage(firstByte) { 129 | if !midi.Message(raw).Is(midi.ChannelMsg) { 130 | // if midi.GetMsgType(raw).Category() != midi.ChannelMessages { 131 | //fmt.Printf("is no channel message, resetting status\n") 132 | w.status = 0 133 | return raw 134 | } 135 | 136 | // for a different status, store runningStatus and write whole message 137 | if firstByte != w.status { 138 | //fmt.Printf("setting status to: % X (was: % X)\n", firstByte, w.status) 139 | w.status = firstByte 140 | return raw 141 | } 142 | 143 | // we got the same status as runningStatus, so omit the status byte when writing 144 | //fmt.Printf("taking running status (% X), writing: % X\n", w.status, raw[1:]) 145 | return raw[1:] 146 | } 147 | 148 | func (w *liveWriter) runningstatus() { 149 | 150 | } 151 | 152 | func (w *liveWriter) write(b []byte) (n int, err error) { 153 | return w.output.Write(b) 154 | } 155 | 156 | type liveWriter struct { 157 | output io.Writer 158 | status byte 159 | } 160 | 161 | // Write writes the given message with running status 162 | func (w *liveWriter) Write(m []byte) (int, error) { 163 | // fmt.Printf("should write % X\n", msg) 164 | // for realtime system messages, don't affect status and write the whole message 165 | if m[0] > 0xF7 { 166 | return w.write(m) 167 | } 168 | 169 | /* 170 | var b1, b2 byte = m[0], 0 171 | 172 | if len(m) > 1 { 173 | b2 = m[1] 174 | } 175 | */ 176 | // for non channel messages, reset status and write whole message 177 | //if !midilib.IsChannelMessage(msg[0]) { 178 | //if midi.GetMsgType(m).Category() != midi.ChannelMessages { 179 | if !midi.Message(m).Is(midi.ChannelMsg) { 180 | // fmt.Printf("is no channel message, resetting status\n") 181 | w.status = 0 182 | return w.write(m) 183 | } 184 | 185 | // for a different status, store runningStatus and write whole message 186 | if m[0] != w.status { 187 | // fmt.Printf("setting status to: % X (was: % X)\n", msg[0], w.status) 188 | w.status = m[0] 189 | return w.write(m) 190 | } 191 | 192 | // we got the same status as runningStatus, so omit the status byte when writing 193 | // fmt.Printf("taking running status (% X), writing: % X\n", w.status, msg[1:]) 194 | return w.write(m[1:]) 195 | } 196 | -------------------------------------------------------------------------------- /v2/internal/utils/midilib_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestKeyFromSharpsOrFlats(t *testing.T) { 11 | var tests = []struct { 12 | sharpsOrFlats int8 13 | mode uint8 14 | key byte 15 | }{ 16 | {-4, 1, 5}, /* 4 flats minor */ 17 | {-4, 0, 8}, /* 4 flats major */ 18 | {3, 1, 6}, /* 3 sharps minor */ 19 | {3, 0, 9}, /* 3 sharps major */ 20 | } 21 | 22 | for _, test := range tests { 23 | key := KeyFromSharpsOrFlats(test.sharpsOrFlats, test.mode) 24 | 25 | if key != test.key { 26 | t.Errorf("KeyFromSharpsOrFlats(%v,%v) = %X; want %X", test.sharpsOrFlats, test.mode, key, test.key) 27 | } 28 | } 29 | } 30 | 31 | func TestParseStatus(t *testing.T) { 32 | var tests = []struct { 33 | byte byte 34 | messageType uint8 35 | messageChannel uint8 36 | }{ 37 | {0xF0, 15, 0}, /* sysex */ 38 | {0xF7, 15, 7}, /* sysex */ 39 | {0xFF, 15, 15}, /* meta */ 40 | {0xF8, 15, 8}, /* reatime MTC */ 41 | {0xC0, 12, 0}, /* prog change chan 0 */ 42 | {0x92, 9, 2}, /* note on chan2 */ 43 | {0x81, 8, 1}, /* note off chan 1 */ 44 | 45 | } 46 | 47 | for _, test := range tests { 48 | typ, ch := ParseStatus(test.byte) 49 | 50 | if typ != test.messageType || ch != test.messageChannel { 51 | t.Errorf("ParseStatus(%X) = %v,%v; want %v,%v", test.byte, typ, ch, test.messageType, test.messageChannel) 52 | } 53 | } 54 | 55 | } 56 | 57 | func TestParsePitchWheelVals(t *testing.T) { 58 | var tests = []struct { 59 | byte0 byte 60 | byte1 byte 61 | relative int16 62 | absolute uint16 63 | }{ 64 | {0x00, 0x00, -8192, 0}, 65 | {0xFF, 0xFF, 8191, 16383}, 66 | {0xFF, 0xBF, -1, 8191}, 67 | {0xF3, 0xF4, 6771, 14963}, 68 | } 69 | 70 | for _, test := range tests { 71 | rel, abs := ParsePitchWheelVals(test.byte0, test.byte1) 72 | 73 | if rel != test.relative || abs != test.absolute { 74 | t.Errorf("ParsePitchWheelVals(%X, %X) = %v,%v; want %v,%v", test.byte0, test.byte1, rel, abs, test.relative, test.absolute) 75 | } 76 | } 77 | 78 | } 79 | 80 | func TestMsbLsbUnsigned(t *testing.T) { 81 | var tests = []struct { 82 | num uint16 83 | descr string 84 | expected string 85 | }{ 86 | {0, "msbLsbUnsigned(0)", "0"}, 87 | {8192, "msbLsbUnsigned(8192)", "1000000"}, 88 | {16383, "msbLsbUnsigned(16383)", "111111101111111"}, 89 | } 90 | 91 | for _, test := range tests { 92 | var b = MsbLsbUnsigned(test.num) 93 | 94 | if got, want := fmt.Sprintf("%b", b), test.expected; got != want { 95 | t.Errorf("%s = %s; want %s", test.descr, got, want) 96 | } 97 | } 98 | 99 | } 100 | 101 | func TestVLQ(t *testing.T) { 102 | var tests = []struct { 103 | num uint32 104 | bytes []byte 105 | }{ 106 | // tests according to MIDI SMF spec 107 | {0x40, []byte{0x40}}, 108 | {0x7F, []byte{0x7F}}, 109 | {0x80, []byte{0x81, 0x00}}, 110 | {0x2000, []byte{0xC0, 0x00}}, 111 | {0x3FFF, []byte{0xFF, 0x7F}}, 112 | {0x4000, []byte{0x81, 0x80, 0x00}}, 113 | {0x100000, []byte{0xC0, 0x80, 0x00}}, 114 | {0x1FFFFF, []byte{0xFF, 0xFF, 0x7F}}, 115 | {0x200000, []byte{0x81, 0x80, 0x80, 0x00}}, 116 | {0x8000000, []byte{0xC0, 0x80, 0x80, 0x00}}, 117 | {0xFFFFFFF, []byte{0xFF, 0xFF, 0xFF, 0x7F}}, 118 | } 119 | 120 | for _, test := range tests { 121 | var b = VlqEncode(test.num) 122 | 123 | var bf bytes.Buffer 124 | binary.Write(&bf, binary.BigEndian, b) 125 | res, _ := ReadVarLength(&bf) 126 | 127 | if got, want := res, test.num; got != want { 128 | t.Errorf("ReadVarLength(%#v) = %d; want %d", test.bytes, got, want) 129 | } 130 | } 131 | 132 | } 133 | 134 | func TestLibBits(t *testing.T) { 135 | 136 | tests := []struct { 137 | result interface{} 138 | descr string 139 | expected string 140 | }{ 141 | {clearBitU16(8191, 3), "clearBitU16(8192,2)", "1111111110111"}, 142 | {clearBitU16(50, 1), "clearBitU16(50,1)", "110000"}, 143 | {ClearBitU8(50, 1), "ClearBitU8(50, 1)", "110000"}, 144 | {ClearBitU8(50, 4), "ClearBitU8(50, 4)", "100010"}, 145 | {hasBitU8(50, 4), "hasBitU8(50, 4)", "%!b(bool=true)"}, 146 | {hasBitU8(50, 3), "hasBitU8(50, 3)", "%!b(bool=false)"}, 147 | {IsStatusByte(50), "IsStatusByte(50)", "%!b(bool=false)"}, 148 | {IsStatusByte(128), "IsStatusByte(128)", "%!b(bool=true)"}, 149 | {IsChannelMessage(128), "IsChannelMessage(128)", "%!b(bool=true)"}, 150 | {IsChannelMessage(121), "IsChannelMessage(121)", "%!b(bool=false)"}, 151 | } 152 | 153 | for _, test := range tests { 154 | 155 | if got, want := fmt.Sprintf("%b", test.result), test.expected; got != want { 156 | t.Errorf("%s = %#v; want %#v", test.descr, got, want) 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /v2/internal/utils/vlq_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var tests = []struct { 9 | num uint32 10 | bytes []byte 11 | }{ 12 | // tests according to MIDI SMF spec 13 | {0x40, []byte{0x40}}, 14 | {0x7F, []byte{0x7F}}, 15 | {0x80, []byte{0x81, 0x00}}, 16 | {0x2000, []byte{0xC0, 0x00}}, 17 | {0x3FFF, []byte{0xFF, 0x7F}}, 18 | {0x4000, []byte{0x81, 0x80, 0x00}}, 19 | {0x100000, []byte{0xC0, 0x80, 0x00}}, 20 | {0x1FFFFF, []byte{0xFF, 0xFF, 0x7F}}, 21 | {0x200000, []byte{0x81, 0x80, 0x80, 0x00}}, 22 | {0x8000000, []byte{0xC0, 0x80, 0x80, 0x00}}, 23 | {0xFFFFFFF, []byte{0xFF, 0xFF, 0xFF, 0x7F}}, 24 | } 25 | 26 | func TestVlqEncode(t *testing.T) { 27 | for _, test := range tests { 28 | var b = VlqEncode(test.num) 29 | 30 | if got, want := fmt.Sprintf("%X", b), fmt.Sprintf("%X", test.bytes); got != want { 31 | t.Errorf("Encode(%#v) = %#v; want %#v", test.num, got, want) 32 | } 33 | } 34 | 35 | } 36 | 37 | func TestVlqDecode(t *testing.T) { 38 | for _, test := range tests { 39 | var b = VlqDecode(test.bytes) 40 | 41 | if got, want := b, test.num; got != want { 42 | t.Errorf("Decode(%#v) = %d; want %d", test.bytes, got, want) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /v2/io.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "gitlab.com/gomidi/midi/v2/drivers" 9 | ) 10 | 11 | // CloseDriver closes the default driver. 12 | func CloseDriver() { 13 | drivers.Close() 14 | } 15 | 16 | // SendTo returns a function that can be used to send messages to the given midi port. 17 | func SendTo(outPort drivers.Out) (func(msg Message) error, error) { 18 | if !outPort.IsOpen() { 19 | err := outPort.Open() 20 | if err != nil { 21 | return nil, err 22 | } 23 | } 24 | return func(msg Message) error { 25 | return outPort.Send(msg.Bytes()) 26 | }, nil 27 | } 28 | 29 | type InPorts []drivers.In 30 | 31 | func (ip InPorts) String() string { 32 | var bf strings.Builder 33 | 34 | for i, p := range ip { 35 | bf.WriteString(fmt.Sprintf("[%v] %s\n", i, p)) 36 | } 37 | 38 | return bf.String() 39 | } 40 | 41 | type OutPorts []drivers.Out 42 | 43 | func (op OutPorts) String() string { 44 | var bf strings.Builder 45 | 46 | for i, p := range op { 47 | bf.WriteString(fmt.Sprintf("[%v] %s\n", i, p)) 48 | } 49 | 50 | return bf.String() 51 | } 52 | 53 | // GetInPorts returns the MIDI input ports 54 | func GetInPorts() InPorts { 55 | ins, err := drivers.Ins() 56 | 57 | if err != nil { 58 | fmt.Fprintf(os.Stderr, "can't get midi in ports: %s\n", err.Error()) 59 | return nil 60 | } 61 | 62 | return ins 63 | } 64 | 65 | // GetOutPorts returns the MIDI output ports 66 | func GetOutPorts() OutPorts { 67 | outs, err := drivers.Outs() 68 | 69 | if err != nil { 70 | fmt.Fprintf(os.Stderr, "can't get midi out ports: %s\n", err.Error()) 71 | return nil 72 | } 73 | 74 | return outs 75 | } 76 | -------------------------------------------------------------------------------- /v2/listen.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "gitlab.com/gomidi/midi/v2/drivers" 5 | midilib "gitlab.com/gomidi/midi/v2/internal/utils" 6 | ) 7 | 8 | func _channelMessage(typ, channel, data1, data2 byte) Message { 9 | switch typ { 10 | case byteChannelPressure: 11 | return AfterTouch(channel, data1) 12 | case byteProgramChange: 13 | return ProgramChange(channel, data1) 14 | case byteControlChange: 15 | return ControlChange(channel, data1, data2) 16 | case byteNoteOn: 17 | return NoteOn(channel, data1, data2) 18 | case byteNoteOff: 19 | return NoteOffVelocity(channel, data1, data2) 20 | case bytePolyphonicKeyPressure: 21 | return PolyAfterTouch(channel, data1, data2) 22 | case bytePitchWheel: 23 | rel, _ := midilib.ParsePitchWheelVals(data1, data2) 24 | return Pitchbend(channel, rel) 25 | default: 26 | panic("unknown typ") 27 | } 28 | } 29 | 30 | // listeningOptions are the options for the listening 31 | type listeningOptions struct { 32 | 33 | // TimeCode lets the timecode messages pass through, if set 34 | TimeCode bool 35 | 36 | // ActiveSense lets the active sense messages pass through, if set 37 | ActiveSense bool 38 | 39 | // SysEx lets the system exclusive messages pass through, if set 40 | SysEx bool 41 | 42 | // SysExBufferSize defines the size of the buffer for sysex messages (in bytes). 43 | // SysEx messages larger than this size will be ignored. 44 | // When SysExBufferSize is 0, the default buffersize (1024) is used. 45 | SysExBufferSize uint32 46 | 47 | // OnError handles occuring errors 48 | OnError func(error) 49 | } 50 | 51 | // Option is an option for listening 52 | type Option func(*listeningOptions) 53 | 54 | // UseTimeCode is an option to receive time code messages 55 | func UseTimeCode() Option { 56 | return func(l *listeningOptions) { 57 | l.TimeCode = true 58 | } 59 | } 60 | 61 | // UseActiveSense is an option to receive active sense messages 62 | func UseActiveSense() Option { 63 | return func(l *listeningOptions) { 64 | l.ActiveSense = true 65 | } 66 | } 67 | 68 | // UseSysEx is an option to receive system exclusive messages 69 | func UseSysEx() Option { 70 | return func(l *listeningOptions) { 71 | l.SysEx = true 72 | } 73 | } 74 | 75 | // SysExBufferSize is an option to set the buffer size for sysex messages 76 | func SysExBufferSize(size uint32) Option { 77 | return func(l *listeningOptions) { 78 | l.SysExBufferSize = size 79 | } 80 | } 81 | 82 | // HandleError sets an error handler when receiving messages 83 | func HandleError(cb func(error)) Option { 84 | return func(l *listeningOptions) { 85 | l.OnError = cb 86 | } 87 | } 88 | 89 | var ErrPortClosed = drivers.ErrPortClosed 90 | var ErrListenStopped = drivers.ErrListenStopped 91 | 92 | // ListenTo listens on the given port and passes the received MIDI data to the given receiver. 93 | // It returns a stop function that may be called to stop the listening. 94 | func ListenTo(inPort drivers.In, recv func(msg Message, timestampms int32), opts ...Option) (stop func(), err error) { 95 | if !inPort.IsOpen() { 96 | err = inPort.Open() 97 | 98 | if err != nil { 99 | return nil, err 100 | } 101 | } 102 | 103 | var opt listeningOptions 104 | for _, o := range opts { 105 | o(&opt) 106 | } 107 | 108 | var conf drivers.ListenConfig 109 | conf.SysExBufferSize = opt.SysExBufferSize 110 | conf.TimeCode = opt.TimeCode 111 | conf.ActiveSense = opt.ActiveSense 112 | conf.SysEx = opt.SysEx 113 | conf.OnErr = opt.OnError 114 | 115 | var isStatusSet bool 116 | var typ, channel byte 117 | 118 | var onMsg = func(data []byte, millisec int32) { 119 | status := data[0] 120 | 121 | var msg Message 122 | switch { 123 | 124 | // realtime message 125 | case status >= 0xF8: 126 | msg = []byte{status} 127 | 128 | // here we clear for System Common Category messages 129 | case status > 0xF0 && status < 0xF7: 130 | isStatusSet = false 131 | switch status { 132 | case byteSysTuneRequest: 133 | msg = Tune() 134 | case byteMIDITimingCodeMessage: 135 | msg = MTC(data[1]) 136 | case byteSysSongPositionPointer: 137 | _, abs := midilib.ParsePitchWheelVals(data[1], data[2]) 138 | msg = SPP(abs) 139 | case byteSysSongSelect: 140 | msg = SongSelect(data[1]) 141 | default: 142 | // undefined syscommon message 143 | // msg = NewUndefined() 144 | } 145 | 146 | // [MIDI] permits 0xF7 octets that are not part of a (0xF0, 0xF7) pair 147 | // to appear on a MIDI 1.0 DIN cable. Unpaired 0xF7 octets have no 148 | // semantic meaning in MIDI apart from cancelling running status. 149 | case status == 0xF7: 150 | isStatusSet = false 151 | 152 | // sysex message 153 | case status == 0xF0: 154 | isStatusSet = false 155 | msg = Message(data) 156 | 157 | // channel message 158 | case status >= 0x80 && status <= 0xEF: 159 | isStatusSet = true 160 | typ, channel = midilib.ParseStatus(status) 161 | msg = _channelMessage(typ, channel, data[1], data[2]) 162 | 163 | default: 164 | // running status 165 | if isStatusSet { 166 | msg = _channelMessage(typ, channel, data[1], data[2]) 167 | } 168 | } 169 | 170 | recv(msg, millisec) 171 | } 172 | 173 | return inPort.Listen(onMsg, conf) 174 | } 175 | -------------------------------------------------------------------------------- /v2/mmc/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 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 mmc helps with reading and writing of MIDI Universal Real Time SysEx Commands. 7 | */ 8 | package mmc 9 | -------------------------------------------------------------------------------- /v2/note.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | /* 9 | in order to be able to easy deal with tones and notes, 10 | all notes are defined on the basic octave which happens to be 11 | c1 (=60), so that it is easy to go below and above 12 | */ 13 | 14 | /* 15 | const ( 16 | C Note = 60 17 | Db Note = 61 18 | D Note = 62 19 | Eb Note = 63 20 | E Note = 64 21 | F Note = 65 22 | Gb Note = 66 23 | G Note = 67 24 | Ab Note = 68 25 | A Note = 69 26 | Bb Note = 70 27 | B Note = 71 28 | ) 29 | */ 30 | 31 | func o(base uint8, oct uint8) uint8 { 32 | if oct > 10 { 33 | oct = 10 34 | } 35 | 36 | if oct == 0 { 37 | return base 38 | } 39 | 40 | res := base + uint8(12*oct) 41 | if res > 127 { 42 | res -= 12 43 | } 44 | 45 | return res 46 | } 47 | 48 | // C returns the key for the MIDI note C in the given octave 49 | func C(oct uint8) uint8 { 50 | return o(0, oct) 51 | } 52 | 53 | // Db returns the key for the MIDI note Db in the given octave 54 | func Db(oct uint8) uint8 { 55 | return o(1, oct) 56 | } 57 | 58 | // D returns the key for the MIDI note D in the given octave 59 | func D(oct uint8) uint8 { 60 | return o(2, oct) 61 | } 62 | 63 | // Eb returns the key for the MIDI note Eb in the given octave 64 | func Eb(oct uint8) uint8 { 65 | return o(3, oct) 66 | } 67 | 68 | // E returns the key for the MIDI note E in the given octave 69 | func E(oct uint8) uint8 { 70 | return o(4, oct) 71 | } 72 | 73 | // F returns the key for the MIDI note F in the given octave 74 | func F(oct uint8) uint8 { 75 | return o(5, oct) 76 | } 77 | 78 | // Gb returns the key for the MIDI note Gb in the given octave 79 | func Gb(oct uint8) uint8 { 80 | return o(6, oct) 81 | } 82 | 83 | // G returns the key for the MIDI note G in the given octave 84 | func G(oct uint8) uint8 { 85 | return o(7, oct) 86 | } 87 | 88 | // Ab returns the key for the MIDI note Ab in the given octave 89 | func Ab(oct uint8) uint8 { 90 | return o(8, oct) 91 | } 92 | 93 | // A returns the key for the MIDI note A in the given octave 94 | func A(oct uint8) uint8 { 95 | return o(9, oct) 96 | } 97 | 98 | // Bb returns the key for the MIDI note Bb in the given octave 99 | func Bb(oct uint8) uint8 { 100 | return o(10, oct) 101 | } 102 | 103 | // B returns the key for the MIDI note B in the given octave 104 | func B(oct uint8) uint8 { 105 | return o(11, oct) 106 | } 107 | 108 | type Interval int8 109 | 110 | const ( 111 | Unison Interval = 0 112 | MinorSecond Interval = 1 113 | MajorSecond Interval = 2 114 | MinorThird Interval = 3 115 | MajorThird Interval = 4 116 | Fourth Interval = 5 117 | Tritone Interval = 6 118 | Fifth Interval = 7 119 | MinorSixth Interval = 8 120 | MajorSixth Interval = 9 121 | MinorSeventh Interval = 10 122 | MajorSeventh Interval = 11 123 | Octave Interval = 12 124 | MinorNinth Interval = 13 125 | MajorNinth Interval = 14 126 | MinorTenth Interval = 15 127 | MajorTenth Interval = 16 128 | Eleventh Interval = 17 129 | DiminishedTwelfth Interval = 18 130 | Twelfth Interval = 19 131 | MinorThirteenth Interval = 20 132 | MajorThirteenth Interval = 21 133 | MinorFourteenth Interval = 22 134 | MajorFourteenth Interval = 23 135 | DoubleOctave Interval = 24 136 | ) 137 | 138 | var intervalNames = map[Interval]string{ 139 | 0: "Unison", 140 | 1: "MinorSecond", 141 | 2: "MajorSecond", 142 | 3: "MinorThird", 143 | 4: "MajorThird", 144 | 5: "Fourth", 145 | 6: "Tritone", 146 | 7: "Fifth", 147 | 8: "MinorSixth", 148 | 9: "MajorSixth", 149 | 10: "MinorSeventh", 150 | 11: "MajorSeventh", 151 | 12: "Octave", 152 | 13: "MinorNinth", 153 | 14: "MajorNinth", 154 | 15: "MinorTenth", 155 | 16: "MajorTenth", 156 | 17: "Eleventh", 157 | 18: "DiminishedTwelfth", 158 | 19: "Twelfth", 159 | 20: "MinorThirteenth", 160 | 21: "MajorThirteenth", 161 | 22: "MinorFourteenth", 162 | 23: "MajorFourteenth", 163 | 24: "DoubleOctave", 164 | } 165 | 166 | func (i Interval) String() string { 167 | var down bool 168 | if i < 0 { 169 | down = true 170 | i = (-1) * i 171 | } 172 | 173 | i = i % 24 174 | 175 | var bd strings.Builder 176 | bd.WriteString(intervalNames[i]) 177 | 178 | if down { 179 | bd.WriteString(" down") 180 | } else { 181 | bd.WriteString(" up") 182 | } 183 | 184 | return bd.String() 185 | } 186 | 187 | type Note uint8 188 | 189 | func (n Note) Interval(o Note) Interval { 190 | return Interval(int8(o) - int8(n)) 191 | } 192 | 193 | func (n Note) Transpose(i Interval) Note { 194 | res := int8(n) + int8(i) 195 | if res < 0 { 196 | res = 0 197 | } 198 | return Note(res) 199 | } 200 | 201 | func (n Note) Value() uint8 { 202 | return uint8(n) 203 | } 204 | 205 | func (n Note) Base() uint8 { 206 | return uint8(n) % 12 207 | } 208 | 209 | func (n Note) Name() (name string) { 210 | switch n % 12 { 211 | case 0: 212 | name = "C" 213 | case 1: 214 | name = "Db" 215 | case 2: 216 | name = "D" 217 | case 3: 218 | name = "Eb" 219 | case 4: 220 | name = "E" 221 | case 5: 222 | name = "F" 223 | case 6: 224 | name = "Gb" 225 | case 7: 226 | name = "G" 227 | case 8: 228 | name = "Ab" 229 | case 9: 230 | name = "A" 231 | case 10: 232 | name = "Bb" 233 | case 11: 234 | name = "B" 235 | default: 236 | panic("unreachable") 237 | } 238 | 239 | return name 240 | } 241 | 242 | func (n Note) String() string { 243 | name := n.Name() 244 | if name != "" { 245 | name += fmt.Sprintf("%v", n.Octave()) 246 | } 247 | return name 248 | } 249 | 250 | func (n Note) Octave() uint8 { 251 | return uint8(n / 12) 252 | } 253 | 254 | // Equal returns true if noteX is the same as noteY 255 | // they may be in different octaves. 256 | func (n Note) Is(o Note) bool { 257 | return n%12 == o%12 258 | } 259 | -------------------------------------------------------------------------------- /v2/pitchbend_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPitchbend(t *testing.T) { 8 | 9 | tests := []struct { 10 | in int16 11 | expected uint16 12 | }{ 13 | { 14 | in: 0, 15 | expected: 8192, 16 | }, 17 | { 18 | in: PitchHighest, 19 | expected: 16383, 20 | }, 21 | { 22 | in: PitchHighest + 1, 23 | expected: 16383, 24 | }, 25 | { 26 | in: PitchLowest, 27 | expected: 0, 28 | }, 29 | { 30 | in: PitchLowest - 1, 31 | expected: 0, 32 | }, 33 | } 34 | 35 | for _, test := range tests { 36 | m := Pitchbend(0, test.in) 37 | 38 | var got uint16 39 | var ch uint8 40 | Message(m).GetPitchBend(&ch, nil, &got) 41 | 42 | if got != test.expected { 43 | t.Errorf("Pitchbend(%v).absValue = %v; wanted %v", test.in, got, test.expected) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /v2/port.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "gitlab.com/gomidi/midi/v2/drivers" 5 | ) 6 | 7 | // FindInPort returns the midi in port that contains the given name 8 | // and an error, if the port can't be found. 9 | func FindInPort(name string) (drivers.In, error) { 10 | in, err := drivers.InByName(name) 11 | if err != nil { 12 | return nil, err 13 | } 14 | in.Close() 15 | return in, nil 16 | } 17 | 18 | // OutPort returns the midi out port for the given port number 19 | func OutPort(portnumber int) (drivers.Out, error) { 20 | return drivers.OutByNumber(portnumber) 21 | } 22 | 23 | // InPort returns the midi in port for the given port number 24 | func InPort(portnumber int) (drivers.In, error) { 25 | return drivers.InByNumber(portnumber) 26 | } 27 | 28 | // FindOutPort returns the midi out port that contains the given name 29 | // and an error, if the port can't be found. 30 | func FindOutPort(name string) (drivers.Out, error) { 31 | out, err := drivers.OutByName(name) 32 | if err != nil { 33 | return nil, err 34 | } 35 | out.Close() 36 | return out, nil 37 | } 38 | -------------------------------------------------------------------------------- /v2/realtime.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | const ( 4 | byteTimingClock = 0xF8 5 | byteTick = 0xF9 6 | byteStart = 0xFA 7 | byteContinue = 0xFB 8 | byteStop = 0xFC 9 | byteUndefined4 = 0xFD 10 | byteActivesense = 0xFE 11 | byteReset = 0xFF 12 | ) 13 | 14 | var rtMessages = map[byte]Type{ 15 | byteTimingClock:/* RealTimeMsg.Set(TimingClockMsg), */ TimingClockMsg, 16 | byteTick:/* RealTimeMsg.Set(TickMsg), */ TickMsg, 17 | byteStart:/* RealTimeMsg.Set(StartMsg), */ StartMsg, 18 | byteContinue:/* RealTimeMsg.Set(ContinueMsg), */ ContinueMsg, 19 | byteStop:/* RealTimeMsg.Set(StopMsg), */ StopMsg, 20 | byteUndefined4:/* RealTimeMsg.Set(UndefinedMsg), */ UnknownMsg, 21 | byteActivesense:/* RealTimeMsg.Set(ActiveSenseMsg), */ ActiveSenseMsg, 22 | byteReset:/* RealTimeMsg.Set(ResetMsg), */ ResetMsg, 23 | } 24 | 25 | // TimingClock returns a timing clock message 26 | func TimingClock() Message { 27 | return []byte{byteTimingClock} 28 | } 29 | 30 | // Tick returns a tick message 31 | func Tick() Message { 32 | return []byte{byteTick} 33 | } 34 | 35 | // Start returns a start message 36 | func Start() Message { 37 | return []byte{byteStart} 38 | } 39 | 40 | // Continue returns a continue message 41 | func Continue() Message { 42 | return []byte{byteContinue} 43 | } 44 | 45 | // Stop returns a stop message 46 | func Stop() Message { 47 | return []byte{byteStop} 48 | } 49 | 50 | /* 51 | // NewUndefined returns an undefined realtime message 52 | func NewUndefined() Message { 53 | return NewMsg([]byte{byteUndefined4}) 54 | } 55 | */ 56 | 57 | // Activesense returns an active sensing message 58 | func Activesense() Message { 59 | return []byte{byteActivesense} 60 | } 61 | 62 | // Reset returns a reset message 63 | func Reset() Message { 64 | return []byte{byteReset} 65 | } 66 | -------------------------------------------------------------------------------- /v2/realtime_test.go: -------------------------------------------------------------------------------- 1 | package midi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "gitlab.com/gomidi/midi/v2" 7 | ) 8 | 9 | func TestRealTime(t *testing.T) { 10 | 11 | tests := []struct { 12 | msg []byte 13 | expected string 14 | }{ 15 | { 16 | midi.TimingClock(), 17 | "TimingClock", 18 | }, 19 | { 20 | midi.Tick(), 21 | "Tick", 22 | }, 23 | { 24 | midi.Start(), 25 | "Start", 26 | }, 27 | { 28 | midi.Continue(), 29 | "Continue", 30 | }, 31 | { 32 | midi.Stop(), 33 | "Stop", 34 | }, 35 | /* 36 | { 37 | midi.NewUndefined(), 38 | "UnknownType", 39 | }, 40 | */ 41 | { 42 | midi.Activesense(), 43 | "ActiveSense", 44 | }, 45 | { 46 | midi.Reset(), 47 | "Reset", 48 | }, 49 | } 50 | 51 | for n, test := range tests { 52 | m := midi.Message(test.msg) 53 | 54 | if got, want := m.String(), test.expected; got != want { 55 | t.Errorf("[%v] (% X).String() = %#v; want %#v", n, test.msg, got, want) 56 | } 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /v2/rpn_nrpn/handler_test.go: -------------------------------------------------------------------------------- 1 | package rpn_nrpn 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "gitlab.com/gomidi/midi/v2" 10 | ) 11 | 12 | type testHandler struct { 13 | handler Handler 14 | bf bytes.Buffer 15 | } 16 | 17 | func (t *testHandler) String() string { 18 | return t.bf.String() 19 | } 20 | 21 | func (t *testHandler) handleCC(channel, cc, val uint8) { 22 | fmt.Fprintf(&t.bf, "CC: ch %v cc: %v val: %v\n", channel, cc, val) 23 | } 24 | 25 | func (t *testHandler) reset() { 26 | t.bf.Reset() 27 | t.handler.NRPN.MSB = func(channel, typ1, typ2, msbval uint8) (handled bool) { 28 | fmt.Fprintf(&t.bf, "NRPN.MSB: ch %v t1: %v t2: %v val: %v\n", channel, typ1, typ2, msbval) 29 | return true 30 | } 31 | 32 | t.handler.NRPN.LSB = func(channel, typ1, typ2, lsbval uint8) (handled bool) { 33 | fmt.Fprintf(&t.bf, "NRPN.LSB: ch %v t1: %v t2: %v val: %v\n", channel, typ1, typ2, lsbval) 34 | return true 35 | } 36 | t.handler.NRPN.Increment = func(channel, typ1, typ2 uint8) (handled bool) { 37 | fmt.Fprintf(&t.bf, "NRPN.Increment: ch %v t1: %v t2: %v\n", channel, typ1, typ2) 38 | return true 39 | } 40 | t.handler.NRPN.Decrement = func(channel, typ1, typ2 uint8) (handled bool) { 41 | fmt.Fprintf(&t.bf, "NRPN.Decrement: ch %v t1: %v t2: %v\n", channel, typ1, typ2) 42 | return true 43 | } 44 | t.handler.NRPN.Reset = func(channel uint8) (handled bool) { 45 | fmt.Fprintf(&t.bf, "NRPN.Reset: ch %v\n", channel) 46 | return true 47 | } 48 | 49 | t.handler.RPN.MSB = func(channel, typ1, typ2, msbval uint8) (handled bool) { 50 | fmt.Fprintf(&t.bf, "RPN.MSB: ch %v t1: %v t2: %v val: %v\n", channel, typ1, typ2, msbval) 51 | return true 52 | } 53 | t.handler.RPN.LSB = func(channel, typ1, typ2, lsbval uint8) (handled bool) { 54 | fmt.Fprintf(&t.bf, "RPN.LSB: ch %v t1: %v t2: %v val: %v\n", channel, typ1, typ2, lsbval) 55 | return true 56 | } 57 | t.handler.RPN.Increment = func(channel, typ1, typ2 uint8) (handled bool) { 58 | fmt.Fprintf(&t.bf, "RPN.Increment: ch %v t1: %v t2: %v\n", channel, typ1, typ2) 59 | return true 60 | } 61 | t.handler.RPN.Decrement = func(channel, typ1, typ2 uint8) (handled bool) { 62 | fmt.Fprintf(&t.bf, "RPN.Decrement: ch %v t1: %v t2: %v\n", channel, typ1, typ2) 63 | return true 64 | } 65 | t.handler.RPN.Reset = func(channel uint8) (handled bool) { 66 | fmt.Fprintf(&t.bf, "RPN.Reset: ch %v\n", channel) 67 | return true 68 | } 69 | 70 | } 71 | 72 | func TestValidHandlers(t *testing.T) { 73 | var th testHandler 74 | 75 | tests := []struct { 76 | messages []midi.Message 77 | // expectedHandled bool 78 | expected string 79 | }{ 80 | {RPNReset(4), ` 81 | CC: ch 4 cc: 101 val: 127 82 | RPN.Reset: ch 4 83 | `}, 84 | {NRPNReset(3), ` 85 | CC: ch 3 cc: 99 val: 127 86 | NRPN.Reset: ch 3 87 | `}, 88 | {RPNIncrement(12, 44, 75), ` 89 | CC: ch 12 cc: 101 val: 44 90 | CC: ch 12 cc: 100 val: 75 91 | RPN.Increment: ch 12 t1: 44 t2: 75 92 | `}, 93 | {NRPNIncrement(2, 34, 45), ` 94 | CC: ch 2 cc: 99 val: 34 95 | CC: ch 2 cc: 98 val: 45 96 | NRPN.Increment: ch 2 t1: 34 t2: 45 97 | `}, 98 | {RPNDecrement(12, 44, 75), ` 99 | CC: ch 12 cc: 101 val: 44 100 | CC: ch 12 cc: 100 val: 75 101 | RPN.Decrement: ch 12 t1: 44 t2: 75 102 | `}, 103 | {NRPNDecrement(2, 34, 45), ` 104 | CC: ch 2 cc: 99 val: 34 105 | CC: ch 2 cc: 98 val: 45 106 | NRPN.Decrement: ch 2 t1: 34 t2: 45 107 | `}, 108 | 109 | {RPN(10, 80, 70, 60, 50), ` 110 | CC: ch 10 cc: 101 val: 80 111 | CC: ch 10 cc: 100 val: 70 112 | RPN.MSB: ch 10 t1: 80 t2: 70 val: 60 113 | RPN.LSB: ch 10 t1: 80 t2: 70 val: 50 114 | `}, 115 | {append(RPN(10, 80, 70, 60, 50), midi.ControlChange(2, 12, 123)), 116 | ` 117 | CC: ch 10 cc: 101 val: 80 118 | CC: ch 10 cc: 100 val: 70 119 | RPN.MSB: ch 10 t1: 80 t2: 70 val: 60 120 | RPN.LSB: ch 10 t1: 80 t2: 70 val: 50 121 | CC: ch 2 cc: 12 val: 123 122 | `}, 123 | 124 | {NRPN(11, 80, 70, 60, 50), ` 125 | CC: ch 11 cc: 99 val: 80 126 | CC: ch 11 cc: 98 val: 70 127 | NRPN.MSB: ch 11 t1: 80 t2: 70 val: 60 128 | NRPN.LSB: ch 11 t1: 80 t2: 70 val: 50 129 | `}, 130 | {append(NRPN(11, 80, 70, 60, 50), midi.ControlChange(2, 12, 123)), 131 | ` 132 | CC: ch 11 cc: 99 val: 80 133 | CC: ch 11 cc: 98 val: 70 134 | NRPN.MSB: ch 11 t1: 80 t2: 70 val: 60 135 | NRPN.LSB: ch 11 t1: 80 t2: 70 val: 50 136 | CC: ch 2 cc: 12 val: 123 137 | `}, 138 | } 139 | 140 | for i, test := range tests { 141 | th.reset() 142 | 143 | for _, msg := range test.messages { 144 | var ch, cc, val uint8 145 | if msg.GetControlChange(&ch, &cc, &val) { 146 | used := th.handler.ReadCCMessage(ch, cc, val) 147 | if !used { 148 | th.handleCC(ch, cc, val) 149 | } 150 | } 151 | } 152 | 153 | got := strings.TrimSpace(th.String()) 154 | expected := strings.TrimSpace(test.expected) 155 | 156 | if got != expected { 157 | t.Errorf("[%v]\ngot:\n%s\nexpected:\n%s", i, got, expected) 158 | } 159 | 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /v2/rpn_nrpn/helper.go: -------------------------------------------------------------------------------- 1 | package rpn_nrpn 2 | 3 | import "gitlab.com/gomidi/midi/v2" 4 | 5 | func cc(channel, ctl, val uint8) midi.Message { 6 | return midi.ControlChange(channel, ctl, val) 7 | } 8 | 9 | var ( 10 | CC_RPN0 = uint8(101) 11 | CC_RPN1 = uint8(100) 12 | 13 | CC_NRPN0 = uint8(99) 14 | CC_NRPN1 = uint8(98) 15 | 16 | CC_INC = uint8(96) 17 | CC_DEC = uint8(97) 18 | 19 | CC_MSB = uint8(6) 20 | CC_LSB = uint8(38) 21 | 22 | VAL_SET = uint8(127) 23 | VAL_UNSET = uint8(0) 24 | ) 25 | 26 | func IsRPN_NRPN_CC(cc uint8) bool { 27 | switch cc { 28 | case CC_RPN0, CC_RPN1, CC_NRPN0, CC_NRPN1, CC_INC, CC_DEC, CC_MSB, CC_LSB: 29 | return true 30 | default: 31 | return false 32 | } 33 | } 34 | 35 | // PitchBendSensitivity sets the pitch bend range via RPN 36 | func PitchBendSensitivity(channel, msbVal, lsbVal uint8) []midi.Message { 37 | return RPN(channel, 0, 0, msbVal, lsbVal) 38 | } 39 | 40 | // FineTuning 41 | func FineTuning(channel, msbVal, lsbVal uint8) []midi.Message { 42 | return RPN(channel, 0, 1, msbVal, lsbVal) 43 | } 44 | 45 | // CoarseTuning 46 | func CoarseTuning(channel, msbVal, lsbVal uint8) []midi.Message { 47 | return RPN(channel, 0, 2, msbVal, lsbVal) 48 | } 49 | 50 | // TuningProgramSelect 51 | func TuningProgramSelect(channel, msbVal, lsbVal uint8) []midi.Message { 52 | return RPN(channel, 0, 3, msbVal, lsbVal) 53 | } 54 | 55 | // TuningBankSelect 56 | func TuningBankSelect(channel, msbVal, lsbVal uint8) []midi.Message { 57 | return RPN(channel, 0, 4, msbVal, lsbVal) 58 | } 59 | -------------------------------------------------------------------------------- /v2/rpn_nrpn/nrpn.go: -------------------------------------------------------------------------------- 1 | package rpn_nrpn 2 | 3 | import ( 4 | "gitlab.com/gomidi/midi/v2" 5 | ) 6 | 7 | // Reset aka Null 8 | func NRPNReset(channel uint8) []midi.Message { 9 | return append([]midi.Message{}, 10 | cc(channel, CC_NRPN0, VAL_SET), 11 | cc(channel, CC_NRPN1, VAL_SET), 12 | ) 13 | } 14 | 15 | func NRPNIncrement(channel uint8, val99, val98 uint8) []midi.Message { 16 | return []midi.Message{ 17 | cc(channel, CC_NRPN0, val99), 18 | cc(channel, CC_NRPN1, val98), 19 | cc(channel, CC_INC, CC_INC), 20 | } 21 | } 22 | 23 | func NRPNDecrement(channel uint8, val99, val98 uint8) []midi.Message { 24 | return []midi.Message{ 25 | cc(channel, CC_NRPN0, val99), 26 | cc(channel, CC_NRPN1, val98), 27 | cc(channel, CC_DEC, VAL_UNSET), 28 | } 29 | } 30 | 31 | // NRPN message consisting of a val99 and val98 to identify the NRPN and a msb and lsb for the value 32 | func NRPN(channel uint8, val99, val98, msbVal, lsbVal uint8) []midi.Message { 33 | return []midi.Message{ 34 | cc(channel, CC_NRPN0, val99), 35 | cc(channel, CC_NRPN1, val98), 36 | cc(channel, CC_MSB, msbVal), 37 | cc(channel, CC_LSB, lsbVal), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /v2/rpn_nrpn/rpn.go: -------------------------------------------------------------------------------- 1 | package rpn_nrpn 2 | 3 | import "gitlab.com/gomidi/midi/v2" 4 | 5 | /* 6 | CC101 00 Selects RPN function. 7 | CC100 00 Selects pitch bend as the parameter you want to adjust. 8 | CC06 XX Sensitivity in half steps. The range is 0-24. 9 | */ 10 | 11 | // Reset aka Null 12 | func RPNReset(channel uint8) []midi.Message { 13 | return append([]midi.Message{}, 14 | cc(channel, CC_RPN0, VAL_SET), 15 | cc(channel, CC_RPN1, VAL_SET), 16 | ) 17 | } 18 | 19 | // RPN message consisting of a val101 and val100 to identify the RPN and a msb and lsb for the value 20 | func RPN(channel, val101, val100, msbVal, lsbVal uint8) []midi.Message { 21 | return []midi.Message{ 22 | cc(channel, CC_RPN0, val101), 23 | cc(channel, CC_RPN1, val100), 24 | cc(channel, CC_MSB, msbVal), 25 | cc(channel, CC_LSB, lsbVal), 26 | } 27 | } 28 | 29 | func RPNIncrement(channel, val101, val100 uint8) []midi.Message { 30 | return []midi.Message{ 31 | cc(channel, CC_RPN0, val101), 32 | cc(channel, CC_RPN1, val100), 33 | cc(channel, CC_INC, VAL_UNSET), 34 | } 35 | } 36 | 37 | func RPNDecrement(channel, val101, val100 uint8) []midi.Message { 38 | return []midi.Message{ 39 | cc(channel, CC_RPN0, val101), 40 | cc(channel, CC_RPN1, val100), 41 | cc(channel, CC_DEC, VAL_UNSET), 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /v2/smf/chunk.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | 9 | "gitlab.com/gomidi/midi/v2/internal/utils" 10 | ) 11 | 12 | // chunk is a chunk of a SMF file. 13 | type chunk struct { 14 | typ []byte // must always be 4 bytes long, to avoid conversions everytime, we take []byte here instead of [4]byte 15 | data []byte 16 | } 17 | 18 | // Len returns the length of the chunk body. 19 | func (c *chunk) Len() int { 20 | return len(c.data) 21 | } 22 | 23 | // SetType sets the type of the chunk. 24 | func (c *chunk) SetType(typ [4]byte) { 25 | c.typ = make([]byte, 4) 26 | c.typ[0] = typ[0] 27 | c.typ[1] = typ[1] 28 | c.typ[2] = typ[2] 29 | c.typ[3] = typ[3] 30 | } 31 | 32 | // Type returns the type of the chunk (from the header). 33 | func (c *chunk) Type() string { 34 | var bf bytes.Buffer 35 | bf.Write(c.typ) 36 | return bf.String() 37 | } 38 | 39 | // Clear removes all data but keeps the type. 40 | func (c *chunk) Clear() { 41 | c.data = nil 42 | } 43 | 44 | // WriteTo writes the content of the chunk to the given writer. 45 | func (c *chunk) WriteTo(wr io.Writer) (int64, error) { 46 | if len(c.typ) != 4 { 47 | return 0, fmt.Errorf("chunk header not set properly") 48 | } 49 | 50 | var bf bytes.Buffer 51 | bf.Write(c.typ) 52 | binary.Write(&bf, binary.BigEndian, int32(c.Len())) 53 | bf.Write(c.data) 54 | n, err := wr.Write(bf.Bytes()) 55 | if err != nil { 56 | return int64(n), fmt.Errorf("could not write chunk: %v", err) 57 | } 58 | return int64(n), nil 59 | } 60 | 61 | // ReadHeader reads the header from the given reader 62 | // and returns the length of the following body. 63 | // For errors, length of 0 is returned. 64 | func (c *chunk) ReadHeader(rd io.Reader) (length uint32, err error) { 65 | c.typ, err = utils.ReadNBytes(4, rd) 66 | 67 | if err != nil { 68 | c.typ = nil 69 | return 70 | } 71 | 72 | return utils.ReadUint32(rd) 73 | } 74 | 75 | // Write writes the given bytes to the body of the chunk. 76 | func (c *chunk) Write(b []byte) (int, error) { 77 | c.data = append(c.data, b...) 78 | return len(b), nil 79 | } 80 | -------------------------------------------------------------------------------- /v2/smf/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 smf helps with reading and writing of Standard MIDI Files. 7 | 8 | The most common time resolution for SMF files are metric ticks. They define, how many ticks a quarter note is 9 | divided into. 10 | 11 | A SMF has of one or more tracks. A track is a slice of events and each event has a delta in ticks to the previous event 12 | and a message. 13 | 14 | The smf package has its own Message type that forms the events and tracks. However it is fully transparent to the midi.Message type 15 | and both types are used in tandem when adding messages to a track with the Add method. 16 | 17 | A created track must be closed with its Close method. The track can then be added to the SMF, which then can be written 18 | to an io.Writer or a file. 19 | 20 | When reading, the tracks contain the resulting messages. The methods of the Message type can then be used to get the 21 | different informations from the message. 22 | 23 | There are also helper functions for playing and recording. 24 | 25 | The TracksReader provides handy shortcuts for reading multiple tracks and also converts the time, 26 | based on the tick resolution and the tempo changes. 27 | 28 | The SMF type is also a JSON Marshaler/Unmarshaler. So it is possible to convert a SMF midi file into a human readable text file back and forth. 29 | */ 30 | package smf 31 | -------------------------------------------------------------------------------- /v2/smf/errors.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | import "errors" 4 | 5 | var errUnexpectedEOF = errors.New("Unexpected End of File found.") 6 | var ( 7 | errUnsupportedSMFFormat = errors.New("The SMF format was not expected.") 8 | errExpectedMthd = errors.New("Expected SMF Midi header.") 9 | errBadSizeChunk = errors.New("Chunk was an unexpected size.") 10 | errInterruptedByCallback = errors.New("interrupted by callback") 11 | 12 | // ErrMissing is the error returned, if there is no more data, but tracks are missing 13 | ErrMissing = errors.New("incomplete, tracks missing") 14 | 15 | // ErrFinished is returned 16 | ErrFinished = errors.New("reading of SMF finished successfully") 17 | ) 18 | -------------------------------------------------------------------------------- /v2/smf/example_test.go: -------------------------------------------------------------------------------- 1 | package smf_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "gitlab.com/gomidi/midi/v2" 8 | "gitlab.com/gomidi/midi/v2/gm" 9 | smf "gitlab.com/gomidi/midi/v2/smf" 10 | ) 11 | 12 | func Example() { 13 | // we write a SMF file into a buffer and read it back 14 | 15 | var ( 16 | bf bytes.Buffer 17 | clock = smf.MetricTicks(96) // resolution: 96 ticks per quarternote 960 is also a common choice 18 | general, piano, bass smf.Track // our tracks 19 | ) 20 | 21 | // first track must have tempo and meter informations 22 | general.Add(0, smf.MetaTrackSequenceName("general")) 23 | general.Add(0, smf.MetaMeter(3, 4)) 24 | general.Add(0, smf.MetaTempo(140)) 25 | general.Add(clock.Ticks4th()*6, smf.MetaTempo(130)) 26 | general.Add(clock.Ticks4th(), smf.MetaTempo(135)) 27 | general.Close(0) // don't forget to close a track 28 | 29 | piano.Add(0, smf.MetaInstrument("Piano")) 30 | piano.Add(0, midi.ProgramChange(0, gm.Instr_HonkytonkPiano.Value())) 31 | piano.Add(0, midi.NoteOn(0, 76, 120)) 32 | // duration: a quarter note (96 ticks in our case) 33 | piano.Add(clock.Ticks4th(), midi.NoteOff(0, 76)) 34 | piano.Close(0) 35 | 36 | bass.Add(0, smf.MetaInstrument("Bass")) 37 | bass.Add(0, midi.ProgramChange(1, gm.Instr_AcousticBass.Value())) 38 | bass.Add(clock.Ticks4th(), midi.NoteOn(1, 47, 64)) 39 | bass.Add(clock.Ticks4th()*3, midi.NoteOff(1, 47)) 40 | bass.Close(0) 41 | 42 | // create the SMF and add the tracks 43 | s := smf.New() 44 | s.TimeFormat = clock 45 | s.Add(general) 46 | s.Add(piano) 47 | s.Add(bass) 48 | 49 | // write the bytes to the buffer 50 | _, err := s.WriteTo(&bf) 51 | 52 | if err != nil { 53 | fmt.Printf("ERROR: %s", err.Error()) 54 | return 55 | } 56 | 57 | // read the bytes 58 | s, err = smf.ReadFrom(bytes.NewReader(bf.Bytes())) 59 | 60 | if err != nil { 61 | fmt.Printf("ERROR: %s", err.Error()) 62 | return 63 | } 64 | 65 | fmt.Printf("got %v tracks\n", len(s.Tracks)) 66 | 67 | for no, track := range s.Tracks { 68 | 69 | // it might be a good idea to go from delta ticks to absolute ticks. 70 | var absTicks uint64 71 | 72 | var trackname string 73 | var channel, program uint8 74 | var gm_name string 75 | 76 | for _, ev := range track { 77 | absTicks += uint64(ev.Delta) 78 | msg := ev.Message 79 | 80 | if msg.Type() == smf.MetaEndOfTrackMsg { 81 | // ignore 82 | continue 83 | } 84 | 85 | switch { 86 | case msg.GetMetaTrackName(&trackname): // set the trackname 87 | case msg.GetMetaInstrument(&trackname): // set the trackname based on instrument name 88 | case msg.GetProgramChange(&channel, &program): 89 | gm_name = "(" + gm.Instr(program).String() + ")" 90 | default: 91 | fmt.Printf("track %v %s %s @%v %s\n", no, trackname, gm_name, absTicks, ev.Message) 92 | } 93 | } 94 | } 95 | 96 | // Output: 97 | // got 3 tracks 98 | // track 0 general @0 MetaTimeSig meter: 3/4 99 | // track 0 general @0 MetaTempo bpm: 140.00 100 | // track 0 general @576 MetaTempo bpm: 130.00 101 | // track 0 general @672 MetaTempo bpm: 135.00 102 | // track 1 Piano (HonkytonkPiano) @0 NoteOn channel: 0 key: 76 velocity: 120 103 | // track 1 Piano (HonkytonkPiano) @96 NoteOff channel: 0 key: 76 104 | // track 2 Bass (AcousticBass) @96 NoteOn channel: 1 key: 47 velocity: 64 105 | // track 2 Bass (AcousticBass) @384 NoteOff channel: 1 key: 47 106 | } 107 | -------------------------------------------------------------------------------- /v2/smf/helpers.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | // dec2binDenom converts the decimal denominator to the binary one 4 | // it works, use it! 5 | func dec2binDenom(dec uint8) (bin uint8) { 6 | if dec <= 1 { 7 | return 0 8 | } 9 | for dec > 2 { 10 | bin++ 11 | dec = dec >> 1 12 | 13 | } 14 | return bin + 1 15 | } 16 | 17 | // bin2decDenom converts the binary denominator to the decimal 18 | func bin2decDenom(bin uint8) uint8 { 19 | if bin == 0 { 20 | return 1 21 | } 22 | return 2 << (bin - 1) 23 | } 24 | -------------------------------------------------------------------------------- /v2/smf/key_test.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | import ( 4 | "testing" 5 | 6 | "gitlab.com/gomidi/midi/v2" 7 | ) 8 | 9 | func writeFile(file string, sig Message) { 10 | s := New() 11 | var t Track 12 | t.Add(0, sig) 13 | t.Add(400, midi.NoteOn(0, 64, 33)) 14 | t.Add(400, midi.NoteOff(0, 64)) 15 | t.Close(0) 16 | s.Add(t) 17 | s.WriteFile(file) 18 | } 19 | 20 | func TestKeys(t *testing.T) { 21 | 22 | tests := []struct { 23 | sig func() Message 24 | file string 25 | expected string 26 | }{ 27 | {CMaj, "c-maj.mid", "CMaj"}, 28 | 29 | {GMaj, "g-maj.mid", "GMaj"}, 30 | {DMaj, "d-maj.mid", "DMaj"}, 31 | {AMaj, "a-maj.mid", "AMaj"}, 32 | {EMaj, "e-maj.mid", "EMaj"}, 33 | {BMaj, "h-maj.mid", "BMaj"}, 34 | {FsharpMaj, "fis-maj.mid", "FsharpMaj"}, 35 | 36 | {FMaj, "f-maj.mid", "FMaj"}, 37 | {BbMaj, "b-maj.mid", "BbMaj"}, 38 | {EbMaj, "es-maj.mid", "EbMaj"}, 39 | {AbMaj, "as-maj.mid", "AbMaj"}, 40 | {DbMaj, "des-maj.mid", "DbMaj"}, 41 | {GbMaj, "ges-maj.mid", "GbMaj"}, 42 | 43 | {AMin, "a-min.mid", "AMin"}, 44 | 45 | {BMin, "h-min.mid", "BMin"}, 46 | {CsharpMin, "cis-min.mid", "CsharpMin"}, 47 | {DsharpMin, "dis-min.mid", "DsharpMin"}, 48 | {EMin, "e-min.mid", "EMin"}, 49 | {FsharpMin, "fis-min.mid", "FsharpMin"}, 50 | {GsharpMin, "gis-min.mid", "GsharpMin"}, 51 | 52 | {DMin, "d-min.mid", "DMin"}, 53 | {GMin, "g-min.mid", "GMin"}, 54 | {CMin, "c-min.mid", "CMin"}, 55 | {FMin, "f-min.mid", "FMin"}, 56 | {BbMin, "b-min.mid", "BbMin"}, 57 | {EbMin, "es-min.mid", "EbMin"}, 58 | } 59 | 60 | for _, test := range tests { 61 | // writeFile(test.file, test.sig) 62 | var k Key 63 | test.sig().GetMetaKey(&k) 64 | 65 | if got, want := k.String(), test.expected; got != want { 66 | t.Errorf("%#v = %v; want %v", test.file, got, want) 67 | } 68 | /* 69 | */ 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /v2/smf/meta_test.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestMessagesString(t *testing.T) { 10 | 11 | tests := []struct { 12 | input Message 13 | expected string 14 | }{ 15 | { 16 | MetaCopyright("(c) 2017"), 17 | "MetaCopyright text: \"(c) 2017\"", 18 | }, 19 | { 20 | MetaCuepoint("verse"), 21 | "MetaCuepoint text: \"verse\"", 22 | }, 23 | { 24 | MetaDevice("2"), 25 | "MetaDevice text: \"2\"", 26 | }, 27 | { 28 | EOT, 29 | "MetaEndOfTrack", 30 | }, 31 | { 32 | MetaKey(0, true, 0, false), 33 | "MetaKeySig key: CMaj", 34 | }, 35 | { 36 | MetaLyric("yeah"), 37 | "MetaLyric text: \"yeah\"", 38 | }, 39 | { 40 | MetaMarker("TODO"), 41 | "MetaMarker text: \"TODO\"", 42 | }, 43 | { 44 | MetaChannel(3), 45 | "MetaChannel channel: 3", 46 | }, 47 | { 48 | MetaPort(10), 49 | "MetaPort port: 10", 50 | }, 51 | { 52 | MetaProgram("violin"), 53 | "MetaProgramName text: \"violin\"", 54 | }, 55 | { 56 | MetaTrackSequenceName("A"), 57 | "MetaTrackName text: \"A\"", 58 | }, 59 | { 60 | MetaSequenceNo(18), 61 | "MetaSeqNumber number: 18", 62 | }, 63 | { 64 | MetaSequencerData([]byte("hello world")), 65 | "MetaSeqData bytes: 68 65 6C 6C 6F 20 77 6F 72 6C 64", 66 | }, 67 | { 68 | MetaSMPTE( 69 | 2, // hour 70 | 3, // minute 71 | 4, // second 72 | 5, // frame 73 | 6, // factional frame 74 | ), 75 | "MetaSMPTEOffset hour: 2 minute: 3 second: 4 frame: 5 fractframe: 6", 76 | }, 77 | { 78 | MetaTempo(240), 79 | "MetaTempo bpm: 240.00", 80 | }, 81 | { 82 | MetaText("hi"), 83 | "MetaText text: \"hi\"", 84 | }, 85 | { 86 | MetaTimeSig(3, 4, 8, 8), 87 | "MetaTimeSig meter: 3/4", 88 | }, 89 | { 90 | MetaInstrument("1st violins"), 91 | "MetaInstrument text: \"1st violins\"", 92 | }, 93 | } 94 | 95 | for _, test := range tests { 96 | 97 | var bf bytes.Buffer 98 | 99 | bf.WriteString(test.input.String()) 100 | 101 | if got, want := bf.String(), test.expected; got != want { 102 | t.Errorf("got: %#v; wanted %#v", got, want) 103 | } 104 | } 105 | 106 | } 107 | 108 | func TestMessagesRaw(t *testing.T) { 109 | 110 | tests := []struct { 111 | input Message 112 | expected string 113 | }{ 114 | { 115 | MetaCopyright("(c) 2017"), 116 | "FF 02 08 28 63 29 20 32 30 31 37", 117 | }, 118 | { 119 | MetaCuepoint("verse"), 120 | "FF 07 05 76 65 72 73 65", 121 | }, 122 | { 123 | MetaDevice("2"), 124 | "FF 09 01 32", 125 | }, 126 | { 127 | EOT, 128 | "FF 2F 00", 129 | }, 130 | { 131 | MetaKey(0, true, 0, false), 132 | "FF 59 02 00 00", 133 | }, 134 | { 135 | MetaLyric("yeah"), 136 | "FF 05 04 79 65 61 68", 137 | }, 138 | { 139 | MetaMarker("TODO"), 140 | "FF 06 04 54 4F 44 4F", 141 | }, 142 | { 143 | MetaChannel(3), 144 | "FF 20 01 03", 145 | }, 146 | { 147 | MetaPort(10), 148 | "FF 21 01 0A", 149 | }, 150 | { 151 | MetaProgram("violin"), 152 | "FF 08 06 76 69 6F 6C 69 6E", 153 | }, 154 | { 155 | MetaTrackSequenceName("A"), 156 | "FF 03 01 41", 157 | }, 158 | { 159 | MetaSequenceNo(18), 160 | "FF 00 02 00 12", 161 | }, 162 | { 163 | MetaSequencerData([]byte("hello world")), 164 | "FF 7F 0B 68 65 6C 6C 6F 20 77 6F 72 6C 64", 165 | }, 166 | { 167 | MetaSMPTE( 168 | 2, 169 | 3, 170 | 4, 171 | 5, 172 | 6, 173 | ), 174 | "FF 54 05 02 03 04 05 06", 175 | }, 176 | { 177 | MetaTempo(240), 178 | "FF 51 03 03 D0 90", 179 | }, 180 | { 181 | MetaText("hi"), 182 | "FF 01 02 68 69", 183 | }, 184 | { 185 | MetaTimeSig(3, 4, 8, 8), 186 | "FF 58 04 03 02 08 08", 187 | }, 188 | { 189 | MetaInstrument("1st violins"), 190 | "FF 04 0B 31 73 74 20 76 69 6F 6C 69 6E 73", 191 | }, 192 | } 193 | 194 | for _, test := range tests { 195 | 196 | var bf bytes.Buffer 197 | 198 | bf.Write(test.input.Bytes()) 199 | 200 | if got, want := fmt.Sprintf("% X", bf.Bytes()), test.expected; got != want { 201 | t.Errorf("got: %#v; wanted %#v", got, want) 202 | } 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /v2/smf/meter_test.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMeter(t *testing.T) { 8 | 9 | tests := []struct { 10 | input Message 11 | expected string 12 | }{ 13 | {MetaMeter(2, 4), "MetaTimeSig meter: 2/4"}, 14 | {MetaMeter(3, 4), "MetaTimeSig meter: 3/4"}, 15 | {MetaMeter(4, 4), "MetaTimeSig meter: 4/4"}, 16 | {MetaMeter(5, 8), "MetaTimeSig meter: 5/8"}, 17 | {MetaMeter(6, 8), "MetaTimeSig meter: 6/8"}, 18 | {MetaMeter(7, 8), "MetaTimeSig meter: 7/8"}, 19 | {MetaMeter(12, 8), "MetaTimeSig meter: 12/8"}, 20 | } 21 | 22 | for _, test := range tests { 23 | 24 | if got, want := test.input.String(), test.expected; got != want { 25 | t.Errorf("(%v).String() = %v; want %v", test.input, got, want) 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /v2/smf/player/errors.go: -------------------------------------------------------------------------------- 1 | package player 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNoSMFData = errors.New("no SMF data is set") 7 | ErrIsPlaying = errors.New("already playing") 8 | ErrIsStopped = errors.New("already stopped") 9 | ErrInvalidSMF = errors.New("failed to read tracks from SMF data") 10 | ErrNoOutPort = errors.New("failed to get sound out port") 11 | errDone = errors.New("done playing SMF data") 12 | errStopped = errors.New("stopped playing") 13 | errPaused = errors.New("paused playing") 14 | ) 15 | -------------------------------------------------------------------------------- /v2/smf/player/utils.go: -------------------------------------------------------------------------------- 1 | package player 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // IgnoreError will call function f and ignore error return. 10 | // Useful to explicitly ignore errors in deferred functions. 11 | func IgnoreError(f func() error) { 12 | _ = f() 13 | } 14 | 15 | // WrapOnError will return nil if errInner is nil. 16 | // Otherwise returns an error that wraps both errInner and errOuter. 17 | func WrapOnError(errInner error, errOuter error) error { 18 | if errInner != nil { 19 | return fmt.Errorf("%w: %w", errOuter, errInner) 20 | } 21 | return nil 22 | } 23 | 24 | var ErrUnavailable = errors.New("unavailable context") 25 | 26 | // UnavailableContext returns a context that is already canceled 27 | func UnavailableContext() context.Context { 28 | // Create a context and cancel immediately 29 | var ctx, cancel = context.WithCancelCause(context.TODO()) 30 | cancel(ErrUnavailable) 31 | return ctx 32 | } 33 | -------------------------------------------------------------------------------- /v2/smf/tempo_test.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestTempo(t *testing.T) { 9 | bt := []byte{0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20} // 120 BPM 10 | 11 | tm := Message(bt) 12 | 13 | var bpm float64 14 | if tm.GetMetaTempo(&bpm) { 15 | if bpm != 120 { 16 | t.Errorf("wrong tempo: wanted 120, got: %v", bpm) 17 | } 18 | } else { 19 | t.Fatalf("is no proper tempo message") 20 | } 21 | } 22 | 23 | func TestTempo2(t *testing.T) { 24 | tt := MetaTempo(120) 25 | 26 | if got, want := tt.Bytes(), []byte{0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20}; !reflect.DeepEqual(got, want) { 27 | t.Errorf("got % X wanted: % X", got, want) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /v2/smf/tempochanges.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | type TempoChange struct { 4 | AbsTicks int64 5 | AbsTimeMicroSec int64 6 | BPM float64 7 | } 8 | 9 | type TempoChanges []*TempoChange 10 | 11 | func (t TempoChanges) Swap(a, b int) { 12 | t[a], t[b] = t[b], t[a] 13 | } 14 | 15 | func (t TempoChanges) Len() int { 16 | return len(t) 17 | } 18 | 19 | func (t TempoChanges) Less(a, b int) bool { 20 | return t[a].AbsTicks < t[b].AbsTicks 21 | } 22 | 23 | func (t TempoChanges) TempoAt(absTicks int64) (bpm float64) { 24 | tc := t.TempoChangeAt(absTicks) 25 | if tc == nil { 26 | return 120.00 27 | } 28 | return tc.BPM 29 | } 30 | 31 | func (t TempoChanges) TempoChangeAt(absTicks int64) (tch *TempoChange) { 32 | for _, tc := range t { 33 | if tc.AbsTicks > absTicks { 34 | break 35 | } 36 | tch = tc 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /v2/smf/timeformat.go: -------------------------------------------------------------------------------- 1 | package smf 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | ) 8 | 9 | var ( 10 | _ TimeFormat = MetricTicks(0) 11 | _ TimeFormat = TimeCode{} 12 | ) 13 | 14 | // TimeFormat is the common interface of all SMF time formats 15 | type TimeFormat interface { 16 | String() string 17 | timeformat() // make the implementation exclusive to this package 18 | } 19 | 20 | // TimeCode is the SMPTE time format. 21 | // It can be comfortable created with the SMPTE* functions. 22 | type TimeCode struct { 23 | FramesPerSecond uint8 24 | SubFrames uint8 25 | } 26 | 27 | // String represents the TimeCode as a string. 28 | func (t TimeCode) String() string { 29 | 30 | switch t.FramesPerSecond { 31 | case 29: 32 | return fmt.Sprintf("SMPTE30DropFrame %v subframes", t.SubFrames) 33 | default: 34 | return fmt.Sprintf("SMPTE%v %v subframes", t.FramesPerSecond, t.SubFrames) 35 | } 36 | 37 | } 38 | 39 | func (t TimeCode) timeformat() {} 40 | 41 | // SMPTE24 returns a SMPTE24 TimeCode with the given subframes. 42 | func SMPTE24(subframes uint8) TimeCode { 43 | return TimeCode{24, subframes} 44 | } 45 | 46 | // SMPTE25 returns a SMPTE25 TimeCode with the given subframes. 47 | func SMPTE25(subframes uint8) TimeCode { 48 | return TimeCode{25, subframes} 49 | } 50 | 51 | // SMPTE30DropFrame returns a SMPTE30 drop frame TimeCode with the given subframes. 52 | func SMPTE30DropFrame(subframes uint8) TimeCode { 53 | return TimeCode{29, subframes} 54 | } 55 | 56 | // SMPTE30 returns a SMPTE30 TimeCode with the given subframes. 57 | func SMPTE30(subframes uint8) TimeCode { 58 | return TimeCode{30, subframes} 59 | } 60 | 61 | // MetricTicks represents the "ticks per quarter note" (metric) time format. 62 | // It defaults to 960 (i.e. 0 is treated as if it where 960 ticks per quarter note). 63 | type MetricTicks uint16 64 | 65 | const defaultMetric MetricTicks = 960 66 | 67 | // In64ths returns the deltaTicks in 64th notes. 68 | // To get 32ths, divide result by 2. 69 | // To get 16ths, divide result by 4. 70 | // To get 8ths, divide result by 8. 71 | // To get 4ths, divide result by 16. 72 | func (q MetricTicks) In64ths(deltaTicks uint32) uint32 { 73 | if q == 0 { 74 | q = defaultMetric 75 | } 76 | return (deltaTicks * 16) / uint32(q) 77 | } 78 | 79 | // Duration returns the time.Duration for a number of ticks at a certain tempo (in fractional BPM) 80 | func (q MetricTicks) Duration(fractionalBPM float64, deltaTicks uint32) time.Duration { 81 | if q == 0 { 82 | q = defaultMetric 83 | } 84 | // (60000 / T) * (d / R) = D[ms] 85 | // durQnMilli := 60000 / float64(tempoBPM) 86 | // _4thticks := float64(deltaTicks) / float64(uint16(q)) 87 | res := 60000000000 * float64(deltaTicks) / (fractionalBPM * float64(uint16(q))) 88 | //fmt.Printf("what: %vns\n", res) 89 | return time.Duration(int64(math.Round(res))) 90 | // return time.Duration(roundFloat(durQnMilli*_4thticks, 0)) * time.Millisecond 91 | } 92 | 93 | // Ticks returns the ticks for a given time.Duration at a certain tempo (in fractional BPM) 94 | func (q MetricTicks) Ticks(fractionalBPM float64, d time.Duration) (ticks uint32) { 95 | if q == 0 { 96 | q = defaultMetric 97 | } 98 | // d = (D[ms] * R * T) / 60000 99 | ticks = uint32(math.Round((float64(d.Nanoseconds()) / 1000000 * float64(uint16(q)) * fractionalBPM) / 60000)) 100 | return ticks 101 | } 102 | 103 | func (q MetricTicks) div(d float64) uint32 { 104 | if q == 0 { 105 | q = defaultMetric 106 | } 107 | return uint32(math.Round(float64(q.Resolution()) / d)) 108 | } 109 | 110 | // Resolution returns the number of the metric ticks (ticks for a quarter note, defaults to 960) 111 | func (q MetricTicks) Resolution() uint16 { 112 | if q == 0 { 113 | q = defaultMetric 114 | } 115 | return uint16(q) 116 | } 117 | 118 | // Ticks4th returns the ticks for a quarter note 119 | func (q MetricTicks) Ticks4th() uint32 { 120 | return uint32(q.Resolution()) 121 | } 122 | 123 | // Ticks8th returns the ticks for a quaver note 124 | func (q MetricTicks) Ticks8th() uint32 { 125 | return q.div(2) 126 | } 127 | 128 | // Ticks16th returns the ticks for a 16th note 129 | func (q MetricTicks) Ticks16th() uint32 { 130 | return q.div(4) 131 | } 132 | 133 | // Ticks32th returns the ticks for a 32th note 134 | func (q MetricTicks) Ticks32th() uint32 { 135 | return q.div(8) 136 | } 137 | 138 | // Ticks64th returns the ticks for a 64th note 139 | func (q MetricTicks) Ticks64th() uint32 { 140 | return q.div(16) 141 | } 142 | 143 | // Ticks128th returns the ticks for a 128th note 144 | func (q MetricTicks) Ticks128th() uint32 { 145 | return q.div(32) 146 | } 147 | 148 | // Ticks256th returns the ticks for a 256th note 149 | func (q MetricTicks) Ticks256th() uint32 { 150 | return q.div(64) 151 | } 152 | 153 | // Ticks512th returns the ticks for a 512th note 154 | func (q MetricTicks) Ticks512th() uint32 { 155 | return q.div(128) 156 | } 157 | 158 | // Ticks1024th returns the ticks for a 1024th note 159 | func (q MetricTicks) Ticks1024th() uint32 { 160 | return q.div(256) 161 | } 162 | 163 | // String returns the string representation of the quarter note resolution 164 | func (q MetricTicks) String() string { 165 | return fmt.Sprintf("%v MetricTicks", q.Resolution()) 166 | } 167 | 168 | func (q MetricTicks) timeformat() {} 169 | -------------------------------------------------------------------------------- /v2/syscommon.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | /* 4 | 5 | System Common Message Status Byte Number of Data Bytes 6 | --------------------- ----------- -------------------- 7 | MIDI Timing Code F1 1 8 | Song Position Pointer F2 2 9 | Song Select F3 1 10 | Tune Request F6 None 11 | 12 | */ 13 | 14 | const ( 15 | byteMIDITimingCodeMessage = byte(0xF1) 16 | byteSysSongPositionPointer = byte(0xF2) 17 | byteSysSongSelect = byte(0xF3) 18 | byteSysTuneRequest = byte(0xF6) 19 | ) 20 | 21 | var syscommMessages = map[byte]Type{ 22 | byteMIDITimingCodeMessage:/* SysCommonMsg.Set(MTCMsg), */ MTCMsg, 23 | byteSysSongPositionPointer:/* SysCommonMsg.Set(SPPMsg), */ SPPMsg, 24 | byteSysSongSelect:/* SysCommonMsg.Set(SongSelectMsg), */ SongSelectMsg, 25 | byteSysTuneRequest:/* SysCommonMsg.Set(TuneMsg), */ TuneMsg, 26 | } 27 | 28 | // Tune returns a tune message 29 | func Tune() Message { 30 | //return NewMessage([]byte{byteSysTuneRequest}) 31 | return []byte{byteSysTuneRequest} 32 | } 33 | 34 | // SPP returns a song position pointer message 35 | func SPP(pointer uint16) Message { 36 | var b = make([]byte, 2) 37 | b[1] = byte(pointer & 0x7F) 38 | b[0] = byte((pointer >> 7) & 0x7F) 39 | //return NewMessage([]byte{byteSysSongPositionPointer, b[0], b[1]}) 40 | return []byte{byteSysSongPositionPointer, b[0], b[1]} 41 | } 42 | 43 | // SongSelect returns a song select message 44 | func SongSelect(song uint8) Message { 45 | // TODO check - it is a guess 46 | //return NewMessage([]byte{byteSysSongSelect, song}) 47 | return []byte{byteSysSongSelect, song} 48 | } 49 | 50 | /* 51 | MTC Quarter Frame 52 | 53 | These are the MTC (i.e. SMPTE based) equivalent of the F8 Timing Clock messages, 54 | though offer much higher resolution, as they are sent at a rate of 96 to 120 times 55 | a second (depending on the SMPTE frame rate). Each Quarter Frame message provides 56 | partial timecode information, 8 sequential messages being required to fully 57 | describe a timecode instant. The reconstituted timecode refers to when the first 58 | partial was received. The most significant nibble of the data byte indicates the 59 | partial (aka Message Type). 60 | 61 | Partial Data byte Usage 62 | 1 0000 bcde Frame number LSBs abcde = Frame number (0 to frameRate-1) 63 | 2 0001 000a Frame number MSB 64 | 3 0010 cdef Seconds LSBs abcdef = Seconds (0-59) 65 | 4 0011 00ab Seconds MSBs 66 | 5 0100 cdef Minutes LSBs abcdef = Minutes (0-59) 67 | 6 0101 00ab Minutes MSBs 68 | 7 0110 defg Hours LSBs ab = Frame Rate (00 = 24, 01 = 25, 10 = 30drop, 11 = 30nondrop) 69 | cdefg = Hours (0-23) 70 | 8 0111 0abc Frame Rate, and Hours MSB 71 | */ 72 | 73 | // MTC returns a timing code message (quarter frame) 74 | func MTC(m uint8) Message { 75 | // TODO check - it is a guess 76 | // TODO provide a better abstraction for MTC 77 | //return NewMessage([]byte{byteMIDITimingCodeMessage, byte(m)}) 78 | return []byte{byteMIDITimingCodeMessage, byte(m)} 79 | } 80 | -------------------------------------------------------------------------------- /v2/syscommon_test.go: -------------------------------------------------------------------------------- 1 | package midi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "gitlab.com/gomidi/midi/v2" 7 | ) 8 | 9 | func TestSysCommon(t *testing.T) { 10 | 11 | tests := []struct { 12 | msg midi.Message 13 | expected string 14 | }{ 15 | { 16 | midi.MTC(3), 17 | "MTC mtc: 3", 18 | }, 19 | { 20 | midi.Tune(), 21 | "Tune", 22 | }, 23 | { 24 | midi.SongSelect(5), 25 | "SongSelect song: 5", 26 | }, 27 | { 28 | midi.SPP(4), 29 | "SPP spp: 4", 30 | }, 31 | { 32 | midi.SPP(4000), 33 | "SPP spp: 4000", 34 | }, 35 | } 36 | 37 | for n, test := range tests { 38 | //m := midi.Message(test.msg) 39 | m := test.msg 40 | 41 | if got, want := m.String(), test.expected; got != want { 42 | t.Errorf("[%v] (% X).String() = %#v; want %#v", n, test.msg, got, want) 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /v2/sysex/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 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 | // Package sysex provides helpers when dealing with system exclusiv messages 6 | package sysex 7 | -------------------------------------------------------------------------------- /v2/sysex/nonrealtime.go: -------------------------------------------------------------------------------- 1 | package sysex 2 | 3 | import "bytes" 4 | 5 | type NonRealtime struct { 6 | Channel byte 7 | SubID1 byte 8 | SubID2 byte 9 | } 10 | 11 | func (r NonRealtime) SysEx() []byte { 12 | var bf bytes.Buffer 13 | 14 | bf.WriteByte(0xF0) 15 | bf.WriteByte(0x7E) 16 | bf.WriteByte(r.Channel) 17 | bf.WriteByte(r.SubID1) 18 | bf.WriteByte(r.SubID2) 19 | 20 | bf.WriteByte(0xF7) 21 | return bf.Bytes() 22 | 23 | } 24 | 25 | func GMSystem(channel byte, enable bool) []byte { 26 | var n NonRealtime 27 | n.Channel = channel 28 | n.SubID1 = 0x09 29 | if enable { 30 | n.SubID2 = 0x01 31 | } else { 32 | n.SubID2 = 0x00 33 | } 34 | 35 | return n.SysEx() 36 | } 37 | 38 | /* 39 | 40 | GM System Enable/Disable 41 | 42 | This Universal SysEx message enables or disables the General MIDI mode of a sound module. Some devices have 43 | built-in GM modules or GM Patch Sets in addition to non-GM Patch Sets or non-GM modes of operation. When GM 44 | is enabled, it replaces any non-GM Patch Set or non-GM mode with a GM mode/patch set. This allows a device to have 45 | modes or Patch Sets that go beyond the limits of GM, and yet, still have the capability to be switched into a 46 | GM-compliant mode when desirable. 47 | 48 | 0xF0 SysEx 49 | 0x7E Non-Realtime 50 | 0x7F The SysEx channel. Could be from 0x00 to 0x7F. 51 | Here we set it to "disregard channel". 52 | 0x09 Sub-ID -- GM System Enable/Disable 53 | 0xNN Sub-ID2 -- NN=00 for disable, NN=01 for enable 54 | 0xF7 End of SysEx 55 | 56 | It is best to respond as quickly as possible to this message, and to be ready to accept incoming note (and other) 57 | messages soon after, as this message may be included at the start of a General MIDI file to ensure playback by a 58 | GM module. Most modules are fully setup in GM mode by 100 milliseconds after receiving the GM System Enable message. 59 | 60 | When GM Mode is first enabled, a device should assume that the "Grand Piano" patch (ie, the first GM patch) is the 61 | currently selected patch upon all 16 MIDI channels. The device should also internally reset all controllers and 62 | assume the power-up state described in the General MIDI Specification. 63 | 64 | While GM mode is enabled, a device should also ignore Bank Select messages (since GM does not have more than one 65 | bank of patches). Only when the GM Disable message is received (with Sub-ID2 = 0 to disable GM mode) will a device 66 | then respond to Bank Select messages (and knock itself out of GM mode). 67 | 68 | */ 69 | 70 | func IdentityRequest(channel byte) []byte { 71 | var n NonRealtime 72 | n.Channel = channel 73 | n.SubID1 = 0x06 74 | n.SubID2 = 0x01 75 | return n.SysEx() 76 | } 77 | 78 | func IdentityReply(channel byte, manuID ManufacturerID, familycode [2]byte, modelnumber [2]byte, version [4]byte) []byte { 79 | var bf bytes.Buffer 80 | var n NonRealtime 81 | n.Channel = channel 82 | n.SubID1 = 0x06 83 | n.SubID2 = 0x02 84 | bt := n.SysEx() 85 | bf.Write(bt[:len(bt)-1]) // strip the 0xF7 86 | bf.WriteByte(byte(manuID)) 87 | bf.WriteByte(familycode[0]) 88 | bf.WriteByte(familycode[1]) 89 | bf.WriteByte(modelnumber[0]) 90 | bf.WriteByte(modelnumber[1]) 91 | bf.WriteByte(version[0]) 92 | bf.WriteByte(version[1]) 93 | bf.WriteByte(version[2]) 94 | bf.WriteByte(version[3]) 95 | bf.WriteByte(0xF7) 96 | 97 | return bf.Bytes() 98 | } 99 | 100 | /* 101 | 102 | Identity Request / Reply 103 | 104 | Sometimes, a device may wish to know what other devices are connected to it. For example, a Patch Editor software running on a computer may wish to know what devices are connected to the computer's MIDI port, so that the software can configure itself to accept dumps from those devices. 105 | 106 | The Identity Request Universal Sysex message can be sent by the Patch Editor software. When this message is received by some device connected to the computer, that device will respond by sending an Identity Reply Universal Sysex message back to the computer. The Patch Editor can then examine the information in the Identity Reply message to determine what make and model device is connected to the computer. Each device that understands the Identity Request will reply with its own Identity Reply message. 107 | 108 | Here is the Identity Request message: 109 | 110 | 0xF0 SysEx 111 | 0x7E Non-Realtime 112 | 0x7F The SysEx channel. Could be from 0x00 to 0x7F. 113 | Here we set it to "disregard channel". 114 | 0x06 Sub-ID -- General Information 115 | 0x01 Sub-ID2 -- Identity Request 116 | 0xF7 End of SysEx 117 | 118 | Here is the Identity Reply message: 119 | 120 | 0xF0 SysEx 121 | 0x7E Non-Realtime 122 | 0x7F The SysEx channel. Could be from 0x00 to 0x7F. 123 | Here we set it to "disregard channel". 124 | 0x06 Sub-ID -- General Information 125 | 0x02 Sub-ID2 -- Identity Reply 126 | 0xID Manufacturer's ID 127 | 0xf1 The f1 and f2 bytes make up the family code. Each 128 | 0xf2 manufacturer assigns different family codes to his products. 129 | 0xp1 The p1 and p2 bytes make up the model number. Each 130 | 0xp2 manufacturer assigns different model numbers to his products. 131 | 0xv1 The v1, v2, v3 and v4 bytes make up the version number. 132 | 0xv2 133 | 0xv3 134 | 0xv4 135 | 0xF7 End of SysEx 136 | 137 | 138 | */ 139 | -------------------------------------------------------------------------------- /v2/sysex/realtime.go: -------------------------------------------------------------------------------- 1 | package sysex 2 | 3 | import "bytes" 4 | 5 | type Realtime struct { 6 | Channel byte 7 | SubID1 byte 8 | SubID2 byte 9 | } 10 | 11 | func (r Realtime) SysEx() []byte { 12 | var bf bytes.Buffer 13 | 14 | bf.WriteByte(0xF0) 15 | bf.WriteByte(0x7F) 16 | bf.WriteByte(r.Channel) 17 | bf.WriteByte(r.SubID1) 18 | bf.WriteByte(r.SubID2) 19 | 20 | bf.WriteByte(0xF7) 21 | return bf.Bytes() 22 | } 23 | 24 | const EveryChannel = 0x7F 25 | 26 | func MasterVolume(channel byte, vol uint16) []byte { 27 | var r Realtime 28 | r.Channel = channel 29 | r.SubID1 = 0x04 30 | r.SubID2 = 0x01 31 | 32 | /* 33 | TODO: parse the bits 0 to 6 and 7 to 13 of a 14-bit volume 34 | */ 35 | 36 | return r.SysEx() 37 | } 38 | 39 | /* 40 | 41 | Master Volume 42 | 43 | This Universal SysEx message adjusts a device's master volume. Remember that in a multitimbral device, 44 | the Volume controller messages are used to control the volumes of the individual Parts. So, we need some 45 | message to control Master Volume. Here it is. 46 | 47 | 0xF0 SysEx 48 | 0x7F Realtime 49 | 0x7F The SysEx channel. Could be from 0x00 to 0x7F. 50 | Here we set it to "disregard channel". 51 | 0x04 Sub-ID -- Device Control 52 | 0x01 Sub-ID2 -- Master Volume 53 | 0xLL Bits 0 to 6 of a 14-bit volume 54 | 0xMM Bits 7 to 13 of a 14-bit volume 55 | 0xF7 End of SysEx 56 | 57 | 58 | */ 59 | -------------------------------------------------------------------------------- /v2/sysex/sysex.go: -------------------------------------------------------------------------------- 1 | package sysex 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | // see https://www.2writers.com/eddie/TutSysEx.htm 9 | type Manufacturer struct { 10 | ManufacturerID ManufacturerID 11 | DeviceID byte 12 | ModelID byte 13 | InfoRequest bool // true: requesting infos, false: sending infos 14 | Address [3]byte 15 | SendingData []byte 16 | NumReqBytes [3]byte 17 | } 18 | 19 | var GMReset = Manufacturer{ 20 | //F0 41 10 42 12 40007F 00 41 F7 21 | ManufacturerID: 0x41, 22 | DeviceID: 0x10, 23 | ModelID: 0x42, 24 | InfoRequest: false, 25 | Address: [3]byte{0x40, 0x00, 0x7F}, 26 | SendingData: []byte{0x00}, 27 | } 28 | 29 | func init() { 30 | bt := fmt.Sprintf("% X", GMReset.SysEx()) 31 | if bt != "F0 41 10 42 12 40 00 7F 00 41 F7" { 32 | panic(bt) 33 | } 34 | } 35 | 36 | func Parse(bt []byte) (*Manufacturer, error) { 37 | if len(bt) < 11 { 38 | return nil, fmt.Errorf("sysex message too short (must be 11 bytes minimum") 39 | } 40 | 41 | if bt[0] != 0xF0 { 42 | return nil, fmt.Errorf("missing start byte 0xF0") 43 | } 44 | 45 | var s Manufacturer 46 | 47 | s.ManufacturerID = ManufacturerID(bt[1]) 48 | s.DeviceID = bt[2] 49 | s.ModelID = bt[3] 50 | switch bt[4] { 51 | case 0x11: 52 | s.InfoRequest = true 53 | case 0x12: 54 | s.InfoRequest = false 55 | default: 56 | return nil, fmt.Errorf("invalid send/req byte") 57 | } 58 | 59 | s.Address[0] = bt[5] 60 | s.Address[1] = bt[6] 61 | s.Address[2] = bt[7] 62 | 63 | if s.InfoRequest { 64 | if len(bt) < 13 { 65 | return nil, fmt.Errorf("sysex message for requesting data too short (must be 13 bytes minimum") 66 | } 67 | s.NumReqBytes[0] = bt[8] 68 | s.NumReqBytes[1] = bt[9] 69 | s.NumReqBytes[2] = bt[10] 70 | } else { 71 | s.SendingData = bt[8 : len(bt)-1] 72 | } 73 | 74 | checksum := bt[len(bt)-2] 75 | 76 | if checksum != s.Checksum() { 77 | return nil, fmt.Errorf("invalid checksum") 78 | } 79 | 80 | if bt[len(bt)-1] != 0xF7 { 81 | return nil, fmt.Errorf("missing end byte 0xF7") 82 | } 83 | 84 | return &s, nil 85 | } 86 | 87 | func (s Manufacturer) Checksum() (sum byte) { 88 | 89 | /* 90 | 1. Convert hex to decimal: 91 | 40h = 64 92 | 11h = 17 93 | 00h = 0 94 | 41h = 65 95 | 63h = 99 96 | 97 | 2. Add values: 98 | 64 + 17 + 0 + 65 + 99 = 245 99 | 100 | 3. Divide by 128 101 | 245 / 128 = 1 remainder 117 102 | 103 | 4. Subtract remainder from 128, if remainder is not 0* 104 | 128 - 117 = 11 105 | 106 | 5. Covert to hex: 107 | 11 = 0Bh 108 | 109 | *If the remainder is 0 then the checksum is 0. 110 | */ 111 | 112 | var bt = []byte{s.Address[0], s.Address[1], s.Address[2]} 113 | 114 | if s.InfoRequest { 115 | bt = append(bt, s.NumReqBytes[0], s.NumReqBytes[1], s.NumReqBytes[2]) 116 | } else { 117 | bt = append(bt, s.SendingData...) 118 | } 119 | 120 | var su int32 121 | 122 | for _, b := range bt { 123 | su += int32(b) 124 | } 125 | 126 | rem := su % 128 127 | 128 | if rem == 0 { 129 | return 0 130 | } 131 | 132 | return byte(128 - rem) 133 | } 134 | 135 | func (s Manufacturer) SysEx() []byte { 136 | var bf bytes.Buffer 137 | 138 | bf.WriteByte(0xF0) 139 | bf.WriteByte(byte(s.ManufacturerID)) 140 | bf.WriteByte(s.DeviceID) 141 | bf.WriteByte(s.ModelID) 142 | if s.InfoRequest { 143 | bf.WriteByte(0x11) 144 | } else { 145 | bf.WriteByte(0x12) 146 | } 147 | bf.WriteByte(s.Address[0]) 148 | bf.WriteByte(s.Address[1]) 149 | bf.WriteByte(s.Address[2]) 150 | 151 | if s.InfoRequest { 152 | bf.WriteByte(s.NumReqBytes[0]) 153 | bf.WriteByte(s.NumReqBytes[1]) 154 | bf.WriteByte(s.NumReqBytes[2]) 155 | } else { 156 | bf.Write(s.SendingData) 157 | } 158 | 159 | bf.WriteByte(s.Checksum()) 160 | 161 | bf.WriteByte(0xF7) 162 | 163 | return bf.Bytes() 164 | } 165 | -------------------------------------------------------------------------------- /v2/sysex_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | //"gitlab.com/gomidi/midi/v2/sysex" 7 | ) 8 | 9 | func TestMessageString(t *testing.T) { 10 | 11 | tests := []struct { 12 | input Message 13 | expected string 14 | }{ 15 | { 16 | //midi.SysEx(sysex.GMSystem(2, true)), 17 | SysEx([]byte{0x7E, 0x02, 0x09, 0x01}), 18 | "SysExType data: 7E 02 09 01", 19 | }, 20 | { 21 | SPP(4), 22 | "SPP spp: 4", 23 | }, 24 | { 25 | SongSelect(2), 26 | "SongSelect song: 2", 27 | }, 28 | { 29 | Tune(), 30 | "Tune", 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | var bf bytes.Buffer 36 | 37 | bf.WriteString(test.input.String()) 38 | 39 | if got, want := bf.String(), test.expected; got != want { 40 | t.Errorf("got: %#v; wanted %#v", got, want) 41 | } 42 | } 43 | 44 | } 45 | 46 | /* 47 | func TestMessagesString(t *testing.T) { 48 | 49 | tests := []struct { 50 | input Message 51 | expected string 52 | }{ 53 | { 54 | MIDITimingCode(3), 55 | "syscommon.MIDITimingCode: 3", 56 | }, 57 | { 58 | SongPositionPointer(4), 59 | "syscommon.SongPositionPointer: 4", 60 | }, 61 | { 62 | SongSelect(2), 63 | "syscommon.SongSelect: 2", 64 | }, 65 | { 66 | TuneRequest, 67 | "syscommon.tuneRequest", 68 | }, 69 | } 70 | 71 | for _, test := range tests { 72 | 73 | var bf bytes.Buffer 74 | 75 | bf.WriteString(test.input.String()) 76 | 77 | if got, want := bf.String(), test.expected; got != want { 78 | t.Errorf("got: %#v; wanted %#v", got, want) 79 | } 80 | } 81 | 82 | } 83 | */ 84 | 85 | /* 86 | func TestMessagesRaw(t *testing.T) { 87 | 88 | tests := []struct { 89 | input Message 90 | expected string 91 | }{ 92 | { 93 | SysEx([]byte("12,3")), 94 | "F0 31 32 2C 33 F7", 95 | }, 96 | { 97 | Start([]byte("12,3")), 98 | "F0 31 32 2C 33", 99 | }, 100 | { 101 | Continue([]byte("12,3")), 102 | "F7 31 32 2C 33", 103 | }, 104 | { 105 | End([]byte("12,3")), 106 | "F7 31 32 2C 33 F7", 107 | }, 108 | { 109 | Escape([]byte{0xFF, 0xF2}), 110 | "F7 FF F2", 111 | }, 112 | } 113 | 114 | for _, test := range tests { 115 | 116 | var bf bytes.Buffer 117 | 118 | bf.Write(test.input.Raw()) 119 | 120 | if got, want := fmt.Sprintf("% X", bf.Bytes()), test.expected; got != want { 121 | t.Errorf("got: %#v; wanted %#v", got, want) 122 | } 123 | } 124 | 125 | } 126 | 127 | func TestMessagesData(t *testing.T) { 128 | 129 | tests := []struct { 130 | input Message 131 | expected string 132 | }{ 133 | { 134 | SysEx([]byte("12,3")), 135 | "31 32 2C 33", 136 | }, 137 | { 138 | Start([]byte("12,3")), 139 | "31 32 2C 33", 140 | }, 141 | { 142 | Continue([]byte("12,3")), 143 | "31 32 2C 33", 144 | }, 145 | { 146 | End([]byte("12,3")), 147 | "31 32 2C 33", 148 | }, 149 | { 150 | Escape([]byte{0xFF, 0xF2}), 151 | "FF F2", 152 | }, 153 | } 154 | 155 | for _, test := range tests { 156 | 157 | var bf bytes.Buffer 158 | 159 | bf.Write(test.input.Data()) 160 | 161 | if got, want := fmt.Sprintf("% X", bf.Bytes()), test.expected; got != want { 162 | t.Errorf("got: %#v; wanted %#v", got, want) 163 | } 164 | } 165 | 166 | } 167 | 168 | func TestMessagesString(t *testing.T) { 169 | 170 | tests := []struct { 171 | input Message 172 | expected string 173 | }{ 174 | { 175 | SysEx([]byte("12,3")), 176 | "sysex.SysEx len: 4", 177 | }, 178 | { 179 | Start([]byte("12,3")), 180 | "sysex.Start len: 4", 181 | }, 182 | { 183 | Continue([]byte("12,3")), 184 | "sysex.Continue len: 4", 185 | }, 186 | { 187 | End([]byte("12,3")), 188 | "sysex.End len: 4", 189 | }, 190 | { 191 | Escape([]byte{0xFF, 0xF2}), 192 | "sysex.Escape len: 2", 193 | }, 194 | } 195 | 196 | for _, test := range tests { 197 | 198 | var bf bytes.Buffer 199 | 200 | bf.WriteString(test.input.String()) 201 | 202 | if got, want := bf.String(), test.expected; got != want { 203 | t.Errorf("got: %#v; wanted %#v", got, want) 204 | } 205 | } 206 | 207 | } 208 | */ 209 | --------------------------------------------------------------------------------