├── .gitignore ├── COPYING ├── README.md ├── example.drumscript ├── go.mod ├── go.sum ├── instrument.go ├── main.go ├── math.go ├── parse.go ├── pattern.go ├── player.go └── song.go /.gitignore: -------------------------------------------------------------------------------- 1 | drumscript 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Thomas Preece 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drumscript 2 | 3 | A simple MIDI drum machine scripting language. 4 | 5 | drumscript triggers MIDI events based on scripted drum patterns; you need a MIDI instrument, sound card or software synth (such as TiMidity++) to hear the sounds. 6 | 7 | ## Building and Installation 8 | 9 | You need Go >= 1.16 and the ability to build CGo packages. You will also need the headers for libportmidi - on Ubuntu or similar you can install the package "libportmidi-dev". 10 | 11 | Then as usual: 12 | 13 | go mod tidy 14 | go build 15 | 16 | You can optionally copy the drumscript executable to be anywhere on your $PATH. 17 | 18 | The language is documented in the comments of example.drumscript, which is designed to be run with TiMidity++. Use the following commands: 19 | 20 | timidity -iA -B2,8 & 21 | ./drumscript example.drumscript 22 | 23 | -------------------------------------------------------------------------------- /example.drumscript: -------------------------------------------------------------------------------- 1 | # Example drumscript by Thomas Preece, July 2022 2 | # It is possible to make the script executable and start it with a shebang 3 | # such as #!/usr/bin/drumscript (or wherever you installed it) if you would like. 4 | 5 | # Set the default MIDI port for this script. You can see a list of MIDI ports 6 | # by running drumscript -l 7 | # You can override the port at runtime by using drumscript -p 8 | port TiMidity 9 | 10 | # Here we define some instruments. Each instrument is represented by a single 11 | # letter of the alphabet (other characters are not valid), which is used in the 12 | # drum patterns that we will define shortly. In each case we specify the MIDI 13 | # instrument number and optionally the velocity (which defaults to 127, the 14 | # maximum allowed). I've added a comment to show what each instrument is. 15 | # The following instrument numbers are based on the General MIDI standard; if 16 | # your MIDI instrument uses different numbers, use the relevant numbers for 17 | # that instrument. The output is always on MIDI channel 10, which is reserved 18 | # for percussion. 19 | instrument a 42 # closed hi hat 20 | instrument c 49 # crash cymbal 21 | instrument x 39 # hand clap 22 | instrument k 35 # bass drum (kick) 23 | instrument s 38 # snare drum 24 | 25 | # The instrument letters are case sensitive, allowing you to define two variants 26 | # of the same instrument as the same letter in different cases. 27 | instrument K 35 80 # kick drum with lower velocity 28 | 29 | # Now we will define some patterns. These are the basic building blocks of 30 | # our drum sequence, and they are later combined into songs that can be 31 | # played. The name of the pattern is case-sensitive and 32 | # must begin with a letter, but can have any other characters (except spaces) 33 | # after it. Within the pattern, each non-blank line represents a beat - in this 34 | # case, we're defining a simple backbeat of kick drum on 1 and 3 and snare drum 35 | # on 2 and 4. I have indented the content of each pattern to make it easier to 36 | # read, but this isn't necessary - you can put as much or as little whitespace 37 | # as you like. 38 | 39 | pattern backbeat 40 | k 41 | s 42 | k 43 | s 44 | end 45 | 46 | # Empty lines are ignored, so if you want a beat to be empty, put a single dot 47 | # on that line. Let's define claps on 2 and 4. The "end" keyword for a pattern 48 | # is actually optional, and usually I leave it out - by starting a new pattern 49 | # or a song (see later), the parser knows that the previous pattern has ended. 50 | 51 | pattern claps 52 | . 53 | x 54 | . 55 | x 56 | 57 | # You can include multiple drums on each beat. If you put multiple letters 58 | # together as a single term, then all of those instruments will trigger at 59 | # the same time. If you put multiple terms on a line, then they will trigger 60 | # at equally-spaced intervals throughout the beat. 61 | 62 | # This pattern triggers the snare and the crash cymbal together on 2 and 4: 63 | 64 | pattern snare-and-cymbal 65 | . 66 | sc 67 | . 68 | sc 69 | 70 | # And this pattern plays quavers (8th-notes) on the closed hi-hats: 71 | 72 | pattern hihats 73 | a a 74 | a a 75 | a a 76 | a a 77 | 78 | # You can use the dot within a sub-beat as well - and note that not all 79 | # beats have to have the same number of events. 80 | 81 | pattern claps2 82 | . 83 | x 84 | . x 85 | x x 86 | 87 | # Now let's start to put these patterns together to make a simple song. 88 | # If you don't specify a song name, it becomes the default song. This song 89 | # will play the backbeat over and over until we quit drumscript using ^C. 90 | # To listen to this example, run: drumscript example.drumscript 91 | 92 | song 93 | backbeat 94 | repeat 95 | 96 | # The default tempo is 120 bpm, but we can change that. To listen to this 97 | # example, run: drumscript example.drumscript fast 98 | 99 | song fast 100 | tempo 160 101 | backbeat 102 | repeat 103 | 104 | # It is also possible to change tempo during a song. 105 | 106 | song two-tempos 107 | tempo 160 108 | backbeat 109 | tempo 90 110 | backbeat 111 | repeat 112 | 113 | # We can specify multiple patterns in a song to be played in order. 114 | 115 | song two-patterns-in-order 116 | backbeat 117 | hihats 118 | repeat 119 | 120 | # We can also specify multiple patterns to be played at the same time. 121 | 122 | song two-patterns-together 123 | backbeat hihats 124 | repeat 125 | 126 | # You can also use the keyword "end" to terminate after playing a song, 127 | # or the keyword "chain" to move to a different song. 128 | 129 | song ends 130 | backbeat hihats 131 | end 132 | 133 | song chains 134 | claps 135 | chain ends 136 | 137 | # If you want to repeat a line a definite number of times, just put a 138 | # number at the start. 139 | 140 | song repeats 141 | 4 hihats claps 142 | end 143 | 144 | # A pattern doesn't have to be four beats long, of course. Let's define 145 | # a waltz beat: 146 | 147 | pattern waltz 148 | k 149 | s 150 | s 151 | 152 | song waltz 153 | waltz 154 | repeat 155 | 156 | # Note that if you trigger two patterns of different length at the same time, 157 | # it waits until all patterns have finished before moving on to the next line. 158 | 159 | song waltz-hihats 160 | waltz hihats 161 | repeat 162 | 163 | # Similarly you can define songs in compound time by having three subdivisions 164 | # of a beat. 165 | 166 | pattern six-eight 167 | ka sa sa 168 | ka sa c 169 | 170 | song six-eight 171 | tempo 90 172 | six-eight 173 | repeat 174 | 175 | # You can also play polyrhythms by playing two patterns at the same time that 176 | # have different numbers of subdivisions of the beat. 177 | 178 | pattern kick-three 179 | k k k 180 | 181 | pattern snare-four 182 | s s s s 183 | 184 | song three-against-four 185 | tempo 60 186 | kick-three snare-four 187 | repeat 188 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module drumscript 2 | 3 | go 1.18 4 | 5 | require gitlab.com/gomidi/midi/v2 v2.0.22 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gitlab.com/gomidi/midi/v2 v2.0.22 h1:PJ2s25rHGKmgEnB3NuO9hwkYBos4a18HVxKmANrCqtg= 2 | gitlab.com/gomidi/midi/v2 v2.0.22/go.mod h1:quTyMKSQ4Klevxu6gY4gy2USbeZra0fV5SalndmPfsY= 3 | -------------------------------------------------------------------------------- /instrument.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "gitlab.com/gomidi/midi/v2" 7 | "strconv" 8 | "unicode" 9 | ) 10 | 11 | const ( 12 | PercussionChannel uint8 = 9 // MIDI channel 10, but the midi library is 0-indexed 13 | DefaultVelocity uint64 = 127 14 | ) 15 | 16 | type Instrument struct { 17 | Letter rune 18 | On, Off midi.Message 19 | Sounding bool 20 | } 21 | 22 | var ( 23 | Instruments []*Instrument 24 | ) 25 | 26 | func createInstrument(fields []string) error { 27 | instrument := new(Instrument) 28 | 29 | if len(fields) < 3 { 30 | return errors.New("Incomplete instrument specification") 31 | } 32 | 33 | // check for a valid instrument letter 34 | if len(fields[1]) != 1 { 35 | return errors.New("Invalid instrument letter") 36 | } 37 | 38 | letter := ' ' 39 | for _, c := range fields[1] { 40 | letter = c 41 | break 42 | } 43 | 44 | if !unicode.IsLetter(letter) { 45 | return errors.New("Invalid instrument letter") 46 | } 47 | 48 | for i := range Instruments { 49 | if Instruments[i].Letter == letter { 50 | return errors.New("Repeated instrument letter") 51 | } 52 | } 53 | 54 | instrument.Letter = letter 55 | 56 | // check for a valid instrument number 57 | number, err := strconv.ParseUint(fields[2], 10, 8) 58 | if err != nil { 59 | return errors.New("Invalid instrument number") 60 | } 61 | 62 | // check for a valid velocity 63 | velocity := DefaultVelocity 64 | if len(fields) >= 4 { 65 | velocity, err = strconv.ParseUint(fields[3], 10, 8) 66 | if err != nil { 67 | return errors.New("Invalid velocity") 68 | } 69 | } 70 | 71 | instrument.On = midi.NoteOn(PercussionChannel, uint8(number), uint8(velocity)) 72 | instrument.Off = midi.NoteOff(PercussionChannel, uint8(number)) 73 | 74 | Instruments = append(Instruments, instrument) 75 | 76 | return nil 77 | } 78 | 79 | func getInstrumentIndex(letter rune) (int, error) { 80 | for i := range Instruments { 81 | if Instruments[i].Letter == letter { 82 | return i, nil 83 | } 84 | } 85 | 86 | return -1, errors.New(fmt.Sprintf("Instrument %s not defined", string(letter))) 87 | } 88 | 89 | func Trigger(index int) { 90 | if !Instruments[index].Sounding { 91 | send(Instruments[index].On) 92 | Instruments[index].Sounding = true 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "gitlab.com/gomidi/midi/v2" 7 | _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" // autoregisters driver 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | func main() { 14 | defer midi.CloseDriver() 15 | 16 | portPtr := flag.String("p", "", "Override the port set in the script") 17 | listPtr := flag.Bool("l", false, "List the available MIDI ports") 18 | flag.Parse() 19 | 20 | if *listPtr { 21 | listPorts() 22 | return 23 | } 24 | 25 | OverridePortName = *portPtr 26 | if OverridePortName != "" { 27 | setPort([]string{}) 28 | } 29 | 30 | fname := "" 31 | songname := "" 32 | 33 | args := flag.Args() 34 | switch len(args) { 35 | case 0: 36 | fmt.Printf("Usage: %s [-p port] filename [song]\n", os.Args[0]) 37 | case 1: 38 | fname = args[0] 39 | case 2: 40 | fname = args[0] 41 | songname = args[1] 42 | } 43 | 44 | err := parseScript(fname) 45 | if err != nil { 46 | fmt.Println(err) 47 | return 48 | } 49 | 50 | if !PortOpen { 51 | fmt.Println("No MIDI port opened") 52 | return 53 | } 54 | 55 | songIndex, err := getSongIndex(songname) 56 | if err != nil { 57 | if songname == "" { 58 | fmt.Println("No default song found") 59 | } else { 60 | fmt.Printf("Song %s not found\n", songname) 61 | } 62 | return 63 | } 64 | 65 | if err := playSong(songIndex); err != nil { 66 | fmt.Println(err.Error()) 67 | return 68 | } 69 | 70 | sc := make(chan os.Signal, 1) 71 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 72 | 73 | select { 74 | case <-sc: 75 | case <-stopped: 76 | } 77 | 78 | stopNotes() 79 | done <- true 80 | } 81 | -------------------------------------------------------------------------------- /math.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // greatest common divisor (GCD) via Euclidean algorithm 4 | func GCD(a, b int) int { 5 | for b != 0 { 6 | t := b 7 | b = a % b 8 | a = t 9 | } 10 | return a 11 | } 12 | 13 | // find Least Common Multiple (LCM) via GCD 14 | func LCM(a, b int, integers ...int) int { 15 | result := a * b / GCD(a, b) 16 | 17 | for i := 0; i < len(integers); i++ { 18 | result = LCM(result, integers[i]) 19 | } 20 | 21 | return result 22 | } 23 | 24 | func LeastCommonMultiple(integers []int) int { 25 | switch len(integers) { 26 | case 0: 27 | return 0 28 | case 1: 29 | return integers[0] 30 | case 2: 31 | return LCM(integers[0], integers[1]) 32 | default: 33 | return LCM(integers[0], integers[1], integers[2:]...) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type parseStateState int 12 | 13 | const ( 14 | BaseState parseStateState = iota 15 | PatternState 16 | SongState 17 | ) 18 | 19 | type parseState struct { 20 | State parseStateState 21 | Tempo uint16 22 | Pattern *Pattern 23 | Song *Song 24 | } 25 | 26 | func parseScript(fname string) error { 27 | state := new(parseState) 28 | state.Pattern = new(Pattern) 29 | state.Song = new(Song) 30 | 31 | file, err := os.Open(fname) 32 | if err != nil { 33 | return err 34 | } 35 | defer file.Close() 36 | 37 | scanner := bufio.NewScanner(file) 38 | lineNumber := 0 39 | for scanner.Scan() { 40 | lineNumber++ 41 | 42 | // remove comments and leading/trailing whitespace 43 | line := strings.TrimSpace(strings.SplitN(scanner.Text(), "#", 2)[0]) 44 | fields := strings.Fields(line) 45 | if len(fields) < 1 { 46 | continue 47 | } 48 | 49 | err = parseFields(fields, state) 50 | if err != nil { 51 | return errors.New(fmt.Sprintf("%s on line %d", err.Error(), lineNumber)) 52 | } 53 | } 54 | 55 | switch state.State { 56 | case PatternState: 57 | endParsePattern(state) 58 | case SongState: 59 | endParseSong(state) 60 | } 61 | 62 | if err := scanner.Err(); err != nil { 63 | return err 64 | } 65 | 66 | convertPatterns() 67 | if err := convertSongs(); err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func parseFields(fields []string, state *parseState) error { 75 | switch state.State { 76 | case BaseState: 77 | switch fields[0] { 78 | case "port": 79 | return setPort(fields) 80 | case "instrument": 81 | return createInstrument(fields) 82 | case "pattern": 83 | return startParsePattern(fields, state) 84 | case "song": 85 | return startParseSong(fields, state) 86 | default: 87 | return errors.New("Invalid command") 88 | } 89 | case PatternState: 90 | switch fields[0] { 91 | case "end": 92 | endParsePattern(state) 93 | return nil 94 | case "pattern": 95 | endParsePattern(state) 96 | return startParsePattern(fields, state) 97 | case "song": 98 | endParsePattern(state) 99 | return startParseSong(fields, state) 100 | default: 101 | return parsePatternLine(fields, state) 102 | } 103 | case SongState: 104 | switch fields[0] { 105 | case "end": 106 | endParseSong(state) 107 | return nil 108 | case "pattern": 109 | endParseSong(state) 110 | return startParsePattern(fields, state) 111 | case "song": 112 | endParseSong(state) 113 | return startParseSong(fields, state) 114 | case "tempo": 115 | return parseSongTempo(fields, state) 116 | case "repeat": 117 | return parseSongRepeat(fields, state) 118 | case "chain": 119 | return parseSongChain(fields, state) 120 | default: 121 | return parseSongLine(fields, state) 122 | } 123 | default: 124 | return errors.New("Invalid parse state") 125 | } 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /pattern.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "unicode" 7 | ) 8 | 9 | type PatternEvent struct { 10 | Instruments []int 11 | } 12 | 13 | type PatternBeat struct { 14 | Events []PatternEvent 15 | } 16 | 17 | type Pattern struct { 18 | Name string 19 | Ready, Active bool 20 | ActiveEvent int 21 | Beats []PatternBeat 22 | Events []PatternEvent 23 | } 24 | 25 | var ( 26 | Patterns []Pattern 27 | EventsPerBeat int 28 | NoOpEvent PatternEvent 29 | ) 30 | 31 | func startParsePattern(fields []string, state *parseState) error { 32 | if state.State != BaseState { 33 | return errors.New("Can't start a new pattern") 34 | } 35 | 36 | if len(fields) != 2 { 37 | return errors.New("Invalid pattern name") 38 | } 39 | 40 | name := fields[1] 41 | 42 | // first character must be a letter 43 | for _, r := range name { 44 | if !unicode.IsLetter(r) { 45 | return errors.New("Invalid pattern name") 46 | } 47 | break 48 | } 49 | 50 | state.State = PatternState 51 | state.Pattern.Name = name 52 | 53 | return nil 54 | } 55 | 56 | func endParsePattern(state *parseState) { 57 | Patterns = append(Patterns, *state.Pattern) 58 | state.State = BaseState 59 | state.Pattern = new(Pattern) 60 | } 61 | 62 | func parsePatternLine(fields []string, state *parseState) error { 63 | var b PatternBeat 64 | for _, f := range fields { 65 | var e PatternEvent 66 | 67 | if f == "." { 68 | b.Events = append(b.Events, NoOpEvent) 69 | continue 70 | } 71 | 72 | for _, r := range f { 73 | instrument, err := getInstrumentIndex(r) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | e.Instruments = append(e.Instruments, instrument) 79 | } 80 | 81 | b.Events = append(b.Events, e) 82 | } 83 | state.Pattern.Beats = append(state.Pattern.Beats, b) 84 | return nil 85 | } 86 | 87 | func getPatternIndex(name string) (int, error) { 88 | for i := range Patterns { 89 | if Patterns[i].Name == name { 90 | return i, nil 91 | } 92 | } 93 | 94 | return -1, errors.New(fmt.Sprintf("Pattern %s not defined", name)) 95 | } 96 | 97 | func convertPatterns() { 98 | // for every defined beat, find the number of events in that beat 99 | eventsPerBeat := make(map[int]bool) 100 | for i := range Patterns { 101 | for j := range Patterns[i].Beats { 102 | eventsThisBeat := len(Patterns[i].Beats[j].Events) 103 | eventsPerBeat[eventsThisBeat] = true 104 | } 105 | } 106 | 107 | // find the lowest common multiple of all the events per beat 108 | var eventCounts []int 109 | for i := range eventsPerBeat { 110 | if eventsPerBeat[i] == true { 111 | eventCounts = append(eventCounts, i) 112 | } 113 | } 114 | 115 | EventsPerBeat = LeastCommonMultiple(eventCounts) 116 | 117 | // now add the relevant number of NoOpEvents to every beat 118 | for i := range Patterns { 119 | for j := range Patterns[i].Beats { 120 | noOpsPerEvent := (EventsPerBeat / len(Patterns[i].Beats[j].Events)) - 1 121 | for _, e := range Patterns[i].Beats[j].Events { 122 | Patterns[i].Events = append(Patterns[i].Events, e) 123 | for k := 0; k < noOpsPerEvent; k++ { 124 | Patterns[i].Events = append(Patterns[i].Events, NoOpEvent) 125 | } 126 | } 127 | } 128 | 129 | Patterns[i].Ready = true 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /player.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "gitlab.com/gomidi/midi/v2" 7 | "time" 8 | ) 9 | 10 | var ( 11 | send func(msg midi.Message) error 12 | OverridePortName string 13 | PortOpen bool 14 | Tempo uint16 15 | ActiveSong, ActiveSongEvent int 16 | ActivePatterns []int 17 | ticker *time.Ticker 18 | done = make(chan bool) 19 | stopped = make(chan bool) 20 | eventLoopRunning bool 21 | ) 22 | 23 | const ( 24 | DefaultTempo uint16 = 120 25 | ) 26 | 27 | func listPorts() { 28 | fmt.Println("Available MIDI ports:\n" + midi.GetOutPorts().String()) 29 | } 30 | 31 | func setPort(fields []string) error { 32 | portname := OverridePortName 33 | 34 | if OverridePortName == "" { 35 | if len(fields) != 2 { 36 | return errors.New("Invalid port name") 37 | } 38 | portname = fields[1] 39 | } 40 | 41 | out, err := midi.FindOutPort(portname) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | send, err = midi.SendTo(out) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | PortOpen = true 52 | 53 | return nil 54 | } 55 | 56 | func eventLoop() { 57 | if eventLoopRunning { 58 | return 59 | } 60 | 61 | go func() { 62 | eventLoopRunning = true 63 | onOff := true 64 | for { 65 | select { 66 | case <-done: 67 | return 68 | case <-ticker.C: 69 | if onOff { 70 | stopNotes() 71 | } else { 72 | nextEvent() 73 | } 74 | onOff = !onOff 75 | } 76 | } 77 | eventLoopRunning = false 78 | }() 79 | } 80 | 81 | func playSong(songIndex int) error { 82 | song := &Songs[songIndex] 83 | 84 | // add a tempo to the first event if necessary 85 | if Tempo == 0 { 86 | if len(song.Events) == 0 { 87 | return errors.New("Song has no events") 88 | } else { 89 | if song.Events[0].Tempo == 0 { 90 | song.Events[0].Tempo = DefaultTempo 91 | } 92 | } 93 | } 94 | 95 | ActiveSong = songIndex 96 | ActiveSongEvent = -1 97 | ActivePatterns = make([]int, 0) 98 | nextEvent() 99 | 100 | return nil 101 | } 102 | 103 | func setTempo(tempo uint16) { 104 | Tempo = tempo 105 | tickDuration := time.Minute / time.Duration(int(tempo)*EventsPerBeat*2) 106 | ticker = time.NewTicker(tickDuration) 107 | eventLoop() 108 | } 109 | 110 | func nextEvent() { 111 | var newActivePatterns []int 112 | for _, i := range ActivePatterns { 113 | if Patterns[i].ActiveEvent < len(Patterns[i].Events) { 114 | for j := range Patterns[i].Events[Patterns[i].ActiveEvent].Instruments { 115 | Trigger(Patterns[i].Events[Patterns[i].ActiveEvent].Instruments[j]) 116 | } 117 | } 118 | Patterns[i].ActiveEvent++ 119 | if Patterns[i].ActiveEvent < len(Patterns[i].Events) { 120 | newActivePatterns = append(newActivePatterns, i) 121 | } 122 | } 123 | 124 | ActivePatterns = newActivePatterns 125 | 126 | if len(ActivePatterns) > 0 { 127 | return 128 | } 129 | 130 | ActiveSongEvent++ 131 | if ActiveSongEvent >= len(Songs[ActiveSong].Events) { 132 | stopped <- true 133 | return 134 | } 135 | 136 | if Songs[ActiveSong].Events[ActiveSongEvent].Repeat { 137 | ActiveSongEvent = -1 138 | nextEvent() 139 | return 140 | } 141 | 142 | if Songs[ActiveSong].Events[ActiveSongEvent].Chain { 143 | ActiveSong = Songs[ActiveSong].Events[ActiveSongEvent].ChainIndex 144 | ActiveSongEvent = -1 145 | nextEvent() 146 | return 147 | } 148 | 149 | if Songs[ActiveSong].Events[ActiveSongEvent].Tempo != 0 { 150 | setTempo(Songs[ActiveSong].Events[ActiveSongEvent].Tempo) 151 | } 152 | 153 | for i := range Songs[ActiveSong].Events[ActiveSongEvent].Patterns { 154 | activatePattern(Songs[ActiveSong].Events[ActiveSongEvent].Patterns[i]) 155 | } 156 | } 157 | 158 | func activatePattern(index int) { 159 | Patterns[index].ActiveEvent = 0 160 | ActivePatterns = append(ActivePatterns, index) 161 | } 162 | 163 | func stopNotes() { 164 | for i := range Instruments { 165 | if Instruments[i].Sounding { 166 | send(Instruments[i].Off) 167 | Instruments[i].Sounding = false 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /song.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "unicode" 8 | ) 9 | 10 | type songEvent struct { 11 | Repeat, Chain bool 12 | Tempo uint16 13 | ChainName string 14 | ChainIndex int 15 | Patterns []int 16 | } 17 | 18 | type Song struct { 19 | Name string 20 | Events []songEvent 21 | } 22 | 23 | var Songs []Song 24 | 25 | func startParseSong(fields []string, state *parseState) error { 26 | if state.State != BaseState { 27 | return errors.New("Can't start a new song") 28 | } 29 | 30 | name := "" 31 | switch len(fields) { 32 | case 1: 33 | // no name => default song - do nothing 34 | case 2: 35 | // song name specified 36 | name = fields[1] 37 | for _, r := range name { 38 | if !unicode.IsLetter(r) { 39 | return errors.New("Invalid song name") 40 | } 41 | break 42 | } 43 | default: 44 | return errors.New("Invalid song name") 45 | } 46 | 47 | // check to see if we already have a song with this name 48 | for i := range Songs { 49 | if Songs[i].Name == name { 50 | return errors.New("Repeated song name") 51 | } 52 | } 53 | 54 | state.State = SongState 55 | state.Song.Name = name 56 | 57 | return nil 58 | } 59 | 60 | func endParseSong(state *parseState) { 61 | Songs = append(Songs, *state.Song) 62 | state.State = BaseState 63 | state.Song = new(Song) 64 | } 65 | 66 | func parseSongTempo(fields []string, state *parseState) error { 67 | if len(fields) != 2 { 68 | return errors.New("Invalid tempo") 69 | } 70 | 71 | tempo, err := strconv.ParseUint(fields[1], 10, 16) 72 | if err != nil { 73 | return errors.New("Invalid tempo") 74 | } 75 | 76 | if tempo == 0 { 77 | return errors.New("Invalid tempo") 78 | } 79 | 80 | state.Tempo = uint16(tempo) 81 | return nil 82 | } 83 | 84 | func parseSongRepeat(fields []string, state *parseState) error { 85 | if len(fields) != 1 { 86 | return errors.New("Invalid repeat command") 87 | } 88 | 89 | var e songEvent 90 | if state.Tempo != 0 { 91 | e.Tempo = state.Tempo 92 | state.Tempo = 0 93 | } 94 | 95 | e.Repeat = true 96 | state.Song.Events = append(state.Song.Events, e) 97 | 98 | endParseSong(state) 99 | 100 | return nil 101 | } 102 | 103 | func parseSongChain(fields []string, state *parseState) error { 104 | if len(fields) != 2 { 105 | return errors.New("Invalid chain command") 106 | } 107 | 108 | name := fields[1] 109 | for _, r := range name { 110 | if !unicode.IsLetter(r) { 111 | return errors.New("Invalid song name in chain") 112 | } 113 | } 114 | 115 | var e songEvent 116 | if state.Tempo != 0 { 117 | e.Tempo = state.Tempo 118 | state.Tempo = 0 119 | } 120 | 121 | e.Chain = true 122 | e.ChainName = name 123 | state.Song.Events = append(state.Song.Events, e) 124 | endParseSong(state) 125 | 126 | return nil 127 | } 128 | 129 | func parseSongLine(fields []string, state *parseState) error { 130 | var ( 131 | e songEvent 132 | err error 133 | ) 134 | 135 | if state.Tempo != 0 { 136 | e.Tempo = state.Tempo 137 | state.Tempo = 0 138 | } 139 | 140 | first := true 141 | count := 1 142 | for _, f := range fields { 143 | // for the first field, it count be a count 144 | if first { 145 | first = false 146 | count, err = strconv.Atoi(f) 147 | if err == nil { 148 | if count < 1 { 149 | return errors.New("Invalid count") 150 | } 151 | 152 | // we found a count - continue to the next field 153 | continue 154 | } else { 155 | count = 1 156 | } 157 | } 158 | 159 | // for all the remaining fields, it must be a pattern name 160 | patternIndex, err := getPatternIndex(f) 161 | if err != nil { 162 | return errors.New(fmt.Sprintf("Undefined pattern %s", f)) 163 | } 164 | e.Patterns = append(e.Patterns, patternIndex) 165 | } 166 | 167 | if len(e.Patterns) == 0 { 168 | return errors.New("No patterns") 169 | } 170 | 171 | for i := 0; i < count; i++ { 172 | state.Song.Events = append(state.Song.Events, e) 173 | } 174 | 175 | return nil 176 | } 177 | 178 | func getSongIndex(name string) (int, error) { 179 | for i := range Songs { 180 | if Songs[i].Name == name { 181 | return i, nil 182 | } 183 | } 184 | 185 | return -1, errors.New(fmt.Sprintf("Pattern %s not defined", name)) 186 | } 187 | 188 | func convertSongs() error { 189 | var err error 190 | for i := range Songs { 191 | for j := range Songs[i].Events { 192 | if Songs[i].Events[j].Chain { 193 | name := Songs[i].Events[j].ChainName 194 | Songs[i].Events[j].ChainIndex, err = getSongIndex(name) 195 | if err != nil { 196 | errors.New(fmt.Sprintf("Undefined song to chain %s", name)) 197 | } 198 | } 199 | } 200 | } 201 | 202 | return nil 203 | } 204 | --------------------------------------------------------------------------------