├── .git-blame-ignore-revs ├── .github └── workflows │ └── scala.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── resources ├── testmidifiles │ ├── MIDI_sample.mid │ └── filefornegativetest └── testmusicxmlfiles │ ├── filefornegativetest │ └── musicXMLTest.xml └── src ├── main ├── antlr4 │ ├── Modulo12Lexer.g4 │ └── Modulo12Parser.g4 └── scala │ └── org │ └── modulo12 │ ├── Main.scala │ ├── core │ ├── MusicFileParser.scala │ ├── SongEvaluator.scala │ ├── SongMetadataEvaluator.scala │ ├── SongParserListener.scala │ └── models │ │ ├── operator.scala │ │ ├── songmodel.scala │ │ ├── songparsemodel.scala │ │ └── sqlmodel.scala │ ├── midi │ └── MidiParser.scala │ ├── musicxml │ └── MusicXMLParser.scala │ └── sql │ ├── SqlParser.scala │ ├── SqlParserErrorListener.scala │ └── SqlVisitor.scala └── test └── scala └── org └── modulo12 ├── core └── SongModelSpec.scala ├── midi └── MidiParserSpec.scala ├── musicxml └── MusicXMLParserSpec.scala └── parser ├── BinaryLogicalOperationSqlSpec.scala ├── InstrumentQuerySqlSpec.scala ├── InvalidQuerySqlSpec.scala ├── KeySignatureSqlSpec.scala ├── SelectClauseSqlSpec.scala └── simpleexpression ├── LyricsComparsionSqlSpec.scala ├── NumBarsComparisonSpec.scala ├── NumTracksComparsionSpec.scala └── TempoComparisonSqlSpec.scala /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 2.5.3 2 | 47ac31b2900080d8b908948eb3052e0e16f7e557 3 | 4 | # Scala Steward: Reformat with scalafmt 2.7.5 5 | 26e35e8ab1b13dbce6cab113be4772eb7b49dea1 6 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Modulo12 CI 2 | 3 | on: 4 | push: 5 | branches: [ mainline ] 6 | pull_request: 7 | branches: [ mainline ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 14 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 14 20 | - name: Run tests 21 | run: sbt test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dotty-ide-artifact 2 | .dotty-ide.json 3 | 4 | DS_Store 5 | *.class 6 | *.log 7 | *~ 8 | 9 | # sbt specific 10 | dist/* 11 | target/ 12 | lib_managed/ 13 | src_managed/ 14 | project/boot/ 15 | project/plugins/project/ 16 | project/local-plugins.sbt 17 | .history 18 | 19 | # Scala-IDE specific 20 | .scala_dependencies 21 | .cache 22 | .classpath 23 | .project 24 | .settings 25 | classes/ 26 | 27 | # idea 28 | .idea 29 | .idea_modules 30 | /.worksheet/ 31 | gen/ 32 | .bsp 33 | src/main/antlr4/Modulo12Lexer.tokens 34 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.6.1 2 | style = defaultWithAlign 3 | project.git = true 4 | maxColumn = 120 5 | unindentTopLevelOperators = true 6 | danglingParentheses = true 7 | spaces.inImportCurlyBraces = true 8 | 9 | rewrite.rules = [ RedundantBraces, SortImports ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modulo12 2 | 3 | [![Build Status](https://github.com/Khalian/Modulo12/workflows/Modulo12%20CI/badge.svg)](https://github.com/Khalian/Modulo12/actions?query=workflow%3A"Modulo12+CI") [![Join the chat at https://gitter.im/Khalian/Modulo12](https://badges.gitter.im/Khalian/Modulo12.svg)](https://gitter.im/Khalian/Modulo12?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | 6 | Modulo12 is a novel SQL like language for parsing data and metadata about music (from midi and musicxml files). 7 | 8 | The purpose of this language is to query facts about musicxml and midi files based of western music theory (e.g. queries of the form, get all the songs under a directory with a specific key signature or in tempo range) or metadata about the song's specific performance (get all songs that have guitars in it) 9 | 10 | The way it works is you put your songs under a directory and then run a query of this form 11 | 12 | ``` 13 | select [input_file_types] from directory_containing_songs where (conditions); 14 | ``` 15 | 16 | Modulo12 will fetch all songs recursively under the directory and apply some conditions and then output the list of songs that match the conditions. 17 | 18 | # Wiki 19 | 20 | The full [wiki](https://github.com/Khalian/Modulo12/wiki) contains the language spec, philosophy and technical architecture of the project. 21 | 22 | # Examples 23 | 24 | Some example commands for querying songs would be like 25 | 26 | ``` 27 | select * from directory_of_songs_path where tempo = 120 and song has lyrics friday; 28 | ``` 29 | 30 | ``` 31 | select midi from directory_of_songs_path where song has instrument piano; 32 | ``` 33 | 34 | ``` 35 | # NumBarLines is a function for the number of bars the song is subdivided into 36 | select musicxml from directory_of_songs_path where numbarlines < 40; 37 | ``` 38 | 39 | ``` 40 | select midi from directory_of_songs_path where key = Eb and tempo > 150; 41 | ``` 42 | 43 | # Development 44 | 45 | ## Pre requisites 46 | 47 | Install [SBT](https://www.scala-sbt.org/1.x/docs/Setup.html) 48 | 49 | ## To clone the project 50 | 51 | ``` 52 | git clone https://github.com/Khalian/Modulo12 53 | ``` 54 | 55 | ## For setting up and running sbt targets 56 | 57 | ``` 58 | # Start modulo12 sql repl 59 | sbt run 60 | 61 | # Clean up dependency cache 62 | sbt clean 63 | 64 | # SBT Compile 65 | sbt compile 66 | 67 | # Run tests 68 | sbt test 69 | 70 | # Format files 71 | sbt scalafmt 72 | 73 | # SBT repl server (once you start this you can just write test instead of full sbt and it will run in repl) 74 | sbt 75 | 76 | # SBT debug server (Note the 5005 is the remote jvm debug port. You can use your ide to hook to it and then debug tests by running test) 77 | sbt -jvm-debug 5005 78 | ``` 79 | 80 | ## IDE Setup 81 | 82 | ### IntelliJ 83 | 84 | Install the following plugins for IntelliJ 85 | 86 | 1. [Scala](https://www.jetbrains.com/help/idea/discover-intellij-idea-for-scala.html) 87 | 2. [Antlr](https://plugins.jetbrains.com/plugin/7358-antlr-v4) 88 | 89 | You can then setup it up either as a VCS project from github or as a scala project after you clone it out. 90 | 91 | ### VSCode 92 | 93 | ``` 94 | # This command launches VS Code for development 95 | sbt launchIDE 96 | ``` 97 | 98 | Please also search the VSCode market place to install the ANTLR4 grammar syntax support plugin 99 | 100 | ### Vim 101 | 102 | Vim is not really an IDE but you can use it for editing, you should install metals along with scala vim syntax highlighting 103 | 104 | [Metals](https://scalameta.org/metals/docs/editors/vim.html) 105 | 106 | [Scala Vim Syntax](https://www.vim.org/scripts/script.php?script_id=3524) 107 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = project 2 | .in(file(".")) 3 | .settings( 4 | name := "Modulo12", 5 | version := "0.1.0", 6 | // Extra Maven repositories 7 | resolvers += "JFugue Repository" at "https://maven.sing-group.org/repository/maven/", 8 | libraryDependencies += "jfugue" % "jfugue" % "5.0.9", 9 | libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.6", 10 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.5" % "test", 11 | libraryDependencies += "xom" % "xom" % "1.3.8", 12 | libraryDependencies += "org.jline" % "jline" % "3.21.0", 13 | scalaVersion := "3.0.0-RC1" 14 | ) 15 | 16 | enablePlugins(Antlr4Plugin) 17 | antlr4PackageName in Antlr4 := Some("org.modulo12") 18 | 19 | // We wish to use the visitor pattern since we are going to use scala pattern matching for writing the compiler 20 | // We wish to use the visitor pattern since we are going to use 21 | // scala pattern matching for writing parts of the evaluation 22 | // Moreover we need to do this using immutables, so no listeners 23 | antlr4GenListener in Antlr4 := false 24 | antlr4GenVisitor in Antlr4 := true 25 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.4.9 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") 2 | addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.5") 3 | addSbtPlugin("com.simplytyped" % "sbt-antlr4" % "0.8.3") 4 | -------------------------------------------------------------------------------- /resources/testmidifiles/MIDI_sample.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khalian/Modulo12/21595e8971fbdfc25ea4fcf6bb637177c2ef33cc/resources/testmidifiles/MIDI_sample.mid -------------------------------------------------------------------------------- /resources/testmidifiles/filefornegativetest: -------------------------------------------------------------------------------- 1 | some random garbage here for negative testing -------------------------------------------------------------------------------- /resources/testmusicxmlfiles/filefornegativetest: -------------------------------------------------------------------------------- 1 | some random garbage here for negative testing -------------------------------------------------------------------------------- /resources/testmusicxmlfiles/musicXMLTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | Voice 9 | 10 | 11 | 12 | 13 | 14 | 4 15 | 16 | -3 17 | major 18 | 19 | 23 | 24 | G 25 | 2 26 | 27 | Langsam, innig. 28 | 29 | 30 | 31 | G 32 | 4 33 | 34 | 2 35 | eighth 36 | up 37 | 38 | 39 |

