├── .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 | [](https://github.com/Khalian/Modulo12/actions?query=workflow%3A"Modulo12+CI") [](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 |
--------------------------------------------------------------------------------