40 | 41 | 42 | 43 | single 44 | something 45 | 46 | 47 | 48 | 49 | 50 | 51 | F 52 | 4 53 | 54 | 3 55 | eighth 56 | 57 | up 58 | 59 | single 60 | du 61 | 62 | 63 | 64 | 65 | E 66 | -1 67 | 4 68 | 69 | 1 70 | 16th 71 | up 72 | 73 | single 74 | nicht, 75 | 76 | 77 | 78 | 79 | E 80 | -1 81 | 4 82 | 83 | 2 84 | eighth 85 | up 86 | 87 | begin 88 | heil 89 | 90 | 91 | 92 | 93 | B 94 | -1 95 | 4 96 | 97 | 1 98 | 16th 99 | up 100 | begin 101 | begin 102 | 103 | 104 | 105 | 106 | end 107 | ger 108 | 109 | 110 | 111 | 112 | 113 | G 114 | 4 115 | 116 | 1 117 | 16th 118 | up 119 | end 120 | end 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/main/antlr4/Modulo12Lexer.g4: -------------------------------------------------------------------------------- 1 | lexer grammar Modulo12Lexer; 2 | 3 | A : [aA]; 4 | B : [bB]; 5 | C : [cC]; 6 | D : [dD]; 7 | E : [eE]; 8 | F : [fF]; 9 | G : [gG]; 10 | H : [hH]; 11 | I : [iI]; 12 | J : [jJ]; 13 | K : [kK]; 14 | L : [lL]; 15 | M : [mM]; 16 | N : [nN]; 17 | O : [oO]; 18 | P : [pP]; 19 | Q : [qQ]; 20 | R : [rR]; 21 | S : [sS]; 22 | T : [tT]; 23 | U : [uU]; 24 | V : [vV]; 25 | W : [wW]; 26 | X : [xX]; 27 | Y : [yY]; 28 | Z : [zZ]; 29 | 30 | // Conditions 31 | EQ: '='; 32 | LEQ: '<='; 33 | GEQ: '>='; 34 | NEQ : '!='; 35 | LT: '<'; 36 | GT: '>'; 37 | HAS: H A S; 38 | 39 | // Logical operations 40 | AND: A N D; 41 | OR : O R; 42 | 43 | // Key words 44 | SELECT: S E L E C T; 45 | MIDI: M I D I; 46 | MUSICXML: M U S I C X M L; 47 | WILDCARD: '*'; 48 | FROM: F R O M; 49 | WHERE: W H E R E; 50 | SEMI: ';'; 51 | COMMA: ','; 52 | 53 | // Song metadata factors 54 | KEY : K E Y; 55 | SHARP: '#'; 56 | FLAT: 'b'; 57 | // Keys with qualifiers, TODO: Simplify this somehow 58 | FSHARP : F SHARP; 59 | CSHARP : C SHARP; 60 | BFLAT : B FLAT; 61 | EFLAT : E FLAT; 62 | AFLAT : A FLAT; 63 | DFLAT : D FLAT; 64 | GFLAT : G FLAT; 65 | CFLAT : C FLAT; 66 | 67 | SCALE: S C A L E; 68 | SCALE_TYPE : M I N O R | M A J O R; 69 | SONG: S O N G; 70 | INSTRUMENT: I N S T R U M E N T; 71 | LYRICS: L Y R I C S; 72 | TEMPO: T E M P O; 73 | NUMBARLINES: N U M B A R L I N E S; 74 | NUMTRACKS : N U M T R A C K S; 75 | 76 | // Generic definitions 77 | NUMBER: ('0' .. '9') + ('.' ('0' .. '9') +)?; 78 | ID: ('a'..'z' | 'A' .. 'Z' | '_' | '/' | '0' .. '9')+ ; 79 | NEWLINE: '\r' ? '\n' -> skip; 80 | WS: (' ' | '\t' | '\n' | '\r')+ -> skip; -------------------------------------------------------------------------------- /src/main/antlr4/Modulo12Parser.g4: -------------------------------------------------------------------------------- 1 | parser grammar Modulo12Parser; 2 | 3 | options { 4 | tokenVocab = Modulo12Lexer; 5 | 6 | // antlr will generate java lexer and parser 7 | language = Java; 8 | } 9 | 10 | sql_statement: 11 | select_key 12 | input_list_clause 13 | from_clause 14 | (where_clause)? 15 | SEMI 16 | ; 17 | 18 | input_list_clause: 19 | input_name (COMMA input_name)* 20 | ; 21 | 22 | input_name: 23 | MIDI | MUSICXML | WILDCARD 24 | ; 25 | 26 | select_key: 27 | SELECT 28 | ; 29 | 30 | from_clause: 31 | FROM directory_name 32 | ; 33 | 34 | where_clause: 35 | WHERE expression 36 | ; 37 | 38 | logical_op: 39 | AND | OR 40 | ; 41 | 42 | expression: 43 | expression logical_op expression 44 | | simple_expression 45 | ; 46 | 47 | simple_expression: 48 | scale_comparison 49 | | key_comparison 50 | | song_has_instrument 51 | | tempo_comparison 52 | | num_barlines_comparision 53 | | num_tracks_comparision 54 | | lyrics_comparison 55 | ; 56 | 57 | lyrics_comparison: 58 | SONG HAS LYRICS words 59 | ; 60 | 61 | num_barlines_comparision: 62 | NUMBARLINES relational_op NUMBER 63 | ; 64 | 65 | num_tracks_comparision: 66 | NUMTRACKS relational_op NUMBER 67 | ; 68 | 69 | tempo_comparison: 70 | TEMPO relational_op NUMBER 71 | ; 72 | 73 | scale_comparison: 74 | SCALE EQ SCALE_TYPE 75 | ; 76 | 77 | key_comparison: 78 | KEY EQ key_type 79 | ; 80 | 81 | key_type: 82 | C | G | D | A | E | B | FSHARP | CSHARP | F | BFLAT | EFLAT | AFLAT | DFLAT | GFLAT | CFLAT 83 | ; 84 | 85 | relational_op: 86 | EQ | LEQ | GEQ | LT | GT | NEQ 87 | ; 88 | 89 | song_has_instrument: 90 | SONG HAS INSTRUMENT ID 91 | ; 92 | 93 | words: 94 | word (COMMA word)* 95 | ; 96 | 97 | word: 98 | ID 99 | ; 100 | 101 | directory_name: 102 | ID 103 | ; -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/Main.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12 2 | 3 | import org.jline.reader.{ EndOfFileException, LineReaderBuilder, UserInterruptException } 4 | import org.jline.terminal.{ Terminal, TerminalBuilder } 5 | import org.jline.utils.{ AttributedStringBuilder, AttributedStyle } 6 | import org.modulo12.core.SongEvaluator 7 | import org.modulo12.core.models.{ InvalidQueryException, Song } 8 | import org.modulo12.midi.MidiParser 9 | import org.modulo12.musicxml.MusicXMLParser 10 | import org.modulo12.sql.SqlParser 11 | 12 | import scala.annotation.tailrec 13 | import scala.sys.exit 14 | import scala.util.{ Failure, Success, Try } 15 | 16 | object Main { 17 | val PROMPT = new AttributedStringBuilder() 18 | .style(AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN)) 19 | .append("modulo12-sql> ") 20 | .toAnsi() 21 | val TERMINAL = TerminalBuilder.builder.build() 22 | TERMINAL.handle(Terminal.Signal.INT, Terminal.SignalHandler.SIG_IGN) 23 | val READER = LineReaderBuilder.builder().terminal(TERMINAL).build(); 24 | 25 | def main(args: Array[String]): Unit = 26 | repl() 27 | 28 | @tailrec 29 | private def repl(): Unit = { 30 | Try { 31 | val query = READER.readLine(PROMPT) 32 | val songsSatisfyingQuery = sqlEval(query) 33 | songsSatisfyingQuery.foreach(song => println(song)) 34 | } match { 35 | case Success(_) => 36 | case Failure(InvalidQueryException(msg)) => println(msg) 37 | case Failure(t: UserInterruptException) => exit() // Ctrl C 38 | case Failure(t: EndOfFileException) => exit() // Ctrl D 39 | case Failure(t) => t.printStackTrace() 40 | } 41 | 42 | repl() 43 | } 44 | 45 | def sqlEval(query: String): List[String] = { 46 | val sqlAST = SqlParser.parse(query) 47 | val midiParser = new MidiParser 48 | val musicXmlParser = new MusicXMLParser 49 | val songsSatisfyingQuery = new SongEvaluator(midiParser, musicXmlParser).processSqlQuery(sqlAST) 50 | songsSatisfyingQuery.map(song => song.filePath) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/MusicFileParser.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core 2 | 3 | import org.modulo12.core.models.{ ParseFileResult, Song, SongData, SongMetadata } 4 | import org.modulo12.core.models.ParseFileResult.Success 5 | 6 | import java.io.File 7 | 8 | trait MusicFileParser { 9 | def parseFile(file: File): ParseFileResult 10 | 11 | def fileExtensions(): List[String] 12 | 13 | def parseAllFiles(directory: File): List[Song] = { 14 | val files = getAllFilesUnderDirectory(directory) 15 | val songs = files 16 | .map(file => (file, parseFile(file))) 17 | .flatMap { case (midiFile, song) => 18 | song match { 19 | case Success(song) => List(song) 20 | case _ => List() 21 | } 22 | } 23 | songs 24 | } 25 | 26 | // TODO: Ideally we should be checking the magic number of these files, 27 | // I will write that code in at a later date. Currently just using extensions 28 | def getAllFilesUnderDirectory(dir: File): List[File] = 29 | getFileTree(dir) 30 | .filter(_.isFile) 31 | .filter(file => fileExtensions().exists(file.getName.endsWith(_))) 32 | .toList 33 | 34 | protected def extractSongDataFromListener(listener: SongParserListener): (SongMetadata, SongData) = { 35 | val metadata = models.SongMetadata( 36 | listener.numBarLines, 37 | listener.numTracks, 38 | listener.temposBPM.toSet, 39 | listener.keySignature, 40 | listener.timeSignature, 41 | listener.instrumentNames.toSet 42 | ) 43 | val lyrics = if (listener.lyrics.isEmpty) None else Option(listener.lyrics.mkString(" ")) 44 | val data = 45 | SongData(listener.notes.toList.distinct, listener.chords.toList.distinct, lyrics) 46 | (metadata, data) 47 | } 48 | 49 | // https://stackoverflow.com/questions/2637643/how-do-i-list-all-files-in-a-subdirectory-in-scala 50 | private def getFileTree(f: File): LazyList[File] = 51 | f #:: (if (f.isDirectory) f.listFiles().to(LazyList).flatMap(getFileTree) 52 | else LazyList.empty) 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/SongEvaluator.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core 2 | 3 | import org.modulo12.core.models.{ 4 | FileType, 5 | LogicalExpression, 6 | LogicalOperator, 7 | RequestedBarLinesComparison, 8 | RequestedInstrumentType, 9 | RequestedKeyType, 10 | RequestedLyrics, 11 | RequestedScaleType, 12 | RequestedTempoComparison, 13 | RequestedTracksComparison, 14 | SimpleExpression, 15 | Song, 16 | SqlAST, 17 | UnknownSimpleExpression, 18 | WhereExpression 19 | } 20 | import org.modulo12.midi.MidiParser 21 | import org.modulo12.musicxml.MusicXMLParser 22 | 23 | import java.io.File 24 | 25 | class SongEvaluator(midiParser: MidiParser, musicXmlParser: MusicXMLParser) { 26 | def processSqlQuery(sqlAST: SqlAST): List[Song] = { 27 | val fileTypes = sqlAST.selectExpression.fileTypesToAnalye.fileTypes 28 | val directory = sqlAST.fromExpression.directoryToAnalyze.directory 29 | val songsToAnalyze = acquireSongsForProcessing(fileTypes, directory) 30 | sqlAST.whereExpression.map(expr => evaluateWhereExpression(expr, songsToAnalyze)).getOrElse(songsToAnalyze) 31 | } 32 | 33 | private def acquireSongsForProcessing(fileTypesToAnalye: Set[FileType], directoryToAnalyze: File): List[Song] = { 34 | val xmlSongsToAnalyze = 35 | if (fileTypesToAnalye.contains(FileType.MusicXML)) 36 | musicXmlParser.parseAllFiles(directoryToAnalyze) 37 | else 38 | List() 39 | 40 | val midiSongToAnalyze = 41 | if (fileTypesToAnalye.contains(FileType.Midi)) 42 | midiParser.parseAllFiles(directoryToAnalyze) 43 | else 44 | List() 45 | 46 | xmlSongsToAnalyze ++ midiSongToAnalyze 47 | } 48 | 49 | private def evaluateWhereExpression(w: WhereExpression, allSongsToAnalyze: List[Song]): List[Song] = 50 | w match { 51 | case s: SimpleExpression => evaluateSimpleExpression(s, allSongsToAnalyze) 52 | case l: LogicalExpression => evaluateLogicalExpression(l, allSongsToAnalyze) 53 | } 54 | 55 | private def evaluateLogicalExpression(l: LogicalExpression, allSongsToAnalyze: List[Song]): List[Song] = { 56 | val leftExpr = evaluateWhereExpression(l.leftExpr, allSongsToAnalyze) 57 | val rightExpr = evaluateWhereExpression(l.rightExpr, allSongsToAnalyze) 58 | 59 | l.logicalOperator match { 60 | case LogicalOperator.AND => leftExpr.toSet.intersect(rightExpr.toSet).toList 61 | case LogicalOperator.OR => leftExpr.toSet.union(rightExpr.toSet).toList 62 | } 63 | } 64 | 65 | private def evaluateSimpleExpression(s: SimpleExpression, allSongsToAnalyze: List[Song]): List[Song] = 66 | s match { 67 | case RequestedScaleType(scaleType) => 68 | SongMetadataEvaluator.filtersSongsWithScaleType(scaleType, allSongsToAnalyze) 69 | case RequestedKeyType(keyType) => 70 | SongMetadataEvaluator.filtersSongsWithKeyType(keyType, allSongsToAnalyze) 71 | case RequestedInstrumentType(instrument) => 72 | SongMetadataEvaluator.filterSongsWithInstrument(instrument, allSongsToAnalyze) 73 | case RequestedTempoComparison(tempo, comparator) => 74 | SongMetadataEvaluator.filterSongsWithTempoComparsion(tempo, comparator, allSongsToAnalyze) 75 | case RequestedBarLinesComparison(numBarlines, comparator) => 76 | SongMetadataEvaluator.filterSongsWithNumBarsComparsion(numBarlines, comparator, allSongsToAnalyze) 77 | case RequestedTracksComparison(numTracks, comparator) => 78 | SongMetadataEvaluator.filterSongsWithNumTracksComparsion(numTracks, comparator, allSongsToAnalyze) 79 | case RequestedLyrics(lyrics) => 80 | SongMetadataEvaluator.filterSongWithLyrics(lyrics, allSongsToAnalyze) 81 | case UnknownSimpleExpression => allSongsToAnalyze 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/SongMetadataEvaluator.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core 2 | 3 | import org.modulo12.core.models.{ Comparator, Key, Scale, Song } 4 | 5 | object SongMetadataEvaluator { 6 | def filtersSongsWithScaleType(requestedScaleType: Scale, songsToAnalyze: List[Song]): List[Song] = 7 | songsToAnalyze.filter { song => 8 | val keySignature = song.metadata.keySignature 9 | keySignature.map(sig => sig.scale.equals(requestedScaleType)).getOrElse(false) 10 | } 11 | 12 | def filtersSongsWithKeyType(requestedKeyType: Key, songsToAnalyze: List[Song]): List[Song] = 13 | songsToAnalyze.filter { song => 14 | val keySignature = song.metadata.keySignature 15 | keySignature.map(sig => sig.key.equals(requestedKeyType)).getOrElse(false) 16 | } 17 | 18 | def filterSongsWithInstrument(instrument: String, songsToAnalyze: List[Song]): List[Song] = 19 | songsToAnalyze.filter { song => 20 | song.metadata.instrumentNames.map(_.toLowerCase).contains(instrument.toLowerCase) 21 | } 22 | 23 | def filterSongsWithTempoComparsion(tempo: Double, comparator: Comparator, songsToAnalyze: List[Song]): List[Song] = 24 | songsToAnalyze.filter { song => 25 | song.metadata.temposBPM.exists(songTempo => Comparator.compare(songTempo, comparator, tempo)) 26 | } 27 | 28 | def filterSongsWithNumTracksComparsion( 29 | numTracks: Double, 30 | comparator: Comparator, 31 | songsToAnalyze: List[Song] 32 | ): List[Song] = 33 | songsToAnalyze.filter { song => 34 | Comparator.compare(song.metadata.numTracks, comparator, numTracks) 35 | } 36 | 37 | def filterSongsWithNumBarsComparsion( 38 | numBars: Double, 39 | comparator: Comparator, 40 | songsToAnalyze: List[Song] 41 | ): List[Song] = 42 | songsToAnalyze.filter { song => 43 | Comparator.compare(song.metadata.numBarLines, comparator, numBars) 44 | } 45 | 46 | def filterSongWithLyrics(lyrics: List[String], songsToAnalyze: List[Song]): List[Song] = 47 | songsToAnalyze.filter { song => 48 | song.data.lyrics match { 49 | case Some(lyricsInSong) => lyrics.exists(lyric => lyricsInSong.toLowerCase().contains(lyric.toLowerCase())) 50 | case None => false 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/SongParserListener.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core 2 | 3 | import org.jfugue.midi.MidiDictionary 4 | import org.jfugue.parser.ParserListenerAdapter 5 | import org.jfugue.theory.{ Chord, Note } 6 | import org.modulo12.core.models.{ Key, KeySignature, Scale, TimeSignature } 7 | 8 | import scala.collection.mutable 9 | import scala.collection.mutable.ListBuffer 10 | 11 | class SongParserListener extends ParserListenerAdapter { 12 | var temposBPM = new mutable.ListBuffer[Int]() 13 | var instrumentNames = new mutable.HashSet[String]() 14 | var timeSignature = Option.empty[TimeSignature] 15 | var keySignature = Option.empty[KeySignature] 16 | var notes = new mutable.ListBuffer[Note] 17 | var chords = new mutable.ListBuffer[Chord] 18 | val lyrics = new mutable.ListBuffer[String] 19 | var numBarLines = 0 20 | var numTracks = 0 21 | 22 | override def onTrackChanged(track: Byte): Unit = 23 | numTracks = numTracks + 1 24 | 25 | override def onBarLineParsed(id: Long): Unit = 26 | numBarLines = numBarLines + 1 27 | 28 | override def onLyricParsed(lyric: String): Unit = 29 | lyrics.addOne(lyric) 30 | 31 | override def onNoteParsed(note: Note): Unit = 32 | notes.addOne(note) 33 | 34 | override def onChordParsed(chord: Chord): Unit = 35 | chords.addOne(chord) 36 | 37 | override def onKeySignatureParsed(key: Byte, scale: Byte): Unit = { 38 | val scaleType = scale.toInt match { 39 | case 0 => Scale.MAJOR 40 | case 1 => Scale.MINOR 41 | case _ => Scale.UNKNOWN 42 | } 43 | 44 | keySignature = Option(KeySignature(Key.rotateOnCircleOfFifths(key.toInt), scaleType)) 45 | } 46 | 47 | override def onTempoChanged(tempoBPM: Int): Unit = 48 | temposBPM.addOne(tempoBPM) 49 | 50 | override def onInstrumentParsed(instrument: Byte): Unit = { 51 | val instrumentName = MidiDictionary.INSTRUMENT_BYTE_TO_STRING.get(instrument) 52 | instrumentNames.add(instrumentName) 53 | } 54 | 55 | override def onTimeSignatureParsed(numerator: Byte, powerOfTwo: Byte): Unit = 56 | timeSignature = Option(TimeSignature(numerator.toInt, powerOfTwo.toInt)) 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/models/operator.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core.models 2 | 3 | sealed trait Comparator 4 | 5 | object Comparator { 6 | case object EQ extends Comparator // Equals 7 | case object NEQ extends Comparator // Not Equals 8 | case object GEQ extends Comparator // Greater than or equals 9 | case object LEQ extends Comparator // Less than or equals 10 | case object GT extends Comparator // Greater than 11 | case object LT extends Comparator // Less than 12 | 13 | def fromString(value: String): Comparator = 14 | value match { 15 | case ">=" => GEQ 16 | case "!=" => NEQ 17 | case "<=" => LEQ 18 | case "=" => EQ 19 | case "<" => LT 20 | case ">" => GT 21 | } 22 | 23 | def compare(operand1: Double, comparator: Comparator, operand2: Double): Boolean = 24 | comparator match { 25 | case EQ => operand1 == operand2 26 | case GEQ => operand1 >= operand2 27 | case LEQ => operand1 <= operand2 28 | case LT => operand1 < operand2 29 | case GT => operand1 > operand2 30 | case NEQ => operand1 != operand2 31 | } 32 | } 33 | 34 | sealed trait LogicalOperator 35 | case object LogicalOperator { 36 | case object AND extends LogicalOperator 37 | case object OR extends LogicalOperator 38 | 39 | def fromString(value: String): LogicalOperator = 40 | value.toUpperCase match { 41 | case "AND" => AND 42 | case "OR" => OR 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/models/songmodel.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core.models 2 | 3 | import org.jfugue.theory.{ Chord, Note } 4 | 5 | // Song data 6 | case class SongData(notes: List[Note], chords: List[Chord], lyrics: Option[String]) 7 | 8 | sealed trait Key 9 | object Key { 10 | // Key with no flats or sharps 11 | case object C extends Key 12 | 13 | // Keys with sharps, laid out clockwise on the circle of fifths 14 | case object G extends Key 15 | case object D extends Key 16 | case object A extends Key 17 | case object E extends Key 18 | case object B extends Key 19 | case object Fshrp extends Key 20 | case object Cshrp extends Key 21 | 22 | // Keys with flats 23 | case object F extends Key 24 | case object Bb extends Key 25 | case object Eb extends Key 26 | case object Ab extends Key 27 | case object Db extends Key 28 | case object Gb extends Key 29 | case object Cb extends Key 30 | 31 | def rotateOnCircleOfFifths(byAmount: Integer): Key = 32 | byAmount match { 33 | case 0 => C 34 | case 1 => G 35 | case 2 => D 36 | case 3 => A 37 | case 4 => E 38 | case 5 => B 39 | case 6 => Fshrp 40 | case 7 => Cshrp 41 | case -1 => F 42 | case -2 => Bb 43 | case -3 => Eb 44 | case -4 => Ab 45 | case -5 => Db 46 | case -6 => Gb 47 | case -7 => Cb 48 | } 49 | 50 | def fromString(value: String): Key = 51 | value.toUpperCase match { 52 | case "C" => C 53 | case "G" => G 54 | case "D" => D 55 | case "A" => A 56 | case "E" => E 57 | case "B" => B 58 | case "F#" => Fshrp 59 | case "C#" => Cshrp 60 | case "F" => F 61 | case "BB" => Bb 62 | case "EB" => Eb 63 | case "AB" => Ab 64 | case "DB" => Db 65 | case "GB" => Gb 66 | case "CB" => Cb 67 | } 68 | } 69 | 70 | // Song metadata 71 | sealed trait Scale 72 | object Scale { 73 | case object MAJOR extends Scale; 74 | case object MINOR extends Scale; 75 | case object UNKNOWN extends Scale; 76 | 77 | def fromString(value: String): Scale = { 78 | val valueUpper = value.toUpperCase() 79 | if (valueUpper.equals("MAJOR")) 80 | MAJOR 81 | else if (valueUpper.equals("MINOR")) 82 | MINOR 83 | else 84 | UNKNOWN 85 | } 86 | } 87 | 88 | case class KeySignature(key: Key, scale: Scale) 89 | 90 | // ADT representing the metadata and data about a song. 91 | case class TimeSignature(beatsPerMeasure: Int, noteValuePerBeat: Int) 92 | 93 | case class SongMetadata( 94 | numBarLines: Int, 95 | numTracks: Int, 96 | temposBPM: Set[Int], 97 | keySignature: Option[KeySignature], 98 | timeSig: Option[TimeSignature], 99 | instrumentNames: Set[String] 100 | ) 101 | 102 | case class Song(filePath: String, metadata: SongMetadata, data: SongData) 103 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/models/songparsemodel.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core.models 2 | 3 | import org.modulo12.core.models.ParseFileResult.Success 4 | import org.modulo12.core.models.{ FileType, ParseFileResult, Song } 5 | 6 | import java.io.File 7 | import javax.sound.midi.Sequence 8 | 9 | sealed trait FileType 10 | object FileType { 11 | case object Midi extends FileType 12 | case object MusicXML extends FileType 13 | } 14 | 15 | sealed trait ParseFileResult 16 | object ParseFileResult { 17 | case class FileNotFound(path: String) extends ParseFileResult 18 | case class IncorrectFileType(path: String, fileType: FileType) extends ParseFileResult 19 | case class Success(song: Song) extends ParseFileResult 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/core/models/sqlmodel.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core.models 2 | 3 | import org.modulo12.core.models.{ 4 | Comparator, 5 | DirectoryToAnalyze, 6 | LogicalOperator, 7 | SimpleExpression, 8 | SqlSubQuery, 9 | WhereExpression, 10 | _ 11 | } 12 | 13 | import java.io.File 14 | 15 | case class InvalidQueryException(msg: String) extends Exception(msg) 16 | 17 | // Top level trait expressing either the query or a sub section of the query 18 | sealed trait SqlSubQuery 19 | 20 | // Abstract syntax tree for sql query 21 | case class SqlAST( 22 | selectExpression: SelectExpression, 23 | fromExpression: FromExpression, 24 | whereExpression: Option[WhereExpression] 25 | ) extends SqlSubQuery 26 | 27 | case class SelectExpression(fileTypesToAnalye: FileTypesToAnalye) 28 | case class FromExpression(directoryToAnalyze: DirectoryToAnalyze) 29 | 30 | // From clause sub results 31 | case class DirectoryToAnalyze(directory: File) extends SqlSubQuery 32 | case class FileTypesToAnalye(fileTypes: Set[FileType]) extends SqlSubQuery 33 | 34 | // Where clause sub results 35 | sealed trait WhereExpression extends SqlSubQuery 36 | case class LogicalExpression( 37 | leftExpr: WhereExpression, 38 | logicalOperator: LogicalOperator, 39 | rightExpr: WhereExpression 40 | ) extends WhereExpression 41 | sealed trait SimpleExpression extends WhereExpression 42 | case class RequestedScaleType(scaleType: Scale) extends SimpleExpression 43 | case class RequestedKeyType(key: Key) extends SimpleExpression 44 | case class RequestedInstrumentType(instrument: String) extends SimpleExpression 45 | case class RequestedTempoComparison(tempo: Double, operator: Comparator) extends SimpleExpression 46 | case class RequestedBarLinesComparison(numBarlines: Double, operator: Comparator) extends SimpleExpression 47 | case class RequestedTracksComparison(numTracks: Double, operator: Comparator) extends SimpleExpression 48 | case class RequestedLyrics(words: List[String]) extends SimpleExpression 49 | case object UnknownSimpleExpression extends SimpleExpression 50 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/midi/MidiParser.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.midi 2 | 3 | import java.io.File 4 | import javax.sound.midi.{ InvalidMidiDataException, Sequence } 5 | import org.jfugue.midi.{ MidiFileManager, MidiParser => JFugueMidiParser } 6 | import org.modulo12.core.models.{ FileType, ParseFileResult, Song, SongData, SongMetadata } 7 | import org.modulo12.core.{ MusicFileParser, SongParserListener } 8 | import org.modulo12.core.models.ParseFileResult.Success 9 | 10 | class MidiParser extends MusicFileParser { 11 | private val parser = new JFugueMidiParser 12 | 13 | override def parseFile(midiFile: File): ParseFileResult = 14 | if (midiFile.exists()) 15 | try { 16 | val sequence = MidiFileManager.load(midiFile) 17 | val (songMeta, songData) = parseSongFromMidiSequence(sequence) 18 | ParseFileResult.Success(Song(midiFile.getName, songMeta, songData)) 19 | } catch { 20 | case _: InvalidMidiDataException => ParseFileResult.IncorrectFileType(midiFile.getAbsolutePath, FileType.Midi) 21 | } 22 | else 23 | ParseFileResult.FileNotFound(midiFile.getAbsolutePath) 24 | 25 | override def fileExtensions() = List(".mid") 26 | 27 | def parseSongFromMidiSequence(sequence: Sequence): (SongMetadata, SongData) = { 28 | val listener = new SongParserListener 29 | parser.addParserListener(listener) 30 | parser.parse(sequence) 31 | extractSongDataFromListener(listener) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/musicxml/MusicXMLParser.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.musicxml 2 | 3 | import org.jfugue.integration.MusicXmlParser 4 | import org.jfugue.midi.MidiFileManager 5 | import nu.xom.ParsingException 6 | import org.modulo12.core.{ MusicFileParser, SongParserListener } 7 | import org.modulo12.core.models.{ FileType, ParseFileResult, Song } 8 | 9 | import java.io.File 10 | import javax.sound.midi.InvalidMidiDataException 11 | 12 | class MusicXMLParser extends MusicFileParser { 13 | private val musicXMLParser = new MusicXmlParser 14 | 15 | override def parseFile(musicXmlFile: File): ParseFileResult = 16 | if (musicXmlFile.exists()) 17 | try { 18 | val listener = new SongParserListener 19 | musicXMLParser.addParserListener(listener) 20 | musicXMLParser.parse(musicXmlFile) 21 | val (songMeta, songData) = extractSongDataFromListener(listener) 22 | ParseFileResult.Success(Song(musicXmlFile.getName, songMeta, songData)) 23 | } catch { 24 | case e: ParsingException => 25 | ParseFileResult.IncorrectFileType(musicXmlFile.getAbsolutePath, FileType.MusicXML) 26 | } 27 | else 28 | ParseFileResult.FileNotFound(musicXmlFile.getAbsolutePath) 29 | 30 | override def fileExtensions(): List[String] = 31 | List(".xml") 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/sql/SqlParser.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.sql 2 | 3 | import org.antlr.v4.runtime.{ ANTLRInputStream, CharStreams, CommonTokenStream } 4 | import org.modulo12.core.models.SqlAST 5 | import org.modulo12.midi.MidiParser 6 | import org.modulo12.musicxml.MusicXMLParser 7 | import org.modulo12.{ Modulo12Lexer, Modulo12Parser } 8 | 9 | object SqlParser { 10 | def parse(query: String): SqlAST = { 11 | val charStream = CharStreams.fromString(query) 12 | val lexer = new Modulo12Lexer(charStream) 13 | val tokens = new CommonTokenStream(lexer) 14 | val parser = new Modulo12Parser(tokens) 15 | 16 | // By default antlr injects a console listener, and the way that worked is that it posted 17 | // syntax errors onto console and then proceeded to actually try to parse the query. 18 | // This bit of code removes that console parser and instead puts a new one that stops execution on 19 | // syntax errors 20 | parser.removeErrorListeners() 21 | parser.addErrorListener(new SqlParserErrorListener()) 22 | 23 | new SqlVisitor().visitSql_statement(parser.sql_statement()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/sql/SqlParserErrorListener.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.sql 2 | 3 | import org.antlr.v4.runtime.atn.ATNConfigSet 4 | import org.antlr.v4.runtime.dfa.DFA 5 | import org.antlr.v4.runtime.{ ANTLRErrorListener, Parser, RecognitionException, Recognizer } 6 | import org.modulo12.core.models.InvalidQueryException 7 | 8 | import java.util 9 | 10 | class SqlParserErrorListener extends ANTLRErrorListener { 11 | override def reportAmbiguity( 12 | recognizer: Parser, 13 | dfa: DFA, 14 | startIndex: Int, 15 | stopIndex: Int, 16 | exact: Boolean, 17 | ambigAlts: util.BitSet, 18 | configs: ATNConfigSet 19 | ): Unit = ??? 20 | 21 | override def reportAttemptingFullContext( 22 | recognizer: Parser, 23 | dfa: DFA, 24 | startIndex: Int, 25 | stopIndex: Int, 26 | conflictingAlts: util.BitSet, 27 | configs: ATNConfigSet 28 | ): Unit = ??? 29 | 30 | override def syntaxError( 31 | recognizer: Recognizer[_, _], 32 | offendingSymbol: Any, 33 | line: Int, 34 | charPositionInLine: Int, 35 | msg: String, 36 | e: RecognitionException 37 | ): Unit = 38 | throw new InvalidQueryException(s"$msg") 39 | 40 | override def reportContextSensitivity( 41 | recognizer: Parser, 42 | dfa: DFA, 43 | startIndex: Int, 44 | stopIndex: Int, 45 | prediction: Int, 46 | configs: ATNConfigSet 47 | ): Unit = ??? 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/org/modulo12/sql/SqlVisitor.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.sql 2 | 3 | import org.modulo12.core.models._ 4 | import org.modulo12.midi.MidiParser 5 | import org.modulo12.musicxml.MusicXMLParser 6 | import org.modulo12.{ Modulo12Parser, Modulo12ParserBaseVisitor } 7 | 8 | import java.io.File 9 | import java.util 10 | import scala.collection.JavaConverters._ 11 | 12 | class SqlVisitor() extends Modulo12ParserBaseVisitor[SqlSubQuery] { 13 | // This is the top level for visitor 14 | override def visitSql_statement(ctx: Modulo12Parser.Sql_statementContext): SqlAST = { 15 | val selectExpression = SelectExpression(visitInput_list_clause(ctx.input_list_clause())) 16 | val fromExpression = FromExpression(visitFrom_clause(ctx.from_clause())) 17 | val whereExpression = 18 | if (ctx.where_clause() != null) 19 | Option(visitWhere_clause(ctx.where_clause())) 20 | else 21 | None 22 | SqlAST(selectExpression, fromExpression, whereExpression) 23 | } 24 | 25 | override def visitInput_list_clause(ctx: Modulo12Parser.Input_list_clauseContext): FileTypesToAnalye = { 26 | val fileTypes = ctx 27 | .input_name() 28 | .asScala 29 | .flatMap(fileType => 30 | fileType.getText.toUpperCase match { 31 | case "MIDI" => List(FileType.Midi) 32 | case "MUSICXML" => List(FileType.MusicXML) 33 | case "*" => List(FileType.Midi, FileType.MusicXML) 34 | } 35 | ) 36 | FileTypesToAnalye(fileTypes.toSet) 37 | } 38 | 39 | override def visitFrom_clause(ctx: Modulo12Parser.From_clauseContext): DirectoryToAnalyze = 40 | DirectoryToAnalyze(new File(ctx.directory_name().ID().getText)) 41 | 42 | override def visitWhere_clause(ctx: Modulo12Parser.Where_clauseContext): WhereExpression = { 43 | val expression = ctx.expression() 44 | constructLogicalExpression(expression) 45 | } 46 | 47 | override def visitSimple_expression(ctx: Modulo12Parser.Simple_expressionContext): SimpleExpression = 48 | // TODO: Add other simple expressions and expand the visitor using pattern matching 49 | if (ctx.scale_comparison() != null) { 50 | val requestedScaleTypeStr = ctx.scale_comparison().SCALE_TYPE().getText 51 | val requestedScaleType = Scale.fromString(requestedScaleTypeStr) 52 | RequestedScaleType(requestedScaleType) 53 | } else if (ctx.key_comparison() != null) { 54 | val requestedKeyTypeStr = ctx.key_comparison().key_type().getText 55 | val requestedKeyType = Key.fromString(requestedKeyTypeStr) 56 | RequestedKeyType(requestedKeyType) 57 | } else if (ctx.song_has_instrument() != null) { 58 | val requestedInstrument = ctx.song_has_instrument().ID().getText 59 | RequestedInstrumentType(requestedInstrument) 60 | } else if (ctx.tempo_comparison() != null) { 61 | val comparator = Comparator.fromString(ctx.tempo_comparison().relational_op().getText) 62 | val tempo = ctx.tempo_comparison().NUMBER().getText.toDouble 63 | RequestedTempoComparison(tempo, comparator) 64 | } else if (ctx.lyrics_comparison() != null) { 65 | val lyricsToCompare = ctx.lyrics_comparison().words().word().asScala.map(_.getText) 66 | RequestedLyrics(lyricsToCompare.toList) 67 | } else if (ctx.num_barlines_comparision() != null) { 68 | val comparator = Comparator.fromString(ctx.num_barlines_comparision().relational_op().getText) 69 | val numBarLines = ctx.num_barlines_comparision().NUMBER().getText.toDouble 70 | RequestedBarLinesComparison(numBarLines, comparator) 71 | } else if (ctx.num_tracks_comparision() != null) { 72 | val comparator = Comparator.fromString(ctx.num_tracks_comparision().relational_op().getText) 73 | val numTracks = ctx.num_tracks_comparision().NUMBER().getText.toDouble 74 | RequestedTracksComparison(numTracks, comparator) 75 | } else 76 | UnknownSimpleExpression 77 | 78 | def constructLogicalExpression( 79 | expression: Modulo12Parser.ExpressionContext 80 | ): WhereExpression = 81 | if (expression.simple_expression() != null) 82 | visitSimple_expression(expression.simple_expression()) 83 | else { 84 | val exprLeft = expression.expression(0) 85 | val exprRight = expression.expression(1) 86 | val evalExprLeft = 87 | if (exprLeft.simple_expression() != null) constructLogicalExpression(exprLeft) 88 | else visitSimple_expression(exprLeft.simple_expression()) 89 | val evalExprRight = 90 | if (exprRight.simple_expression() != null) constructLogicalExpression(exprRight) 91 | else visitSimple_expression(exprRight.simple_expression()) 92 | 93 | val logicalOp = LogicalOperator.fromString(expression.logical_op().getText) 94 | LogicalExpression(evalExprLeft, logicalOp, evalExprRight) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/core/SongModelSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.core 2 | 3 | import org.scalatest._ 4 | import matchers._ 5 | import flatspec._ 6 | import org.modulo12.core.models.{ Comparator, Key, LogicalOperator, Scale } 7 | import org.modulo12.midi.MidiParser 8 | 9 | import java.io.File 10 | 11 | class SongModelSpec extends AnyFlatSpec with should.Matchers { 12 | "ScaleType toString" should "evaluate minor scale correctly" in { 13 | Scale.fromString("minor") should be(Scale.MINOR) 14 | Scale.fromString("Minor") should be(Scale.MINOR) 15 | Scale.fromString("MINOR") should be(Scale.MINOR) 16 | Scale.fromString("MiNoR") should be(Scale.MINOR) 17 | } 18 | 19 | it should "evaluate major scale correctly" in { 20 | Scale.fromString("major") should be(Scale.MAJOR) 21 | Scale.fromString("Major") should be(Scale.MAJOR) 22 | Scale.fromString("MAJOR") should be(Scale.MAJOR) 23 | Scale.fromString("MaJoR") should be(Scale.MAJOR) 24 | } 25 | 26 | it should "evaluate unknown scale correctly" in { 27 | Scale.fromString("unknown") should be(Scale.UNKNOWN) 28 | Scale.fromString("UNknown") should be(Scale.UNKNOWN) 29 | Scale.fromString("UNKNOWN") should be(Scale.UNKNOWN) 30 | } 31 | 32 | it should "evaluate to unknown on invalid inputs" in { 33 | Scale.fromString("") should be(Scale.UNKNOWN) 34 | Scale.fromString("blah") should be(Scale.UNKNOWN) 35 | Scale.fromString("gibberish") should be(Scale.UNKNOWN) 36 | } 37 | 38 | "Comparator fromString" should "should evaluate equals correctly" in { 39 | Comparator.fromString("=") should be(Comparator.EQ) 40 | } 41 | 42 | it should "should evaluate not equal to correctly" in { 43 | Comparator.fromString("!=") should be(Comparator.NEQ) 44 | } 45 | 46 | it should "should evaluate less than correctly" in { 47 | Comparator.fromString("<") should be(Comparator.LT) 48 | } 49 | 50 | it should "should evaluate greater than correctly" in { 51 | Comparator.fromString(">") should be(Comparator.GT) 52 | } 53 | 54 | it should "should evaluate less than or equal to correctly" in { 55 | Comparator.fromString("<=") should be(Comparator.LEQ) 56 | } 57 | 58 | it should "should evaluate greater than or equal to correctly" in { 59 | Comparator.fromString(">=") should be(Comparator.GEQ) 60 | } 61 | 62 | "Comparator compare" should "should compare equals correctly" in { 63 | Comparator.compare(6.0, Comparator.EQ, 6.0) should be(true) 64 | Comparator.compare(6.0, Comparator.EQ, 7.0) should be(false) 65 | } 66 | 67 | it should "should compare not equals correctly" in { 68 | Comparator.compare(6.0, Comparator.NEQ, 6.0) should be(false) 69 | Comparator.compare(6.0, Comparator.NEQ, 7.0) should be(true) 70 | } 71 | 72 | it should "should compare less than correctly" in { 73 | Comparator.compare(5.0, Comparator.LT, 6.0) should be(true) 74 | Comparator.compare(6.0, Comparator.LT, 5.0) should be(false) 75 | } 76 | 77 | it should "should compare greater than correctly" in { 78 | Comparator.compare(5.0, Comparator.GT, 6.0) should be(false) 79 | Comparator.compare(6.0, Comparator.GT, 5.0) should be(true) 80 | } 81 | 82 | it should "should compare less than or equal to correctly" in { 83 | Comparator.compare(5.0, Comparator.LEQ, 6.0) should be(true) 84 | Comparator.compare(6.0, Comparator.LEQ, 5.0) should be(false) 85 | Comparator.compare(6.0, Comparator.LEQ, 6.0) should be(true) 86 | } 87 | 88 | it should "should compare greater than or equal to correctly" in { 89 | Comparator.compare(5.0, Comparator.GEQ, 6.0) should be(false) 90 | Comparator.compare(6.0, Comparator.GEQ, 5.0) should be(true) 91 | Comparator.compare(6.0, Comparator.GEQ, 6.0) should be(true) 92 | } 93 | 94 | "Logical Operator from string" should "evaluate AND operator correctly" in { 95 | LogicalOperator.fromString("AND") should be(LogicalOperator.AND) 96 | LogicalOperator.fromString("and") should be(LogicalOperator.AND) 97 | LogicalOperator.fromString("aND") should be(LogicalOperator.AND) 98 | } 99 | 100 | it should "evaluate OR operator correctly" in { 101 | LogicalOperator.fromString("OR") should be(LogicalOperator.OR) 102 | LogicalOperator.fromString("or") should be(LogicalOperator.OR) 103 | LogicalOperator.fromString("Or") should be(LogicalOperator.OR) 104 | } 105 | 106 | "Key rotateOnCircle of Fifth" should "evaluate correctly for keys" in { 107 | Key.rotateOnCircleOfFifths(0) should be(Key.C) 108 | Key.rotateOnCircleOfFifths(1) should be(Key.G) 109 | Key.rotateOnCircleOfFifths(2) should be(Key.D) 110 | Key.rotateOnCircleOfFifths(3) should be(Key.A) 111 | Key.rotateOnCircleOfFifths(4) should be(Key.E) 112 | Key.rotateOnCircleOfFifths(5) should be(Key.B) 113 | Key.rotateOnCircleOfFifths(6) should be(Key.Fshrp) 114 | Key.rotateOnCircleOfFifths(7) should be(Key.Cshrp) 115 | Key.rotateOnCircleOfFifths(-1) should be(Key.F) 116 | Key.rotateOnCircleOfFifths(-2) should be(Key.Bb) 117 | Key.rotateOnCircleOfFifths(-3) should be(Key.Eb) 118 | Key.rotateOnCircleOfFifths(-4) should be(Key.Ab) 119 | Key.rotateOnCircleOfFifths(-5) should be(Key.Db) 120 | Key.rotateOnCircleOfFifths(-6) should be(Key.Gb) 121 | Key.rotateOnCircleOfFifths(-7) should be(Key.Cb) 122 | } 123 | 124 | "Key fromString" should "evaluate correctly for keys" in { 125 | Key.fromString("c") should be(Key.C) 126 | Key.fromString("G") should be(Key.G) 127 | Key.fromString("d") should be(Key.D) 128 | Key.fromString("A") should be(Key.A) 129 | Key.fromString("e") should be(Key.E) 130 | Key.fromString("b") should be(Key.B) 131 | Key.fromString("f#") should be(Key.Fshrp) 132 | Key.fromString("C#") should be(Key.Cshrp) 133 | Key.fromString("F") should be(Key.F) 134 | Key.fromString("Bb") should be(Key.Bb) 135 | Key.fromString("eb") should be(Key.Eb) 136 | Key.fromString("AB") should be(Key.Ab) 137 | Key.fromString("Db") should be(Key.Db) 138 | Key.fromString("Gb") should be(Key.Gb) 139 | Key.fromString("Cb") should be(Key.Cb) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/midi/MidiParserSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.midi 2 | import org.scalatest._ 3 | import matchers._ 4 | import flatspec._ 5 | import org.modulo12.core.models.{ FileType, ParseFileResult, Song, TimeSignature } 6 | 7 | import java.io.File 8 | import scala.language.implicitConversions 9 | 10 | class MidiParserSpec extends AnyFlatSpec with should.Matchers with Inside { 11 | val midiFile = new File("resources/testmidifiles/MIDI_sample.mid") 12 | val midiParser = new MidiParser 13 | 14 | "parse midi file" should "return FileNotFound for a non exitent file" in { 15 | val path = "resources/testmidifiles/nonexistent" 16 | val result = midiParser.parseFile(new File(path)) 17 | result should matchPattern { case ParseFileResult.FileNotFound(path) => } 18 | } 19 | 20 | it should "Not Midi for files that are not music xml" in { 21 | val path = "resources/testmidifiles/filefornegativetest" 22 | val result = midiParser.parseFile(new File(path)) 23 | result should matchPattern { case ParseFileResult.IncorrectFileType(path, FileType.Midi) => } 24 | } 25 | 26 | "parse file" should "parse song correctly for a correct file" in { 27 | val result = midiParser.parseFile(midiFile) 28 | inside(result) { case ParseFileResult.Success(song) => 29 | // TODO: This nees more thorough testing. 30 | val Song(songFileName, songMeta, songData) = song 31 | songMeta.timeSig should be(Some(TimeSignature(4, 2))) 32 | songMeta.instrumentNames should contain allOf ("Electric_Jazz_Guitar", "Piano", "Electric_Bass_Finger") 33 | songMeta.temposBPM should be(Set(120)) 34 | songMeta.keySignature should be(None) 35 | songMeta.numBarLines should be(0) 36 | songMeta.numTracks should be(4) 37 | 38 | songData.chords.size should be(0) 39 | // TODO: Fix this, it non deterministically fails, not priority until the metadata language is completed 40 | // songData.notes.size should be(1651) 41 | songData.lyrics.isEmpty should be(true) 42 | } 43 | } 44 | 45 | "get midi files under directory" should "return midi files correctly for a valid directory" in { 46 | val result = midiParser.getAllFilesUnderDirectory(new File("resources/testmidifiles")).map(file => file.getName) 47 | result should equal(List("MIDI_sample.mid")) 48 | } 49 | 50 | it should "return empty list invalid directory" in { 51 | val result = midiParser.getAllFilesUnderDirectory(new File("resources/invalidDirectory")).map(file => file.getName) 52 | result should equal(List()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/musicxml/MusicXMLParserSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.musicxml 2 | 3 | import org.scalatest._ 4 | import matchers._ 5 | import flatspec._ 6 | import org.modulo12.core.models.Scale.MAJOR 7 | import org.modulo12.core.models.{ FileType, Key, KeySignature, ParseFileResult, Song } 8 | 9 | import java.io.File 10 | import scala.language.implicitConversions 11 | 12 | class MusicXMLParserSpec extends AnyFlatSpec with should.Matchers with Inside { 13 | val musicXMLFile = new File("resources/testmusicxmlfiles/musicXMLTest.xml") 14 | val musicXmlParser = new MusicXMLParser 15 | 16 | "parse midi file" should "return FileNotFound for a non exitent file" in { 17 | val path = "resources/testmusicxmlfiles/nonexistent" 18 | val result = musicXmlParser.parseFile(new File(path)) 19 | result should matchPattern { case ParseFileResult.FileNotFound(path) => } 20 | } 21 | 22 | it should "return incorrect file type for files that are not music xml" in { 23 | val path = "resources/testmusicxmlfiles/filefornegativetest" 24 | val result = musicXmlParser.parseFile(new File(path)) 25 | result should matchPattern { case ParseFileResult.IncorrectFileType(path, FileType.MusicXML) => } 26 | } 27 | 28 | "get music xml files under directory" should "return midi files correctly for a valid directory" in { 29 | val result = 30 | musicXmlParser.getAllFilesUnderDirectory(new File("resources/testmusicxmlfiles")).map(file => file.getName) 31 | result should equal(List("musicXMLTest.xml")) 32 | } 33 | 34 | "parse file" should "parse music xml file correctly for a correct file" in { 35 | val result = musicXmlParser.parseFile(musicXMLFile) 36 | inside(result) { 37 | // TODO: Verify and Improve these tests 38 | case ParseFileResult.Success(song) => 39 | val Song(songFileName, songMeta, songData) = song 40 | songMeta.timeSig should be(None) 41 | songMeta.instrumentNames should be(Set()) 42 | songMeta.temposBPM should be(Set()) 43 | songMeta.keySignature should contain(KeySignature(Key.Eb, MAJOR)) 44 | songMeta.numBarLines should be(2) 45 | songMeta.numTracks should be(1) 46 | 47 | songData.chords.size should be(0) 48 | songData.notes.size should be(6) 49 | songData.lyrics should contain("something du nicht, heil ger") 50 | } 51 | } 52 | it should "return empty list invalid directory" in { 53 | val result = 54 | musicXmlParser.getAllFilesUnderDirectory(new File("resources/invalidDirectory")).map(file => file.getName) 55 | result should equal(List()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/BinaryLogicalOperationSqlSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | import org.modulo12.sql.SqlParser 9 | 10 | class BinaryLogicalOperationSqlSpec extends AnyFlatSpec with should.Matchers with Inside { 11 | "sql evaluator" should "return nothing for a valid AND condition with one of the statements as false" in { 12 | Main.sqlEval("select midi from resources where tempo = 120 AND song has lyrics friday, go;") should equal(List()) 13 | } 14 | 15 | it should "return song correctly for a valid AND condition with both statements as true" in { 16 | Main.sqlEval("select midi from resources where tempo = 120 and song has instrument piano;") should equal( 17 | List("MIDI_sample.mid") 18 | ) 19 | } 20 | 21 | it should "return nothing for a valid AND condition with both statements as false" in { 22 | Main.sqlEval("select midi from resources where tempo = 110 AND song has instrument guitar;") should equal(List()) 23 | } 24 | 25 | it should "return song correctly for a valid OR condition with both statements true" in { 26 | Main.sqlEval("select midi from resources where tempo = 120 OR song has instrument piano;") should equal( 27 | List("MIDI_sample.mid") 28 | ) 29 | } 30 | 31 | it should "return song correctly for a valid OR condition with one of the statements as true" in { 32 | Main.sqlEval("select midi from resources where tempo = 120 OR song has lyrics friday, go;") should equal( 33 | List("MIDI_sample.mid") 34 | ) 35 | } 36 | 37 | it should "return nothing for a valid OR condition with both statements as false" in { 38 | Main.sqlEval("select midi from resources where tempo = 150 OR song has lyrics friday, go;") should equal(List()) 39 | } 40 | 41 | // TODO: Write more complex unit tests here 42 | } 43 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/InstrumentQuerySqlSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | import org.modulo12.sql.SqlParser 9 | 10 | class InstrumentQuerySqlSpec extends AnyFlatSpec with should.Matchers with Inside { 11 | "sql evaluator" should "return song correctly if there is an instrument in it" in { 12 | Main.sqlEval("SELECT midi FROM resources where song has instrument piano;") should equal(List("MIDI_sample.mid")) 13 | } 14 | 15 | it should "return nothing if there we query for an instrument and there are none listed in the file" in { 16 | Main.sqlEval("SELECT musicxml FROM resources where SONG has instrument guitar;") should equal(List()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/InvalidQuerySqlSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | import org.modulo12.core.models.InvalidQueryException 9 | import org.modulo12.sql.SqlParser 10 | 11 | class InvalidQuerySqlSpec extends AnyFlatSpec with should.Matchers with Inside { 12 | "sql evaluator" should "throw invalid query exception for wrong select clause" in { 13 | the[InvalidQueryException] thrownBy (Main.sqlEval( 14 | "selec" 15 | )) should have message "mismatched input 'selec' expecting SELECT" 16 | the[InvalidQueryException] thrownBy (Main.sqlEval( 17 | "select wrong" 18 | )) should have message "mismatched input 'wrong' expecting {MIDI, MUSICXML, '*'}" 19 | the[InvalidQueryException] thrownBy (Main.sqlEval( 20 | "select midi" 21 | )) should have message "mismatched input '' expecting {FROM, ','}" 22 | } 23 | 24 | it should "throw invalid query exception for the wrong from clause" in { 25 | the[InvalidQueryException] thrownBy (Main.sqlEval("select midi fro")) should have message "missing FROM at 'fro'" 26 | the[InvalidQueryException] thrownBy (Main.sqlEval( 27 | "select midi from" 28 | )) should have message "mismatched input '' expecting {FROM, ','}" 29 | } 30 | 31 | it should "throw invalid query exception for the wrong where clause" in { 32 | the[InvalidQueryException] thrownBy (Main.sqlEval( 33 | "select midi from folder where" 34 | )) should have message "mismatched input '' expecting {KEY, SCALE, SONG, TEMPO, NUMBARLINES, NUMTRACKS}" 35 | the[InvalidQueryException] thrownBy (Main.sqlEval( 36 | "select midi from folder where INVALIDENTITY > 666" 37 | )) should have message "mismatched input 'INVALIDENTITY' expecting {KEY, SCALE, SONG, TEMPO, NUMBARLINES, NUMTRACKS}" 38 | the[InvalidQueryException] thrownBy (Main.sqlEval( 39 | "select midi from folder where tempo <> 120" 40 | )) should have message "extraneous input '>' expecting NUMBER" 41 | the[InvalidQueryException] thrownBy (Main.sqlEval( 42 | "select midi from folder where numbarlines ? 120" 43 | )) should have message "missing {'=', '<=', '>=', '!=', '<', '>'} at '120'" 44 | the[InvalidQueryException] thrownBy (Main.sqlEval( 45 | "select midi from folder where song has got lyrics hello" 46 | )) should have message "no viable alternative at input 'songhasgot'" 47 | the[InvalidQueryException] thrownBy (Main.sqlEval( 48 | "select midi from folder where tempo > 120 XOR numbarlines = 2" 49 | )) should have message "mismatched input 'XOR' expecting {AND, OR, ';'}" 50 | the[InvalidQueryException] thrownBy (Main.sqlEval( 51 | "select midi from folder where tempo > 120 AND numbarlines = 2" 52 | )) should have message "missing ';' at ''" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/KeySignatureSqlSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | 9 | class KeySignatureSqlSpec extends AnyFlatSpec with should.Matchers with Inside { 10 | "sql evaluator" should "return song correctly where condition for scale type requested is actual scale type" in { 11 | Main.sqlEval("SELECT MUSICXML FROM resources where scale = major;") should equal(List("musicXMLTest.xml")) 12 | } 13 | 14 | it should "return nothing where condition for scale type requested is incorrect" in { 15 | Main.sqlEval("SELECT MUSICXML FROM resources where scale = minor;") should equal(List()) 16 | } 17 | 18 | it should "return song correctly where condition for key type requested is actual key type" in { 19 | Main.sqlEval("SELECT musicxml FROM resources where key = Eb;") should equal(List("musicXMLTest.xml")) 20 | } 21 | 22 | it should "return nothing where condition for key type requested is incorrect" in { 23 | Main.sqlEval("SELECT MUSICXML FROM resources where key = Bb;") should equal(List()) 24 | Main.sqlEval("SELECT MUSICXML FROM resources where key = F#;") should equal(List()) 25 | Main.sqlEval("SELECT MUSICXML FROM resources where key = C;") should equal(List()) 26 | } 27 | 28 | it should "return song correctly if request for key signature is correct" in { 29 | Main.sqlEval("SELECT MUSICXML FROM resources where key = Eb AND scale = major;") should equal( 30 | List("musicXMLTest.xml") 31 | ) 32 | Main.sqlEval("SELECT MUSICXML FROM resources where key = Eb OR scale = major;") should equal( 33 | List("musicXMLTest.xml") 34 | ) 35 | } 36 | 37 | it should "return song correctly if request for key signature is incorrect" in { 38 | Main.sqlEval("SELECT MUSICXML FROM resources where key = Db AND scale = minor;") should equal(List()) 39 | Main.sqlEval("SELECT MUSICXML FROM resources where key = Bb OR scale = minor;") should equal(List()) 40 | } 41 | 42 | // TODO: Add more test cases 43 | } 44 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/SelectClauseSqlSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | 9 | class SelectClauseSqlSpec extends AnyFlatSpec with should.Matchers with Inside { 10 | "sql evaluator" should "return empty for non existent directory" in { 11 | Main.sqlEval("select midi from resources/nonexistent;") should equal(List()) 12 | } 13 | 14 | it should "return empty for valid direcotry with no midi" in { 15 | Main.sqlEval("select midi from resources/testmusicxmlfiles;") should equal(List()) 16 | } 17 | 18 | it should "return empty for valid direcotry with no music xml" in { 19 | Main.sqlEval("select musicxml from resources/testmidifiles;") should equal(List()) 20 | } 21 | 22 | it should "return midi files in directory as is if no where clause is present" in { 23 | Main.sqlEval("select midi from resources/testmidifiles;") should equal(List("MIDI_sample.mid")) 24 | } 25 | 26 | it should "return midi files in directory with different case of keywords" in { 27 | Main.sqlEval("SELECT MIDI FROM resources/testmidifiles;") should equal(List("MIDI_sample.mid")) 28 | } 29 | 30 | it should "return music xml files in directory as is if no where clause is present" in { 31 | Main.sqlEval("select musicxml from resources/testmusicxmlfiles;") should equal(List("musicXMLTest.xml")) 32 | } 33 | 34 | it should "return music xml files in directory with different case of keywords" in { 35 | Main.sqlEval("SELECT MUSICXML FROM resources/testmusicxmlfiles;") should equal(List("musicXMLTest.xml")) 36 | } 37 | 38 | it should "return both music xml and midi if present under directory" in { 39 | Main.sqlEval("SELECT MUSICXML, MIDI FROM resources;") should equal(List("musicXMLTest.xml", "MIDI_sample.mid")) 40 | } 41 | 42 | it should "only return music xml if only queried for music xml" in { 43 | Main.sqlEval("SELECT MUSICXML FROM resources;") should equal(List("musicXMLTest.xml")) 44 | } 45 | 46 | it should "only return midi if only queried for midi" in { 47 | Main.sqlEval("select Midi FROM resources;") should equal(List("MIDI_sample.mid")) 48 | } 49 | 50 | it should "return all files for wildcard after select clause on correct directory" in { 51 | Main.sqlEval("select * FROM resources;") should equal(List("musicXMLTest.xml", "MIDI_sample.mid")) 52 | } 53 | 54 | it should "return empty for wildcard after select clause on incorrect directory" in { 55 | Main.sqlEval("select * FROM incorrectDir;") should equal(List()) 56 | } 57 | 58 | it should "be valid syntax if there is a number in the directory" in { 59 | Main.sqlEval("select * FROM incorrectDir123;") should equal(List()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/simpleexpression/LyricsComparsionSqlSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser.simpleexpression 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | 9 | class LyricsComparsionSqlSpec extends AnyFlatSpec with should.Matchers with Inside { 10 | "sql evaluator" should "return song correctly if lyric word requested is in the song" in { 11 | Main.sqlEval("SELECT musicxml FROM resources where SONG has lyrics something;") should equal( 12 | List("musicXMLTest.xml") 13 | ) 14 | } 15 | 16 | it should "return song correctly if lyric words requested is in the song" in { 17 | Main.sqlEval("SELECT musicxml FROM resources where SONG has lyrics something, heil;") should equal( 18 | List("musicXMLTest.xml") 19 | ) 20 | } 21 | 22 | it should "return nothing if lyrics requested are not in the song" in { 23 | Main.sqlEval("SELECT musicxml FROM resources where SONG has lyrics wrong, words;") should equal(List()) 24 | } 25 | 26 | it should "return nothing if lyrics requested but there are no lyrics in the song" in { 27 | Main.sqlEval("SELECT midi FROM resources where SONG has lyrics something;") should equal(List()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/simpleexpression/NumBarsComparisonSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser.simpleexpression 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | 9 | class NumBarsComparisonSpec extends AnyFlatSpec with should.Matchers with Inside { 10 | "sql evaluator" should "return song correctly if num bars queried is equal to the number of bars in the song" in { 11 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines = 2;") should equal(List("musicXMLTest.xml")) 12 | } 13 | 14 | it should "return song correctly if song's num of bars is less than requested num of bars" in { 15 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines < 3;") should equal(List("musicXMLTest.xml")) 16 | } 17 | 18 | it should "return song correctly if song's num of bars is greater than requested num of bars" in { 19 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines > 1;") should equal(List("musicXMLTest.xml")) 20 | } 21 | 22 | it should "return song correctly if song's num of bars is not equal to requested num of bars" in { 23 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines != 1;") should equal(List("musicXMLTest.xml")) 24 | } 25 | 26 | it should "return song correctly if song's num of bars is less than or equal to requested num of bars" in { 27 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines <= 3;") should equal(List("musicXMLTest.xml")) 28 | } 29 | 30 | it should "return song correctly if song's num of bars is greater than or equal to than requested num of bars" in { 31 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines >= 1;") should equal(List("musicXMLTest.xml")) 32 | } 33 | 34 | it should "return nothing if num bars is not present in the song" in { 35 | Main.sqlEval("SELECT midi FROM resources where numbarlines = 1;") should equal(List()) 36 | } 37 | 38 | it should "return nothing if song num bar lines requested is greater than its value" in { 39 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines > 3;") should equal(List()) 40 | } 41 | 42 | it should "return nothing if song num bar lines requested is less than its value" in { 43 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines < 1;") should equal(List()) 44 | } 45 | 46 | it should "return nothing if song num bar lines requested with not equals comparator actually is the requested value" in { 47 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines != 2;") should equal(List()) 48 | } 49 | 50 | it should "return nothing if song num bar lines requested is greater or equal to than its value" in { 51 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines >= 3;") should equal(List()) 52 | } 53 | 54 | it should "return nothing if song num bar lines requested is less than or equal to than its value" in { 55 | Main.sqlEval("SELECT musicxml FROM resources where numbarlines <= 0;") should equal(List()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/simpleexpression/NumTracksComparsionSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser.simpleexpression 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | import org.modulo12.sql.SqlParser 9 | 10 | class NumTracksComparsionSpec extends AnyFlatSpec with should.Matchers with Inside { 11 | "sql evaluator" should "return song correctly if num tracks queried is equal to the number of tracks in the song" in { 12 | Main.sqlEval("SELECT musicxml FROM resources where numtracks = 1;") should equal(List("musicXMLTest.xml")) 13 | Main.sqlEval("SELECT midi FROM resources where numtracks = 4;") should equal(List("MIDI_sample.mid")) 14 | } 15 | 16 | it should "return song correctly if song's num of tracks is less than requested num of tracks" in { 17 | Main.sqlEval("SELECT * FROM resources where numtracks < 6;") should equal( 18 | List("musicXMLTest.xml", "MIDI_sample.mid") 19 | ) 20 | } 21 | 22 | it should "return song correctly if song's num of tracks is greater than requested num of tracks" in { 23 | Main.sqlEval("SELECT musicxml FROM resources where numtracks > 0;") should equal(List("musicXMLTest.xml")) 24 | } 25 | 26 | it should "return song correctly if song's num of tracks is not equal to requested num of tracks" in { 27 | Main.sqlEval("SELECT midi FROM resources where numtracks != 2;") should equal(List("MIDI_sample.mid")) 28 | } 29 | 30 | it should "return song correctly if song's num of tracks is less than or equal to requested num of tracks" in { 31 | Main.sqlEval("SELECT * FROM resources where numtracks <= 4;") should equal( 32 | List("musicXMLTest.xml", "MIDI_sample.mid") 33 | ) 34 | } 35 | 36 | it should "return song correctly if song's num of tracks is greater than or equal to requested num of tracks" in { 37 | Main.sqlEval("SELECT musicxml FROM resources where numtracks >= 1;") should equal(List("musicXMLTest.xml")) 38 | } 39 | 40 | it should "return nothing if song num tracks requested is greater than its value" in { 41 | Main.sqlEval("SELECT musicxml FROM resources where numtracks > 10;") should equal(List()) 42 | } 43 | 44 | it should "return nothing if song num tracks requested is less than its value" in { 45 | Main.sqlEval("SELECT musicxml FROM resources where numtracks < 1;") should equal(List()) 46 | } 47 | 48 | it should "return nothing if song num tracks requested with not equals comparator actually is the num tracks value" in { 49 | Main.sqlEval("SELECT musicxml FROM resources where numtracks != 1;") should equal(List()) 50 | } 51 | 52 | it should "return nothing if song num tracks requested is greater or equal to than its value" in { 53 | Main.sqlEval("SELECT musicxml FROM resources where numtracks >= 3;") should equal(List()) 54 | } 55 | 56 | it should "return nothing if song num tracks requested is less than or equal to than its value" in { 57 | Main.sqlEval("SELECT musicxml FROM resources where numtracks <= 0;") should equal(List()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/scala/org/modulo12/parser/simpleexpression/TempoComparisonSqlSpec.scala: -------------------------------------------------------------------------------- 1 | package org.modulo12.parser.simpleexpression 2 | 3 | import org.scalatest.Inside 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should 6 | import org.antlr.v4.runtime.ANTLRInputStream 7 | import org.modulo12.Main 8 | import org.modulo12.sql.SqlParser 9 | 10 | class TempoComparisonSqlSpec extends AnyFlatSpec with should.Matchers with Inside { 11 | "sql parser" should "return song correctly if song tempo is greater than requested tempo" in { 12 | Main.sqlEval("SELECT midi FROM resources where tempo > 90;") should equal(List("MIDI_sample.mid")) 13 | } 14 | 15 | it should "return song correctly if song tempo is less than requested tempo" in { 16 | Main.sqlEval("SELECT midi FROM resources where tempo < 150;") should equal(List("MIDI_sample.mid")) 17 | } 18 | 19 | it should "return song correctly if song tempo is equal to requested tempo" in { 20 | Main.sqlEval("SELECT midi FROM resources where tempo = 120;") should equal(List("MIDI_sample.mid")) 21 | } 22 | 23 | it should "return song correctly if song tempo is greater than or equal to requested tempo" in { 24 | Main.sqlEval("SELECT midi FROM resources where tempo >= 120;") should equal(List("MIDI_sample.mid")) 25 | } 26 | 27 | it should "return song correctly if song tempo is less than or equal to requested tempo" in { 28 | Main.sqlEval("SELECT midi FROM resources where tempo <= 120;") should equal(List("MIDI_sample.mid")) 29 | } 30 | 31 | it should "return nothing if song tempo requested is greater than its value" in { 32 | Main.sqlEval("SELECT midi FROM resources where tempo > 130;") should equal(List()) 33 | } 34 | 35 | it should "return nothing if song tempo requested is less than its value" in { 36 | Main.sqlEval("SELECT midi FROM resources where tempo < 110;") should equal(List()) 37 | } 38 | 39 | it should "return nothing if song tempo requested with not equals comparator actually is the requested tempo value" in { 40 | Main.sqlEval("SELECT midi FROM resources where tempo != 120;") should equal(List()) 41 | } 42 | 43 | it should "return nothing if song tempo requested is greater or equal to than its value" in { 44 | Main.sqlEval("SELECT midi FROM resources where tempo >= 130;") should equal(List()) 45 | } 46 | 47 | it should "return nothing if song tempo requested is less than or equal to its value" in { 48 | Main.sqlEval("SELECT midi FROM resources where tempo < 110;") should equal(List()) 49 | } 50 | 51 | it should "return nothing if we try to compare temp and there is no tempo in the song" in { 52 | Main.sqlEval("SELECT musicxml FROM resources where tempo = 110;") should equal(List()) 53 | } 54 | } 55 | --------------------------------------------------------------------------------