From 7fa871bc43c39d99b19320801c30fb63ab9b2492 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 20 Oct 2023 20:09:20 +0900 Subject: [PATCH 01/23] :hammer: prepare --- .../Sounds/Score/Parsers/IScoreParser.cs | 7 +++++++ .../Sounds/Score/Parsers/SMSParser.cs | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs create mode 100644 src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs diff --git a/src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs b/src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs new file mode 100644 index 0000000..a70bf76 --- /dev/null +++ b/src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs @@ -0,0 +1,7 @@ +using SoundMaker.Sounds.Score; + +namespace SoundMaker.Sounds.Score.Parsers; +public interface IScoreParser +{ + IEnumerable Parse(); +} diff --git a/src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs b/src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs new file mode 100644 index 0000000..29f3fd0 --- /dev/null +++ b/src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs @@ -0,0 +1,17 @@ +using System.Text.RegularExpressions; + +namespace SoundMaker.Sounds.Score.Parsers; +public class SMSParser : IScoreParser +{ + public SMSParser(string data) + { + this.Data = data; + } + + private string Data { get; } + + public IEnumerable Parse() + { + throw new NotImplementedException(); + } +} From f4ce3c8d97bb738b7dad6837f3e58bac64b29984 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Mon, 26 Feb 2024 23:42:37 +0900 Subject: [PATCH 02/23] :hammer: prepare --- .editorconfig | 78 +++++++++++++++++++++++++++++++++----------------- SoundMaker.sln | 6 ++++ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/.editorconfig b/.editorconfig index 95e7bc9..8419f59 100644 --- a/.editorconfig +++ b/.editorconfig @@ -81,14 +81,14 @@ csharp_style_var_for_built_in_types = true:silent csharp_style_var_when_type_is_apparent = true:silent # 式のようなメンバー -csharp_style_expression_bodied_accessors = true -csharp_style_expression_bodied_constructors = false -csharp_style_expression_bodied_indexers = true -csharp_style_expression_bodied_lambdas = true -csharp_style_expression_bodied_local_functions = false -csharp_style_expression_bodied_methods = false -csharp_style_expression_bodied_operators = false -csharp_style_expression_bodied_properties = true +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent # パターン マッチング設定 csharp_style_pattern_matching_over_as_with_null_check = true:suggestion @@ -102,7 +102,7 @@ csharp_style_prefer_switch_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # 修飾子設定 -csharp_prefer_static_local_function = true +csharp_prefer_static_local_function = true:suggestion csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async # コード ブロックの設定 @@ -113,27 +113,27 @@ csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent # 式レベルの設定 -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_local_over_anonymous_function = true -csharp_style_prefer_null_check_over_type_check = true -csharp_style_prefer_range_operator = true -csharp_style_prefer_tuple_swap = true -csharp_style_prefer_utf8_string_literals = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent # 'using' ディレクティブの基本設定 csharp_using_directive_placement = outside_namespace:silent # 改行設定 -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true -csharp_style_allow_embedded_statements_on_same_line_experimental = true +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent #### C# 書式ルール #### @@ -224,6 +224,10 @@ dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent [*.{cs,vb}] tab_width = 4 @@ -237,5 +241,27 @@ dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent dotnet_style_operator_placement_when_wrapping = beginning_of_line +end_of_line = crlf dotnet_code_quality_unused_parameters = all:suggestion -end_of_line = crlf \ No newline at end of file +dotnet_style_readonly_field = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_collection_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = false:silent +dotnet_style_prefer_conditional_expression_over_return = false:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent \ No newline at end of file diff --git a/SoundMaker.sln b/SoundMaker.sln index 5472aa1..2606294 100644 --- a/SoundMaker.sln +++ b/SoundMaker.sln @@ -9,6 +9,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundMakerTests", "test\Sou EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundMakerConsole", "src\SoundMakerConsole\SoundMakerConsole.csproj", "{56C904EC-478C-4219-9599-63C1F2041DB4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ソリューション項目", "ソリューション項目", "{A79A7D6C-7F61-4B70-B921-18678D37B354}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From c8db9d9cca1f31af680288c3d5548f125fb40bf7 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:48:25 +0900 Subject: [PATCH 03/23] :+1: remove old implements --- .../Sounds/Score/Parsers/IScoreParser.cs | 7 ------- .../Sounds/Score/Parsers/SMSParser.cs | 17 ----------------- 2 files changed, 24 deletions(-) delete mode 100644 src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs delete mode 100644 src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs diff --git a/src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs b/src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs deleted file mode 100644 index a70bf76..0000000 --- a/src/SoundMaker/Sounds/Score/Parsers/IScoreParser.cs +++ /dev/null @@ -1,7 +0,0 @@ -using SoundMaker.Sounds.Score; - -namespace SoundMaker.Sounds.Score.Parsers; -public interface IScoreParser -{ - IEnumerable Parse(); -} diff --git a/src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs b/src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs deleted file mode 100644 index 29f3fd0..0000000 --- a/src/SoundMaker/Sounds/Score/Parsers/SMSParser.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.RegularExpressions; - -namespace SoundMaker.Sounds.Score.Parsers; -public class SMSParser : IScoreParser -{ - public SMSParser(string data) - { - this.Data = data; - } - - private string Data { get; } - - public IEnumerable Parse() - { - throw new NotImplementedException(); - } -} From d82600958b810966bc3eb41733e7b463f61a4810 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:50:09 +0900 Subject: [PATCH 04/23] :arrow_double_down: lexer --- src/SoundMaker/ScoreData/SMSC/Lexer.cs | 107 +++++++++++++++++++++ src/SoundMaker/ScoreData/SMSC/Token.cs | 13 +++ src/SoundMaker/ScoreData/SMSC/TokenType.cs | 19 ++++ 3 files changed, 139 insertions(+) create mode 100644 src/SoundMaker/ScoreData/SMSC/Lexer.cs create mode 100644 src/SoundMaker/ScoreData/SMSC/Token.cs create mode 100644 src/SoundMaker/ScoreData/SMSC/TokenType.cs diff --git a/src/SoundMaker/ScoreData/SMSC/Lexer.cs b/src/SoundMaker/ScoreData/SMSC/Lexer.cs new file mode 100644 index 0000000..d44aeaf --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/Lexer.cs @@ -0,0 +1,107 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace SoundMaker.ScoreData.SMSC; +internal class Lexer +{ + private readonly Regex _alphaRegex = new("[a-z]|[A-z]"); + + private readonly string _data = ""; + + public Lexer(string data) + { + _data = data; + } + + public List ReadAll() + { + var tokens = new List(); + foreach (var line in _data.Split('\n')) + { + var chars = line.ToCharArray(); + var otherTypeLiteralBuilder = new StringBuilder(); + for (var i = 0; i < chars.Length; i++) + { + char? next = i + 1 < chars.Length ? chars[i + 1] : null; + // others + if (IsOtherChar(chars[i], next)) + { + _ = otherTypeLiteralBuilder.Append(chars[i]); + } + else if (otherTypeLiteralBuilder.Length != 0) + { + var literal = otherTypeLiteralBuilder.ToString(); + var type = MatchOtherType(literal); + tokens.Add(new(type, otherTypeLiteralBuilder.ToString())); + _ = otherTypeLiteralBuilder.Clear(); + } + + // comment out + if (i + 1 < chars.Length && chars[i] is '/' && next is '/') + { + break; + } + + // space + if (char.IsWhiteSpace(chars[i])) + { + continue; + } + + // number + if (char.IsDigit(chars[i])) + { + tokens.Add(new(TokenType.Number, chars[i].ToString())); + continue; + } + + // symbols + Token token = chars[i] switch + { + '.' => new(TokenType.Dot, "."), + '#' => new(TokenType.Sharp, "#"), + '(' => new(TokenType.LeftParentheses, "("), + ')' => new(TokenType.RightParentheses, ")"), + ',' => new(TokenType.Comma, ","), + _ => new(TokenType.Unknown, chars[i].ToString()), + }; + tokens.Add(token); + } + } + return tokens; + } + + private TokenType MatchOtherType(string other) + { + if (_alphaRegex.IsMatch(other)) + { + return other switch + { + "tie" => TokenType.Tie, + "tup" => TokenType.Tuplet, + _ => TokenType.Alphabet, + }; + } + return TokenType.Unknown; + } + + private bool IsOtherChar(char character, char? nextCharacter) + { + var next = nextCharacter ?? 'a'; // 'a' is not comment prefix. + return + !IsSymbol(character) && + !IsCommentPrefix(character, next) && + !char.IsWhiteSpace(character) && + !char.IsDigit(character); + } + + private bool IsCommentPrefix(char character, char nextCharacter) + { + return character is '/' && nextCharacter is '/'; + } + + private bool IsSymbol(char character) + { + return character is '.' or '#' or '(' or ')' or ','; + } +} diff --git a/src/SoundMaker/ScoreData/SMSC/Token.cs b/src/SoundMaker/ScoreData/SMSC/Token.cs new file mode 100644 index 0000000..e5516d7 --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/Token.cs @@ -0,0 +1,13 @@ +namespace SoundMaker.ScoreData.SMSC; +internal record class Token +{ + public Token(TokenType type, string literal) + { + Type = type; + Literal = literal; + } + + public TokenType Type { get; } + + public string Literal { get; } +} diff --git a/src/SoundMaker/ScoreData/SMSC/TokenType.cs b/src/SoundMaker/ScoreData/SMSC/TokenType.cs new file mode 100644 index 0000000..d79f149 --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/TokenType.cs @@ -0,0 +1,19 @@ +namespace SoundMaker.ScoreData.SMSC; +internal enum TokenType +{ + // Symbols + Dot, // . + Sharp, // # + LeftParentheses, // ( + RightParentheses, // ) + Comma, // , + // Characters + Alphabet, + Number, + // keywords + Tie, + Tuplet, + Rest, + // ? + Unknown, +} From 2e529d9368b96e6ddd1eb4c81040c686c6b56b9d Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Wed, 28 Feb 2024 20:27:15 +0900 Subject: [PATCH 05/23] :bug: fix number and rest --- src/SoundMaker/ScoreData/SMSC/Lexer.cs | 102 ++++++++++++++++++------- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/src/SoundMaker/ScoreData/SMSC/Lexer.cs b/src/SoundMaker/ScoreData/SMSC/Lexer.cs index d44aeaf..563b15f 100644 --- a/src/SoundMaker/ScoreData/SMSC/Lexer.cs +++ b/src/SoundMaker/ScoreData/SMSC/Lexer.cs @@ -1,10 +1,12 @@ -using System.Text; +using System.Runtime.CompilerServices; +using System.Text; using System.Text.RegularExpressions; +[assembly: InternalsVisibleTo("SoundMakerTests")] namespace SoundMaker.ScoreData.SMSC; internal class Lexer { - private readonly Regex _alphaRegex = new("[a-z]|[A-z]"); + private readonly Regex _alphaRegex = new("[a-z]|[A-Z]"); private readonly string _data = ""; @@ -16,69 +18,114 @@ public Lexer(string data) public List ReadAll() { var tokens = new List(); - foreach (var line in _data.Split('\n')) + var data = _data.Replace("\r\n", "\n").Replace('\r', '\n'); + var lines = data.Split('\n'); + for (var i = 0; i < lines.Length; i++) { + var line = lines[i]; + var lineNumber = i + 1; var chars = line.ToCharArray(); var otherTypeLiteralBuilder = new StringBuilder(); - for (var i = 0; i < chars.Length; i++) + var numberLiteralBuilder = new StringBuilder(); + for (var j = 0; j < chars.Length; j++) { - char? next = i + 1 < chars.Length ? chars[i + 1] : null; + char? next = j + 1 < chars.Length ? chars[j + 1] : null; + // numbers + if (char.IsNumber(chars[j])) + { + _ = numberLiteralBuilder.Append(chars[j]); + if (otherTypeLiteralBuilder.Length != 0) + { + var literal = otherTypeLiteralBuilder.ToString(); + var type = MatchOtherType(literal); + tokens.Add(new(type, otherTypeLiteralBuilder.ToString(), lineNumber)); + _ = otherTypeLiteralBuilder.Clear(); + } + continue; + } // others - if (IsOtherChar(chars[i], next)) + if (IsOtherChar(chars[j], next)) + { + _ = otherTypeLiteralBuilder.Append(chars[j]); + if (numberLiteralBuilder.Length != 0) + { + var literal = numberLiteralBuilder.ToString(); + tokens.Add(new(TokenType.Number, literal, lineNumber)); + _ = numberLiteralBuilder.Clear(); + } + continue; + } + + if (numberLiteralBuilder.Length != 0) { - _ = otherTypeLiteralBuilder.Append(chars[i]); + var literal = numberLiteralBuilder.ToString(); + tokens.Add(new(TokenType.Number, literal, lineNumber)); + _ = numberLiteralBuilder.Clear(); } - else if (otherTypeLiteralBuilder.Length != 0) + if (otherTypeLiteralBuilder.Length != 0) { var literal = otherTypeLiteralBuilder.ToString(); var type = MatchOtherType(literal); - tokens.Add(new(type, otherTypeLiteralBuilder.ToString())); + tokens.Add(new(type, literal, lineNumber)); _ = otherTypeLiteralBuilder.Clear(); } // comment out - if (i + 1 < chars.Length && chars[i] is '/' && next is '/') + if (j + 1 < chars.Length && IsCommentPrefix(chars[j], next)) { break; } // space - if (char.IsWhiteSpace(chars[i])) + if (char.IsWhiteSpace(chars[j])) { continue; } - // number - if (char.IsDigit(chars[i])) - { - tokens.Add(new(TokenType.Number, chars[i].ToString())); - continue; - } - // symbols - Token token = chars[i] switch + Token token = chars[j] switch { - '.' => new(TokenType.Dot, "."), - '#' => new(TokenType.Sharp, "#"), - '(' => new(TokenType.LeftParentheses, "("), - ')' => new(TokenType.RightParentheses, ")"), - ',' => new(TokenType.Comma, ","), - _ => new(TokenType.Unknown, chars[i].ToString()), + '.' => new(TokenType.Dot, ".", lineNumber), + '#' => new(TokenType.Sharp, "#", lineNumber), + '(' => new(TokenType.LeftParentheses, "(", lineNumber), + ')' => new(TokenType.RightParentheses, ")", lineNumber), + ',' => new(TokenType.Comma, ",", lineNumber), + _ => new(TokenType.Unknown, chars[j].ToString(), lineNumber), }; tokens.Add(token); } + + if (numberLiteralBuilder.Length != 0) + { + var literal = numberLiteralBuilder.ToString(); + tokens.Add(new(TokenType.Number, literal, lineNumber)); + _ = numberLiteralBuilder.Clear(); + } + + if (otherTypeLiteralBuilder.Length != 0) + { + var literal = otherTypeLiteralBuilder.ToString(); + var type = MatchOtherType(literal); + tokens.Add(new(type, literal, lineNumber)); + _ = otherTypeLiteralBuilder.Clear(); + } } return tokens; } private TokenType MatchOtherType(string other) { + if (int.TryParse(other, out _)) + { + return TokenType.Number; + } if (_alphaRegex.IsMatch(other)) { return other switch { "tie" => TokenType.Tie, "tup" => TokenType.Tuplet, + "rest" => TokenType.Rest, _ => TokenType.Alphabet, }; } @@ -91,11 +138,10 @@ private bool IsOtherChar(char character, char? nextCharacter) return !IsSymbol(character) && !IsCommentPrefix(character, next) && - !char.IsWhiteSpace(character) && - !char.IsDigit(character); + !char.IsWhiteSpace(character); } - private bool IsCommentPrefix(char character, char nextCharacter) + private bool IsCommentPrefix(char character, char? nextCharacter) { return character is '/' && nextCharacter is '/'; } From 9bdc08c321998d0d877fd5ab6049fae4248d77ea Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sat, 9 Mar 2024 01:12:37 +0900 Subject: [PATCH 06/23] :arrow_double_down: impliment Parser --- src/SoundMaker/ScoreData/SMSC/Error.cs | 2 + src/SoundMaker/ScoreData/SMSC/Parser.cs | 413 ++++++++++++++++++ src/SoundMaker/ScoreData/SMSC/SMSC.bnf | 32 ++ .../ScoreData/SMSC/SMSCReadErrorType.cs | 14 + .../ScoreData/SMSC/SMSCReadResult.cs | 50 +++ src/SoundMaker/ScoreData/SMSC/Token.cs | 5 +- src/SoundMaker/ScoreData/SMSC/TokenType.cs | 2 + 7 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 src/SoundMaker/ScoreData/SMSC/Error.cs create mode 100644 src/SoundMaker/ScoreData/SMSC/Parser.cs create mode 100644 src/SoundMaker/ScoreData/SMSC/SMSC.bnf create mode 100644 src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs create mode 100644 src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs diff --git a/src/SoundMaker/ScoreData/SMSC/Error.cs b/src/SoundMaker/ScoreData/SMSC/Error.cs new file mode 100644 index 0000000..7fa3297 --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/Error.cs @@ -0,0 +1,2 @@ +namespace SoundMaker.ScoreData.SMSC; +public record Error(SMSCReadErrorType Type, int LineNumber); diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs new file mode 100644 index 0000000..79f4040 --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -0,0 +1,413 @@ +using SoundMaker.Sounds.Score; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("SoundMakerTests")] + +namespace SoundMaker.ScoreData.SMSC; + +internal class TokenQueue +{ + private readonly Queue _tokens; + + public TokenQueue(IEnumerable tokens) + { + _tokens = new(tokens); + } + + public Token? PrevToken { get; private set; } + + public int Count => _tokens.Count; + + public bool TryDequeue([MaybeNullWhen(false)] out Token token) + { + if (_tokens.TryDequeue(out token)) + { + PrevToken = token; + return true; + } + return false; + } + + public Token Dequeue() + { + return _tokens.Dequeue(); + } + + public bool TryPeek([MaybeNullWhen(false)] out Token token) + { + return _tokens.TryPeek(out token); + } +} +internal class Parser +{ + private record LengthResult(LengthType LengthType, bool IsDotted); + + private record ScaleResult(Scale Scale, int ScaleNumber); + + private record ParseResult(T? Value, Error? Error) where T : class + { + public bool TryGetValue([MaybeNullWhen(false)] out T value) + { + if (Value is null || Error is not null) + { + value = null; + return false; + } + value = Value; + return true; + } + } + + private readonly TokenQueue _tokens; + + public Parser(IEnumerable tokens) + { + _tokens = new(tokens); + } + + public List Parse() + { + var statements = new List(); + while (_tokens.Count > 0) + { + var statementResult = ParseStatement(); + if (statementResult.TryGetValue(out var statement)) + { + statements.Add(statement); + } + } + return statements; + } + + private ParseResult ParseStatement() + { + if (!_tokens.TryPeek(out var current)) + { + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + } + var statementResult = current.Type switch + { + TokenType.Tie => ParseTie(), + TokenType.Tuplet => ParseTuplet(), + TokenType.Rest => ParseRest(), + _ => ParseNote() + }; + return statementResult; + } + + private ParseResult ParseTie() + { + + if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Tie) + { + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + } + if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.LeftParentheses) + { + return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + } + var scaleResult = ParseScale(); + if (!scaleResult.TryGetValue(out var scale)) + { + return new(null, scaleResult.Error); + } + + var notes = new List(); + // 引数内の長さを表す情報を解析する + while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses) + { + if (current.Type is TokenType.Comma) + { + _ = _tokens.Dequeue(); + } + else + { + return new(null, new(SMSCReadErrorType.NotFoundComma, current.LineNumber)); + } + + var lengthResult = ParseLength(); + if (!lengthResult.TryGetValue(out var length)) + { + return new(null, lengthResult.Error); + } + + var note = new Note(scale.Scale, scale.ScaleNumber, length.LengthType, length.IsDotted); + notes.Add(note); + } + // トークンが空になっている場合、')'が存在していない + if (_tokens.Count == 0) + { + return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + } + // ')'を破棄 + _ = _tokens.Dequeue(); + + Tie tie; + if (notes.Count == 0) + { + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + } + else if (notes.Count == 1) + { + tie = new Tie(notes[0], new List()); + } + else + { + tie = new Tie(notes[0], notes.GetRange(1, notes.Count - 1)); + } + + return new(tie, null); + } + + private ParseResult ParseTuplet() + { + if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Tuplet) + { + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + } + if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.LeftParentheses) + { + return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + } + var lengthResult = ParseLength(); + + var components = new List(); + while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses) + { + if (current.Type is TokenType.Comma) + { + _ = _tokens.Dequeue(); + } + else + { + return new(null, new(SMSCReadErrorType.NotFoundComma, current.LineNumber)); + } + + if (!_tokens.TryPeek(out current)) + { + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + } + + if (current.Type is TokenType.Tie) + { + var componentResult = ParseTie(); + if (!componentResult.TryGetValue(out var component)) + { + return new(null, componentResult.Error); + } + components.Add(component); + } + else if (current.Type is TokenType.Tuplet) + { + var componentResult = ParseTuplet(); + if (!componentResult.TryGetValue(out var component)) + { + return new(null, componentResult.Error); + } + components.Add(component); + } + else if (current.Type is TokenType.Rest) + { + _ = _tokens.Dequeue(); + var isDottedRest = false; + if (_tokens.TryPeek(out var dotToken) && dotToken.Type is TokenType.Dot) + { + _ = _tokens.Dequeue(); + isDottedRest = true; + } + var rest = new Rest(LengthType.Whole, isDottedRest); + components.Add(rest); + } + else + { + var scaleResult = ParseScale(); + if (!scaleResult.TryGetValue(out var scale)) + { + return new(null, scaleResult.Error); + } + var isDottedNote = false; + if (_tokens.TryPeek(out var dotToken) && dotToken.Type is TokenType.Dot) + { + _ = _tokens.Dequeue(); + isDottedNote = true; + } + var note = new Note(scale.Scale, scale.ScaleNumber, LengthType.Whole, isDottedNote); + components.Add(note); + } + } + + // トークンが空になっている場合、')'が存在していない + if (_tokens.Count == 0) + { + return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + } + // ')'を破棄 + _ = _tokens.Dequeue(); + + if (!lengthResult.TryGetValue(out var length)) + { + return new(null, lengthResult.Error); + } + var tuplet = new Tuplet(components, length.LengthType, length.IsDotted); + return new(tuplet, null); + } + + private ParseResult ParseNote() + { + var scaleResult = ParseScale(); + if (!scaleResult.TryGetValue(out var scale)) + { + return new(null, scaleResult.Error); + } + if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Comma) + { + return new(null, new Error(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken?.LineNumber ?? 0)); + } + var lengthResult = ParseLength(); + if (!lengthResult.TryGetValue(out var length)) + { + return new(null, lengthResult.Error); + } + var note = new Note(scale.Scale, scale.ScaleNumber, length.LengthType, length.IsDotted); + return new(note, null); + } + + private ParseResult ParseRest() + { + if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Rest) + { + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + } + if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.Comma) + { + return new(null, new(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken?.LineNumber ?? 0)); + } + var lengthResult = ParseLength(); + if (!lengthResult.TryGetValue(out var length)) + { + return new(null, lengthResult.Error); + } + var rest = new Rest(length.LengthType, length.IsDotted); + return new(rest, null); + } + + /// + /// 音の高さを解析する。例) C4, C#4 + /// + /// + private ParseResult ParseScale() + { + if (!_tokens.TryDequeue(out var current) || !_tokens.TryDequeue(out var next)) + { + return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, _tokens.PrevToken?.LineNumber ?? 0)); + } + + Scale? scaleNullable; + if (current.Type is not TokenType.Alphabet) + { + return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + } + + int scaleNumber; + if (next.Type is TokenType.Sharp) + { + if (!_tokens.TryDequeue(out var nextNext)) + { + return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, next.LineNumber)); + } + if (nextNext.Type is not TokenType.Number) + { + return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, nextNext.LineNumber)); + } + + scaleNullable = current.Literal switch + { + "A" or "a" => Scale.ASharp, + "C" or "c" => Scale.CSharp, + "D" or "d" => Scale.DSharp, + "F" or "f" => Scale.FSharp, + "G" or "g" => Scale.GSharp, + _ => null, + }; + + if (scaleNullable is null) + { + return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + } + + scaleNumber = int.Parse(nextNext.Literal); + + var scaleResult = new ScaleResult(scaleNullable.Value, scaleNumber); + return new(scaleResult, null); + } + else if (next.Type is TokenType.Number) + { + scaleNumber = int.Parse(next.Literal); + scaleNullable = current.Literal switch + { + "A" or "a" => Scale.A, + "B" or "b" => Scale.B, + "C" or "c" => Scale.C, + "D" or "d" => Scale.D, + "E" or "e" => Scale.E, + "F" or "f" => Scale.F, + "G" or "g" => Scale.G, + _ => null, + }; + + if (scaleNullable is null) + { + return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + } + + var scaleResult = new ScaleResult(scaleNullable.Value, scaleNumber); + return new(scaleResult, null); + } + + return new(null, new(SMSCReadErrorType.InvalidScale, current.LineNumber)); + } + + private ParseResult ParseLength() + { + LengthType? lengthTypeNullable; + var isDotted = false; + + if (!_tokens.TryDequeue(out var current)) + { + return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken?.LineNumber ?? 0)); + } + + if (current.Type is not TokenType.Number) + { + return new(null, new Error(SMSCReadErrorType.InvalidLength, current.LineNumber)); + } + + lengthTypeNullable = int.Parse(current.Literal) switch + { + 1 => LengthType.Whole, + 2 => LengthType.Half, + 4 => LengthType.Quarter, + 8 => LengthType.Eighth, + 16 => LengthType.Sixteenth, + 32 => LengthType.ThirtySecond, + 64 => LengthType.SixtyFourth, + _ => null, + }; + + if (_tokens.TryPeek(out current) && current.Type is TokenType.Dot) + { + if (_tokens.TryDequeue(out current) && current.Type is TokenType.Dot) + { + isDotted = true; + } + } + + if (lengthTypeNullable is null) + { + return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken?.LineNumber ?? 0)); + } + + var length = new LengthResult(lengthTypeNullable.Value, isDotted); + return new(length, null); + } +} diff --git a/src/SoundMaker/ScoreData/SMSC/SMSC.bnf b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf new file mode 100644 index 0000000..ec2c43b --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf @@ -0,0 +1,32 @@ +# 音階を表すアルファベット('C4'の'C'の部分) - Scale symbol (the 'C' part of 'C4') +scaleSymbol ::= A|B|C|D|E|F|G + +# 音階の数字('C4'の'4'の部分) - Scale number (the '4' part of 'C4') +scaleNumber ::= 0|1|2|3|4|5|6|7|8 + +# 音の長さの数字(全音符: 1, 16分音符: 16) - Length number (whole note: 1, sixteenth note: 16) +lengthCount ::= 1|2|4|8|16|32|64 + +# 長さを表す(16.など) - Length representation (16., etc.) +length ::= '.'? + +# 音階(C#4など) - Scale (C#4, etc.) +scale ::= '#'? + +# 音符 - Note +note ::= ',' + +# 休符 - Rest +rest ::= 'rest' ',' + +# タイの表記方法 - Tie notation +tie ::= 'tie' '(' ',' |'rest' {',' |'rest'}* ')' + +# 連符の表記方法 - Tuplet notation +tuplet ::= 'tup' '(' ',' { '.'?}|{'rest' '.'?}| {',' '.'?}* ')' + +# 文の定義 - Sentence definition +statement ::= ||| + +# SMSC(SoundMaker SCore)データ - SMSC(SoundMaker SCore) data +smsc ::= * diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs b/src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs new file mode 100644 index 0000000..7e98b45 --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs @@ -0,0 +1,14 @@ +namespace SoundMaker.ScoreData.SMSC; + +public enum SMSCReadErrorType +{ + // OOが無い + NotFoundComma, + NotFoundLeftParentheses, + NotFoundRightParentheses, + // 不正 + InvalidLength, + InvalidScale, + // 未定義の文 + UndefinedStatement, +} diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs b/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs new file mode 100644 index 0000000..3cb54b5 --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs @@ -0,0 +1,50 @@ +using SoundMaker.Sounds.Score; +using System.Diagnostics.CodeAnalysis; + +namespace SoundMaker.ScoreData.SMSC; +public class SMSCReadResult +{ + private readonly IReadOnlyList? _value; + + private SMSCReadResult(IReadOnlyList? value, IReadOnlyList errors) + { + _value = value; + Errors = errors; + } + + internal static SMSCReadResult Success(IReadOnlyList value) + { + return new SMSCReadResult(value, Array.Empty()); + } + + internal static SMSCReadResult Failure(IReadOnlyList errors) + { + return new SMSCReadResult(null, errors); + } + + public IReadOnlyList Errors { get; } + + public bool IsSuccess => Errors.Count is 0; + + public bool TryGetValue([MaybeNullWhen(false)] out IReadOnlyList value) + { + if (_value is not null && IsSuccess) + { + value = _value; + return true; + } + + value = null; + return false; + } + + public IReadOnlyList Unwrap() + { + if (!IsSuccess || _value is null) + { + throw new NullReferenceException("The result does not contain a valid value."); + } + + return _value; + } +} diff --git a/src/SoundMaker/ScoreData/SMSC/Token.cs b/src/SoundMaker/ScoreData/SMSC/Token.cs index e5516d7..6e48aef 100644 --- a/src/SoundMaker/ScoreData/SMSC/Token.cs +++ b/src/SoundMaker/ScoreData/SMSC/Token.cs @@ -1,13 +1,16 @@ namespace SoundMaker.ScoreData.SMSC; internal record class Token { - public Token(TokenType type, string literal) + public Token(TokenType type, string literal, int lineNumber) { Type = type; Literal = literal; + LineNumber = lineNumber; } public TokenType Type { get; } public string Literal { get; } + + public int LineNumber { get; } } diff --git a/src/SoundMaker/ScoreData/SMSC/TokenType.cs b/src/SoundMaker/ScoreData/SMSC/TokenType.cs index d79f149..46a6fb2 100644 --- a/src/SoundMaker/ScoreData/SMSC/TokenType.cs +++ b/src/SoundMaker/ScoreData/SMSC/TokenType.cs @@ -14,6 +14,8 @@ internal enum TokenType Tie, Tuplet, Rest, + // line break + LineBreak, // ? Unknown, } From 01276be666d40263a247820331fbd0a985cfd35a Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sat, 9 Mar 2024 01:13:21 +0900 Subject: [PATCH 07/23] :+1: Add tests for lexer and parser --- test/UnitTests/ScoreData/SMSC/TestLexer.cs | 95 +++++++++ test/UnitTests/ScoreData/SMSC/TestParser.cs | 201 ++++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 test/UnitTests/ScoreData/SMSC/TestLexer.cs create mode 100644 test/UnitTests/ScoreData/SMSC/TestParser.cs diff --git a/test/UnitTests/ScoreData/SMSC/TestLexer.cs b/test/UnitTests/ScoreData/SMSC/TestLexer.cs new file mode 100644 index 0000000..0b10191 --- /dev/null +++ b/test/UnitTests/ScoreData/SMSC/TestLexer.cs @@ -0,0 +1,95 @@ +using SoundMaker.ScoreData.SMSC; + +namespace SoundMakerTests.UnitTests.ScoreData.SMSC; + +public class TestLexer +{ + [Fact(DisplayName = "コメントは解析しないか")] + public void TestReadAll_Comment() + { + var data = @"// Comment"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + Assert.Empty(tokens); + } + + [Fact(DisplayName = "通常の音符を解析できるか")] + public void TestReadAll_Note() + { + var data = @"// Note +C4, 16 +"; + var lexer = new Lexer(data); + var expected = new List() + { + new(TokenType.Alphabet, "C", 2), + new(TokenType.Number, "4", 2), + new(TokenType.Comma, ",", 2), + new(TokenType.Number, "16", 2), + }; + var tokens = lexer.ReadAll(); + Assert.Equal(expected.Count, tokens.Count); + + AssertToken(expected[0], tokens[0]); + AssertToken(expected[1], tokens[1]); + AssertToken(expected[2], tokens[2]); + AssertToken(expected[3], tokens[3]); + } + + [Fact(DisplayName = "付点音符を解析できるか")] + public void TestReadAll_DottedNote() + { + var data = @"// Note +C4, 16. +"; + var lexer = new Lexer(data); + var expected = new List() + { + new(TokenType.Alphabet, "C", 2), + new(TokenType.Number, "4", 2), + new(TokenType.Comma, ",", 2), + new(TokenType.Number, "16", 2), + new(TokenType.Dot, ".", 2), + }; + var tokens = lexer.ReadAll(); + Assert.Equal(expected.Count, tokens.Count); + + AssertToken(expected[0], tokens[0]); + AssertToken(expected[1], tokens[1]); + AssertToken(expected[2], tokens[2]); + AssertToken(expected[3], tokens[3]); + AssertToken(expected[4], tokens[4]); + } + + [Fact(DisplayName = "半音上の音符を解析できるか")] + public void TestReadAll_SharpNote() + { + var data = @"// Note +C#4, 16 +"; + var lexer = new Lexer(data); + var expected = new List() + { + new(TokenType.Alphabet, "C", 2), + new(TokenType.Sharp, "#", 2), + new(TokenType.Number, "4", 2), + new(TokenType.Comma, ",", 2), + new(TokenType.Number, "16", 2), + }; + var tokens = lexer.ReadAll(); + Assert.Equal(expected.Count, tokens.Count); + + AssertToken(expected[0], tokens[0]); + AssertToken(expected[1], tokens[1]); + AssertToken(expected[2], tokens[2]); + AssertToken(expected[3], tokens[3]); + AssertToken(expected[4], tokens[4]); + } + + private void AssertToken(Token expected, Token actual) + { + Assert.Equal(expected.Type, actual.Type); + Assert.Equal(expected.Literal, actual.Literal); + Assert.Equal(expected.LineNumber, actual.LineNumber); + } +} diff --git a/test/UnitTests/ScoreData/SMSC/TestParser.cs b/test/UnitTests/ScoreData/SMSC/TestParser.cs new file mode 100644 index 0000000..9188514 --- /dev/null +++ b/test/UnitTests/ScoreData/SMSC/TestParser.cs @@ -0,0 +1,201 @@ +using SoundMaker.ScoreData.SMSC; +using SoundMaker.Sounds.Score; + +namespace SoundMakerTests.UnitTests.ScoreData.SMSC; + +public class TestParser +{ + [Fact(DisplayName = "普通の音符を解析できるか")] + public void TestParse_Note() + { + var data = @"C4, 16"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var expectedCollection = new List() + { + new Note(Scale.C, 4, LengthType.Sixteenth), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Note)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Scale, actual.Scale); + Assert.Equal(expected.ScaleNumber, actual.ScaleNumber); + Assert.Equal(expected.Length, actual.Length); + Assert.Equal(expected.IsDotted, actual.IsDotted); + } + + [Fact(DisplayName = "付点音符を解析できるか")] + public void TestParse_DottedNote() + { + var data = @"C4, 16."; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var expectedCollection = new List() + { + new Note(Scale.C, 4, LengthType.Sixteenth, true), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Note)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Scale, actual.Scale); + Assert.Equal(expected.ScaleNumber, actual.ScaleNumber); + Assert.Equal(expected.Length, actual.Length); + Assert.Equal(expected.IsDotted, actual.IsDotted); + } + + [Fact(DisplayName = "半音上の音符を解析できるか")] + public void TestParse_SharpNote() + { + var data = @"C#4, 16"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var expectedCollection = new List() + { + new Note(Scale.CSharp, 4, LengthType.Sixteenth), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Note)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Scale, actual.Scale); + Assert.Equal(expected.ScaleNumber, actual.ScaleNumber); + Assert.Equal(expected.Length, actual.Length); + Assert.Equal(expected.IsDotted, actual.IsDotted); + } + + [Fact(DisplayName = "普通の休符を解析できるか")] + public void TestParse_Rest() + { + var data = @"rest, 16"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var expectedCollection = new List() + { + new Rest(LengthType.Sixteenth), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Rest)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Length, actual.Length); + Assert.Equal(expected.IsDotted, actual.IsDotted); + } + + [Fact(DisplayName = "付点休符を解析できるか")] + public void TestParse_DottedRest() + { + var data = @"rest, 16."; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var expectedCollection = new List() + { + new Rest(LengthType.Sixteenth, true), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Rest)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Length, actual.Length); + Assert.Equal(expected.IsDotted, actual.IsDotted); + } + + [Fact(DisplayName = "タイを解析できるか")] + public void TestParse_Tie() + { + var data = @"tie(C#4, 16, 8, 8, 8.)"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var expectedCollection = new List() + { + new Tie(new(Scale.CSharp, 4, LengthType.Sixteenth), new List() + { + new(LengthType.Eighth), + new(LengthType.Eighth), + new(LengthType.Eighth, true), + }), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Tie)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Count, actual.Count); + } + + [Fact(DisplayName = "連符を解析できるか")] + public void TestParse_Tuplet() + { + var data = @"tup(16, C#4, rest, E4.)"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var expectedCollection = new List() + { + new Tuplet(new List() + { + new Note(Scale.CSharp, 4, LengthType.Whole), + new Rest(LengthType.Eighth), + new Note(Scale.E, 4,LengthType.Whole, true), + }, LengthType.Sixteenth), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Tuplet)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Count, actual.Count); + } + + [Fact(DisplayName = "連符内のタイや連符を解析できるか")] + public void TestParse_TupletInTupletAndTie() + { + var data = @"tup(16, tup(16, rest, rest), tie(C#4, 16, 8, 8, 8.))"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + + var rest = new Rest(LengthType.Eighth); + var expectedCollection = new List() + { + new Tuplet(new List() + { + new Tuplet(new List() + { + rest, rest + }, LengthType.Sixteenth), + new Tie(new(Scale.CSharp, 4, LengthType.Sixteenth), new List() + { + new(LengthType.Eighth), + new(LengthType.Eighth), + new(LengthType.Eighth, true), + }), + }, LengthType.Sixteenth), + }; + var actualCollection = parser.Parse(); + Assert.Equal(expectedCollection.Count, actualCollection.Count); + + var expected = (Tuplet)expectedCollection[0]; + var actual = Assert.IsType(actualCollection[0]); + Assert.Equal(expected.Count, actual.Count); + } +} From 59fc5d82f24ba45a2e22707aad44d26951986db8 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sat, 9 Mar 2024 15:43:48 +0900 Subject: [PATCH 08/23] :+1: Add comments --- src/SoundMaker/ScoreData/SMSC/Parser.cs | 106 ++++++++++++++++++++---- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs index 79f4040..6d7447e 100644 --- a/src/SoundMaker/ScoreData/SMSC/Parser.cs +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -65,9 +65,14 @@ public Parser(IEnumerable tokens) _tokens = new(tokens); } + /// + /// 解析する + /// + /// 解析結果 public List Parse() { var statements = new List(); + _ = new List(); while (_tokens.Count > 0) { var statementResult = ParseStatement(); @@ -79,43 +84,59 @@ public List Parse() return statements; } + /// + /// 文を解析する + /// + /// 解析結果 private ParseResult ParseStatement() { + // 現在のトークンの種類を見たいだけなのでPeekする。 if (!_tokens.TryPeek(out var current)) { return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); } + // 文の解析を現在のトークンに基づいて行う。 var statementResult = current.Type switch { TokenType.Tie => ParseTie(), TokenType.Tuplet => ParseTuplet(), TokenType.Rest => ParseRest(), + // 上記に当てはまらない場合は音符として解析する。 _ => ParseNote() }; return statementResult; } + /// + /// タイを解析する + /// + /// 解析結果 private ParseResult ParseTie() { - + // トークンが'tie'かを確認する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Tie) { return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); } + // トークンが'('かを確認する if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.LeftParentheses) { return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken?.LineNumber ?? 0)); } + // 音程を解析する var scaleResult = ParseScale(); if (!scaleResult.TryGetValue(out var scale)) { return new(null, scaleResult.Error); } + // 引数内の長さを表す情報を解析する。トークンがなくなるか')'になるまで解析する。 + // 長さの情報は可変長引数として取る。 var notes = new List(); - // 引数内の長さを表す情報を解析する while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses) { + // ','の場合はDequeueする。tie(音程, 長さ, 長さ, ...)という並びなので、while文の先頭にこの処理を置く。 + // 音程解析はwhileの前で実装済みなので、", 長さ, 長さ, ..."という部分をここで解析する。 if (current.Type is TokenType.Comma) { _ = _tokens.Dequeue(); @@ -125,16 +146,18 @@ private ParseResult ParseTie() return new(null, new(SMSCReadErrorType.NotFoundComma, current.LineNumber)); } + // 長さを解析する。 var lengthResult = ParseLength(); if (!lengthResult.TryGetValue(out var length)) { return new(null, lengthResult.Error); } + // 音符を作成する。 var note = new Note(scale.Scale, scale.ScaleNumber, length.LengthType, length.IsDotted); notes.Add(note); } - // トークンが空になっている場合、')'が存在していない + // トークンが空になっている場合、')'が存在していないので、エラーを出力。 if (_tokens.Count == 0) { return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken?.LineNumber ?? 0)); @@ -142,38 +165,54 @@ private ParseResult ParseTie() // ')'を破棄 _ = _tokens.Dequeue(); + // タイを作成する。 Tie tie; + // 音符が0個の場合は解析できていない。 if (notes.Count == 0) { return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); } + // 個数が1個の場合はadditionalNotesに空配列を渡す else if (notes.Count == 1) { - tie = new Tie(notes[0], new List()); + tie = new Tie(notes[0], Array.Empty()); } + // 個数が2個以上の場合は、先頭をbaseとし、残りをadditionalNotesとする。 else { tie = new Tie(notes[0], notes.GetRange(1, notes.Count - 1)); } - + // 解析結果を返す。 return new(tie, null); } + /// + /// 連符を解析する + /// + /// 解析結果 private ParseResult ParseTuplet() { + // トークンが'tup'かを確認する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Tuplet) { return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); } + // トークンが'('かを確認する if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.LeftParentheses) { return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken?.LineNumber ?? 0)); } + // 長さを解析する var lengthResult = ParseLength(); + // 連符にするSoundComponentを解析する var components = new List(); + // 引数内の長さを表す情報を解析する。トークンがなくなるか')'になるまで解析する。 + // 情報は可変長引数として取る。 while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses) { + // ','の場合はDequeueする。tup(長さ, 音の部品, 音の部品, ...)という並びなので、while文の先頭にこの処理を置く。 + // 長さの解析はwhileの前で実装済みなので、", 音の部品, 音の部品, ..."という部分をここで解析する。 if (current.Type is TokenType.Comma) { _ = _tokens.Dequeue(); @@ -183,11 +222,13 @@ private ParseResult ParseTuplet() return new(null, new(SMSCReadErrorType.NotFoundComma, current.LineNumber)); } + // 条件分岐の為にトークンをピークする if (!_tokens.TryPeek(out current)) { return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); } + // タイの場合はタイを解析する if (current.Type is TokenType.Tie) { var componentResult = ParseTie(); @@ -197,6 +238,7 @@ private ParseResult ParseTuplet() } components.Add(component); } + // 連符の場合は連符を解析(再帰呼び出し)する。 else if (current.Type is TokenType.Tuplet) { var componentResult = ParseTuplet(); @@ -206,31 +248,39 @@ private ParseResult ParseTuplet() } components.Add(component); } + // 休符の場合は休符を解析するが、連符専用の書き方なのでここで実装する。 else if (current.Type is TokenType.Rest) { + // 'rest'のトークンを破棄 _ = _tokens.Dequeue(); var isDottedRest = false; + // '.'の場合は付点休符とする if (_tokens.TryPeek(out var dotToken) && dotToken.Type is TokenType.Dot) { _ = _tokens.Dequeue(); isDottedRest = true; } + // 連符なので長さは適当に入れる var rest = new Rest(LengthType.Whole, isDottedRest); components.Add(rest); } + // 上記以外は音符として解析するが、連符専用の書き方なのでここで実装する。 else { + // 音程を解析する var scaleResult = ParseScale(); if (!scaleResult.TryGetValue(out var scale)) { return new(null, scaleResult.Error); } var isDottedNote = false; + // '.'の場合は付点音符とする if (_tokens.TryPeek(out var dotToken) && dotToken.Type is TokenType.Dot) { _ = _tokens.Dequeue(); isDottedNote = true; } + // 連符なので長さは適当に入れる var note = new Note(scale.Scale, scale.ScaleNumber, LengthType.Whole, isDottedNote); components.Add(note); } @@ -252,17 +302,24 @@ private ParseResult ParseTuplet() return new(tuplet, null); } + /// + /// 音符を解析する + /// + /// 解析結果 private ParseResult ParseNote() { + // 音程を解析する var scaleResult = ParseScale(); if (!scaleResult.TryGetValue(out var scale)) { return new(null, scaleResult.Error); } + // ','かを判定する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Comma) { return new(null, new Error(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken?.LineNumber ?? 0)); } + // 長さを解析する var lengthResult = ParseLength(); if (!lengthResult.TryGetValue(out var length)) { @@ -272,16 +329,23 @@ private ParseResult ParseNote() return new(note, null); } + /// + /// 休符を解析する + /// + /// private ParseResult ParseRest() { + // 'rest'かを判定する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Rest) { return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); } + // ','かを判定する if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.Comma) { return new(null, new(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken?.LineNumber ?? 0)); } + // 長さを解析する var lengthResult = ParseLength(); if (!lengthResult.TryGetValue(out var length)) { @@ -297,27 +361,30 @@ private ParseResult ParseRest() /// private ParseResult ParseScale() { + // 最低でもトークンが2つ取れないと解析できないので、ここで判定する。 if (!_tokens.TryDequeue(out var current) || !_tokens.TryDequeue(out var next)) { - return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, _tokens.PrevToken?.LineNumber ?? 0)); } Scale? scaleNullable; + // 音程はアルファベットで書かれている if (current.Type is not TokenType.Alphabet) { - return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); } int scaleNumber; + // 次のトークンが'#'の場合は半音上の音程として解析する if (next.Type is TokenType.Sharp) { if (!_tokens.TryDequeue(out var nextNext)) { - return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, next.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, next.LineNumber)); } if (nextNext.Type is not TokenType.Number) { - return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, nextNext.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, nextNext.LineNumber)); } scaleNullable = current.Literal switch @@ -332,7 +399,7 @@ private ParseResult ParseScale() if (scaleNullable is null) { - return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); } scaleNumber = int.Parse(nextNext.Literal); @@ -340,6 +407,7 @@ private ParseResult ParseScale() var scaleResult = new ScaleResult(scaleNullable.Value, scaleNumber); return new(scaleResult, null); } + // 次のトークンが数字の場合はナチュラルな音程 else if (next.Type is TokenType.Number) { scaleNumber = int.Parse(next.Literal); @@ -357,16 +425,20 @@ private ParseResult ParseScale() if (scaleNullable is null) { - return new ParseResult(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); } var scaleResult = new ScaleResult(scaleNullable.Value, scaleNumber); return new(scaleResult, null); } - + // 上記条件に当てはまらない場合はエラー return new(null, new(SMSCReadErrorType.InvalidScale, current.LineNumber)); } + /// + /// 長さを解析する + /// + /// 解析結果 private ParseResult ParseLength() { LengthType? lengthTypeNullable; @@ -377,6 +449,7 @@ private ParseResult ParseLength() return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken?.LineNumber ?? 0)); } + // 数字かを判定する if (current.Type is not TokenType.Number) { return new(null, new Error(SMSCReadErrorType.InvalidLength, current.LineNumber)); @@ -394,14 +467,15 @@ private ParseResult ParseLength() _ => null, }; + // 次のトークンが'.'だった場合は付点音符とする。 if (_tokens.TryPeek(out current) && current.Type is TokenType.Dot) { - if (_tokens.TryDequeue(out current) && current.Type is TokenType.Dot) - { - isDotted = true; - } + // '.'を破棄 + _ = _tokens.Dequeue(); + isDotted = true; } + // 不正な長さの場合 if (lengthTypeNullable is null) { return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken?.LineNumber ?? 0)); From 2e16c995e546cae4ec04f222ec0415360751867f Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sun, 10 Mar 2024 10:39:02 +0900 Subject: [PATCH 09/23] :recycle: refactoring --- .gitignore | 3 ++ .../Sounds/Score/BasicSoundComponentBase.cs | 2 +- src/SoundMaker/Sounds/Score/Note.cs | 6 ++-- ...uclator.cs => SoundWaveLengthCalclator.cs} | 4 +-- src/SoundMaker/Sounds/Score/Tie.cs | 4 +-- src/SoundMaker/Sounds/Score/Tuplet.cs | 4 +-- test/SoundMakerTests.csproj | 10 +++--- .../SMSC/{TestLexer.cs => LexerTest.cs} | 2 +- .../SMSC/{TestParser.cs => ParserTest.cs} | 35 ++++++++++++++----- 9 files changed, 46 insertions(+), 24 deletions(-) rename src/SoundMaker/Sounds/Score/{SoundWaveLengthCaluclator.cs => SoundWaveLengthCalclator.cs} (92%) rename test/UnitTests/ScoreData/SMSC/{TestLexer.cs => LexerTest.cs} (99%) rename test/UnitTests/ScoreData/SMSC/{TestParser.cs => ParserTest.cs} (87%) diff --git a/.gitignore b/.gitignore index d440c1d..b111e13 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,6 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd /Shinko.cs + +# VS Code workspace +SoundMaker.code-workspace \ No newline at end of file diff --git a/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs b/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs index a9686dc..9fbbb32 100644 --- a/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs +++ b/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs @@ -33,6 +33,6 @@ public BasicSoundComponentBase(LengthType length, bool isDotted) public int GetWaveArrayLength(SoundFormat format, int tempo) { - return SoundWaveLengthCaluclator.Caluclate(format, tempo, Length, IsDotted); + return SoundWaveLengthCalclator.Calclate(format, tempo, Length, IsDotted); } } diff --git a/src/SoundMaker/Sounds/Score/Note.cs b/src/SoundMaker/Sounds/Score/Note.cs index c8082c5..403f4ca 100644 --- a/src/SoundMaker/Sounds/Score/Note.cs +++ b/src/SoundMaker/Sounds/Score/Note.cs @@ -14,7 +14,7 @@ public class Note : BasicSoundComponentBase /// length (ex. "quarter" note) 長さ(音楽的な、「四分」音符、「全」休符のような長さを表す。) /// is note/rest dotted. 付点かを表す論理型 /// Scale and scale number must be only the range of sound that the piano can produce. - public Note(Scale scale, int scaleNumber, LengthType length, bool isDotted = false) + public Note(Scale scale, int scaleNumber, LengthType length, bool isDotted = false) : base(length, isDotted) { CheckArgument(scale, scaleNumber); @@ -50,8 +50,8 @@ public Note(LengthType length, bool isDotted = false) : base(length, isDotted) /// private double[] AHertz { get; } = new double[] { - // A0からA - 27.5d, + // A0からA + 27.5d, 55.0d, 110.0d, 220.0d, diff --git a/src/SoundMaker/Sounds/Score/SoundWaveLengthCaluclator.cs b/src/SoundMaker/Sounds/Score/SoundWaveLengthCalclator.cs similarity index 92% rename from src/SoundMaker/Sounds/Score/SoundWaveLengthCaluclator.cs rename to src/SoundMaker/Sounds/Score/SoundWaveLengthCalclator.cs index 647619c..b07c16c 100644 --- a/src/SoundMaker/Sounds/Score/SoundWaveLengthCaluclator.cs +++ b/src/SoundMaker/Sounds/Score/SoundWaveLengthCalclator.cs @@ -2,7 +2,7 @@ /// /// 音の配列の長さを計算するクラス /// -internal static class SoundWaveLengthCaluclator +internal static class SoundWaveLengthCalclator { /// /// メソッド。 @@ -13,7 +13,7 @@ internal static class SoundWaveLengthCaluclator /// 付点の場合はTrueに設定する /// 音の配列の長さ : int /// Tempo must be non-negative and greater than 0. - public static int Caluclate(SoundFormat format, int tempo, LengthType length, bool isDotted) + public static int Calclate(SoundFormat format, int tempo, LengthType length, bool isDotted) { if (tempo <= 0) { diff --git a/src/SoundMaker/Sounds/Score/Tie.cs b/src/SoundMaker/Sounds/Score/Tie.cs index d583b64..467629e 100644 --- a/src/SoundMaker/Sounds/Score/Tie.cs +++ b/src/SoundMaker/Sounds/Score/Tie.cs @@ -17,7 +17,7 @@ public Tie(Note baseNote, LengthType additionalLength, bool additionalIsDotted = BaseNote = baseNote; AdditionalNotes = new List() { - new Note(baseNote.Scale, baseNote.ScaleNumber, additionalLength, additionalIsDotted) + new(baseNote.Scale, baseNote.ScaleNumber, additionalLength, additionalIsDotted) }; } @@ -63,7 +63,7 @@ public int GetWaveArrayLength(SoundFormat format, int tempo) var length = BaseNote.GetWaveArrayLength(format, tempo); foreach (var note in AdditionalNotes) { - length += SoundWaveLengthCaluclator.Caluclate(format, tempo, note.Length, note.IsDotted); + length += SoundWaveLengthCalclator.Calclate(format, tempo, note.Length, note.IsDotted); } return length; } diff --git a/src/SoundMaker/Sounds/Score/Tuplet.cs b/src/SoundMaker/Sounds/Score/Tuplet.cs index 0d59540..7dd1b18 100644 --- a/src/SoundMaker/Sounds/Score/Tuplet.cs +++ b/src/SoundMaker/Sounds/Score/Tuplet.cs @@ -22,7 +22,7 @@ public Tuplet(IReadOnlyList tupletComponents, LengthType length /// /// components to be tuplet. 連符にする基本の音のリスト /// - private IReadOnlyList TupletComponents { get; } + internal IReadOnlyList TupletComponents { get; } /// /// get the component at index. index番目の連符の音を取得する。 @@ -51,7 +51,7 @@ public Tuplet(IReadOnlyList tupletComponents, LengthType length public int GetWaveArrayLength(SoundFormat format, int tempo) { - return SoundWaveLengthCaluclator.Caluclate(format, tempo, Length, IsDotted); + return SoundWaveLengthCalclator.Calclate(format, tempo, Length, IsDotted); } public ushort[] GenerateWave(SoundFormat format, int tempo, int length, WaveTypeBase waveType) diff --git a/test/SoundMakerTests.csproj b/test/SoundMakerTests.csproj index fb3be2a..0f17d1a 100644 --- a/test/SoundMakerTests.csproj +++ b/test/SoundMakerTests.csproj @@ -8,6 +8,12 @@ false + + + + + + @@ -21,10 +27,6 @@ - - - - diff --git a/test/UnitTests/ScoreData/SMSC/TestLexer.cs b/test/UnitTests/ScoreData/SMSC/LexerTest.cs similarity index 99% rename from test/UnitTests/ScoreData/SMSC/TestLexer.cs rename to test/UnitTests/ScoreData/SMSC/LexerTest.cs index 0b10191..ec88de0 100644 --- a/test/UnitTests/ScoreData/SMSC/TestLexer.cs +++ b/test/UnitTests/ScoreData/SMSC/LexerTest.cs @@ -2,7 +2,7 @@ namespace SoundMakerTests.UnitTests.ScoreData.SMSC; -public class TestLexer +public class LexerTest { [Fact(DisplayName = "コメントは解析しないか")] public void TestReadAll_Comment() diff --git a/test/UnitTests/ScoreData/SMSC/TestParser.cs b/test/UnitTests/ScoreData/SMSC/ParserTest.cs similarity index 87% rename from test/UnitTests/ScoreData/SMSC/TestParser.cs rename to test/UnitTests/ScoreData/SMSC/ParserTest.cs index 9188514..a8d2230 100644 --- a/test/UnitTests/ScoreData/SMSC/TestParser.cs +++ b/test/UnitTests/ScoreData/SMSC/ParserTest.cs @@ -3,7 +3,7 @@ namespace SoundMakerTests.UnitTests.ScoreData.SMSC; -public class TestParser +public class ParserTest { [Fact(DisplayName = "普通の音符を解析できるか")] public void TestParse_Note() @@ -17,7 +17,10 @@ public void TestParse_Note() { new Note(Scale.C, 4, LengthType.Sixteenth), }; - var actualCollection = parser.Parse(); + + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Note)expectedCollection[0]; @@ -40,7 +43,9 @@ public void TestParse_DottedNote() { new Note(Scale.C, 4, LengthType.Sixteenth, true), }; - var actualCollection = parser.Parse(); + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Note)expectedCollection[0]; @@ -63,7 +68,9 @@ public void TestParse_SharpNote() { new Note(Scale.CSharp, 4, LengthType.Sixteenth), }; - var actualCollection = parser.Parse(); + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Note)expectedCollection[0]; @@ -86,7 +93,9 @@ public void TestParse_Rest() { new Rest(LengthType.Sixteenth), }; - var actualCollection = parser.Parse(); + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Rest)expectedCollection[0]; @@ -107,7 +116,9 @@ public void TestParse_DottedRest() { new Rest(LengthType.Sixteenth, true), }; - var actualCollection = parser.Parse(); + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Rest)expectedCollection[0]; @@ -133,7 +144,9 @@ public void TestParse_Tie() new(LengthType.Eighth, true), }), }; - var actualCollection = parser.Parse(); + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Tie)expectedCollection[0]; @@ -158,7 +171,9 @@ public void TestParse_Tuplet() new Note(Scale.E, 4,LengthType.Whole, true), }, LengthType.Sixteenth), }; - var actualCollection = parser.Parse(); + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Tuplet)expectedCollection[0]; @@ -191,7 +206,9 @@ public void TestParse_TupletInTupletAndTie() }), }, LengthType.Sixteenth), }; - var actualCollection = parser.Parse(); + var result = parser.Parse(); + Assert.True(result.IsSuccess); + var actualCollection = result.Unwrap(); Assert.Equal(expectedCollection.Count, actualCollection.Count); var expected = (Tuplet)expectedCollection[0]; From f9bf42e42a863a3889dfcb5999ff60237d39c4dd Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sun, 10 Mar 2024 10:45:09 +0900 Subject: [PATCH 10/23] :+1: Fixed to return result object --- src/SoundMaker/ScoreData/SMSC/Parser.cs | 9 +++-- .../ScoreData/SMSC/SMSCReadResult.cs | 37 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs index 6d7447e..8f7ad68 100644 --- a/src/SoundMaker/ScoreData/SMSC/Parser.cs +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -69,10 +69,10 @@ public Parser(IEnumerable tokens) /// 解析する /// /// 解析結果 - public List Parse() + public SMSCReadResult Parse() { var statements = new List(); - _ = new List(); + var errors = new List(); while (_tokens.Count > 0) { var statementResult = ParseStatement(); @@ -81,7 +81,10 @@ public List Parse() statements.Add(statement); } } - return statements; + var result = errors.Any() ? + SMSCReadResult.Failure(errors) : + SMSCReadResult.Success(statements); + return result; } /// diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs b/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs index 3cb54b5..461234d 100644 --- a/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs +++ b/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs @@ -1,12 +1,11 @@ using SoundMaker.Sounds.Score; -using System.Diagnostics.CodeAnalysis; namespace SoundMaker.ScoreData.SMSC; public class SMSCReadResult { - private readonly IReadOnlyList? _value; + private readonly IReadOnlyList _value; - private SMSCReadResult(IReadOnlyList? value, IReadOnlyList errors) + private SMSCReadResult(IReadOnlyList value, IReadOnlyList errors) { _value = value; Errors = errors; @@ -19,32 +18,52 @@ internal static SMSCReadResult Success(IReadOnlyList value) internal static SMSCReadResult Failure(IReadOnlyList errors) { - return new SMSCReadResult(null, errors); + return new SMSCReadResult(Array.Empty(), errors); } + /// + /// Errors when reading SMSC.
+ /// SMSCを読み込んだ際のエラー + ///
public IReadOnlyList Errors { get; } + /// + /// Whether the reading was successful.
+ /// 読み込みに成功したか + ///
public bool IsSuccess => Errors.Count is 0; - public bool TryGetValue([MaybeNullWhen(false)] out IReadOnlyList value) + /// + /// Returns whether the reading was successful and, if successful, returns the result. If it fails, an empty array will be in value.
+ /// 読み込みに成功したかを返し、成功した場合は結果を返す。失敗時は空の配列がvalueに入る。 + ///
+ /// On success: result, On failure: empty array + /// On success: true, On failure: false + public bool TryGetValue(out IReadOnlyList value) { - if (_value is not null && IsSuccess) + if (IsSuccess) { value = _value; return true; } - value = null; + value = Array.Empty(); return false; } + /// + /// Returns the result assuming the reading was successful. If it fails, an empty array is returned.
+ /// 読み込みに成功した前提で結果を返す。失敗時は空の配列が戻る。 + ///
+ /// On success: result, On failure: empty array public IReadOnlyList Unwrap() { - if (!IsSuccess || _value is null) + if (!IsSuccess) { - throw new NullReferenceException("The result does not contain a valid value."); + return Array.Empty(); } return _value; } + } From e431805fdeddc67e76365d25cae3472e3fa5d117 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sun, 10 Mar 2024 10:58:56 +0900 Subject: [PATCH 11/23] :arrow_double_down: Add the smsc serializer --- src/SoundMaker/ScoreData/SMSC/SMSCFormat.cs | 33 +++++ .../ScoreData/SMSC/SMSCSerializer.cs | 133 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 src/SoundMaker/ScoreData/SMSC/SMSCFormat.cs create mode 100644 src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCFormat.cs b/src/SoundMaker/ScoreData/SMSC/SMSCFormat.cs new file mode 100644 index 0000000..dd2b8e6 --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/SMSCFormat.cs @@ -0,0 +1,33 @@ +using SoundMaker.Sounds.Score; + +namespace SoundMaker.ScoreData.SMSC; +/// +/// The SMSC (SoundMaker SCore) Format +/// +public static class SMSCFormat +{ + /// + /// Reads SMSC data.
+ /// SMSCデータを読み込む。 + ///
+ /// + /// + public static SMSCReadResult Read(string data) + { + var lexer = new Lexer(data); + var result = new Parser(lexer.ReadAll()).Parse(); + return result; + } + + /// + /// Outputs SMSC data.
+ /// SMSCデータを出力する。 + ///
+ /// Sound components to write. 書き込むサウンドコンポーネント + /// SMSC data. SMSCデータ + public static string Serialize(IEnumerable components) + { + return SMSCSerializer.Serialize(components); + } +} + diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs b/src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs new file mode 100644 index 0000000..e5d177d --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs @@ -0,0 +1,133 @@ +using SoundMaker.Sounds.Score; +using System.Text; + +namespace SoundMaker.ScoreData.SMSC; +internal static class SMSCSerializer +{ + public static string Serialize(IEnumerable components) + { + var smscBuilder = new StringBuilder(); + foreach (var component in components) + { + var line = component switch + { + Note note => SerializeNote(note), + Rest rest => SerializeRest(rest), + Tie tie => SerializeTie(tie), + Tuplet tuplet => SerializeTuplet(tuplet), + _ => "", + }; + _ = smscBuilder.AppendLine(line); + } + return smscBuilder.ToString(); + } + + private static string SerializeNote(Note note) + { + var scale = SerializeScale(note.Scale, note.ScaleNumber); + var length = SerializeLength(note.Length, note.IsDotted); + return new StringBuilder(scale).Append(',').Append(length).ToString(); + } + + private static string SerializeRest(Rest rest) + { + var length = SerializeLength(rest.Length, rest.IsDotted); + return new StringBuilder("rest").Append(',').Append(length).ToString(); + } + + private static string SerializeTie(Tie tie) + { + var builder = new StringBuilder("tie("); + var scale = SerializeScale(tie.BaseNote.Scale, tie.BaseNote.ScaleNumber); + var length = SerializeLength(tie.BaseNote.Length, tie.BaseNote.IsDotted); + _ = builder.Append(scale).Append(',').Append(length); + foreach (var note in tie.AdditionalNotes) + { + length = SerializeLength(note.Length, note.IsDotted); + _ = builder.Append(',').Append(length); + } + _ = builder.Append(')'); + return builder.ToString(); + } + + private static string SerializeTuplet(Tuplet tuplet) + { + var builder = new StringBuilder("tup("); + var length = SerializeLength(tuplet.Length, tuplet.IsDotted); + _ = builder.Append(length); + foreach (var component in tuplet.TupletComponents) + { + if (component is Tuplet tup) + { + var tupStr = SerializeTuplet(tup); + _ = builder.Append(',').Append(tupStr); + } + else if (component is Tie tie) + { + var tieStr = SerializeTie(tie); + _ = builder.Append(',').Append(tieStr); + } + else if (component is Rest rest) + { + _ = builder.Append(',').Append("rest"); + if (rest.IsDotted) + { + _ = builder.Append('.'); + } + } + else if (component is Note note) + { + var scale = SerializeScale(note.Scale, note.ScaleNumber); + _ = builder.Append(',').Append(scale); + if (note.IsDotted) + { + _ = builder.Append('.'); + } + } + } + _ = builder.Append(')'); + return builder.ToString(); + } + + private static string SerializeScale(Scale scaleType, int scaleNumber) + { + var scale = scaleType switch + { + Scale.A => "A", + Scale.ASharp => "A#", + Scale.B => "B", + Scale.C => "C", + Scale.CSharp => "C#", + Scale.D => "D", + Scale.DSharp => "D#", + Scale.E => "E", + Scale.F => "F", + Scale.FSharp => "F#", + Scale.G => "G", + Scale.GSharp => "G#", + _ => "A" + }; + return new StringBuilder(scale).Append(scaleNumber.ToString()).ToString(); + } + + private static string SerializeLength(LengthType lengthType, bool isDotted) + { + var length = lengthType switch + { + LengthType.Whole => "1", + LengthType.Half => "2", + LengthType.Quarter => "4", + LengthType.Eighth => "8", + LengthType.Sixteenth => "16", + LengthType.ThirtySecond => "32", + LengthType.SixtyFourth => "64", + _ => "1" + }; + var builder = new StringBuilder(length); + if (isDotted) + { + _ = builder.Append('.'); + } + return builder.ToString(); + } +} From cf34c187911e6daa0509d7cd025b62ca0076e201 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sun, 10 Mar 2024 19:24:05 +0900 Subject: [PATCH 12/23] :+1: Add serializer test --- .../ScoreData/SMSC/SMSCSerializerTest.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs diff --git a/test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs b/test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs new file mode 100644 index 0000000..63b6907 --- /dev/null +++ b/test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs @@ -0,0 +1,33 @@ +using SoundMaker.ScoreData.SMSC; +using SoundMaker.Sounds.Score; + +namespace SoundMakerTests.UnitTests.ScoreData.SMSC; +public class SMSCSerializerTest +{ + [Fact(DisplayName = "SoundComponentが正しくSMSCデータになるか")] + public void TestSerialize() + { + var note = new Note(Scale.C, 4, LengthType.Whole, true); + + var expected = @"C4,1. +rest,4 +tie(C4,1.,1.,1.) +tup(2,C4.,C4.,C4.) +"; + + var components = new List() + { + note, + new Rest(LengthType.Quarter, false), + new Tie(note, new List() + { + note, note + }), + new Tuplet(new List() {note, note, note}, LengthType.Half, false) + }; + + var actual = SMSCSerializer.Serialize(components); + + Assert.Equal(expected, actual); + } +} From a131a158428020af9d13b48f2127dc34b717d586 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Sun, 10 Mar 2024 19:24:19 +0900 Subject: [PATCH 13/23] :recycle: refactoring --- src/SoundMaker/AssemblyInfo.cs | 3 +++ src/SoundMaker/ScoreData/SMSC/Lexer.cs | 4 +-- src/SoundMaker/ScoreData/SMSC/Parser.cs | 6 ++--- src/SoundMaker/Sounds/Score/Note.cs | 34 ++++++------------------- 4 files changed, 14 insertions(+), 33 deletions(-) create mode 100644 src/SoundMaker/AssemblyInfo.cs diff --git a/src/SoundMaker/AssemblyInfo.cs b/src/SoundMaker/AssemblyInfo.cs new file mode 100644 index 0000000..f3430d9 --- /dev/null +++ b/src/SoundMaker/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SoundMakerTests")] diff --git a/src/SoundMaker/ScoreData/SMSC/Lexer.cs b/src/SoundMaker/ScoreData/SMSC/Lexer.cs index 563b15f..7e29102 100644 --- a/src/SoundMaker/ScoreData/SMSC/Lexer.cs +++ b/src/SoundMaker/ScoreData/SMSC/Lexer.cs @@ -1,7 +1,5 @@ -using System.Runtime.CompilerServices; -using System.Text; +using System.Text; using System.Text.RegularExpressions; -[assembly: InternalsVisibleTo("SoundMakerTests")] namespace SoundMaker.ScoreData.SMSC; internal class Lexer diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs index 8f7ad68..7342486 100644 --- a/src/SoundMaker/ScoreData/SMSC/Parser.cs +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -1,7 +1,5 @@ using SoundMaker.Sounds.Score; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("SoundMakerTests")] namespace SoundMaker.ScoreData.SMSC; @@ -178,12 +176,12 @@ private ParseResult ParseTie() // 個数が1個の場合はadditionalNotesに空配列を渡す else if (notes.Count == 1) { - tie = new Tie(notes[0], Array.Empty()); + tie = new(notes[0], Array.Empty()); } // 個数が2個以上の場合は、先頭をbaseとし、残りをadditionalNotesとする。 else { - tie = new Tie(notes[0], notes.GetRange(1, notes.Count - 1)); + tie = new(notes[0], notes.GetRange(1, notes.Count - 1)); } // 解析結果を返す。 return new(tie, null); diff --git a/src/SoundMaker/Sounds/Score/Note.cs b/src/SoundMaker/Sounds/Score/Note.cs index 6483723..ee8697b 100644 --- a/src/SoundMaker/Sounds/Score/Note.cs +++ b/src/SoundMaker/Sounds/Score/Note.cs @@ -69,26 +69,11 @@ public Note(LengthType length, bool isDotted = false) : base(length, isDotted) { var scale = Scale.A; var scaleNumber = 4; - Hertz += AHertz[scaleNumber]; + Hertz += _hertz[(scale, scaleNumber)]; Scale = scale; ScaleNumber = scaleNumber; } - /// - /// 「ラ」の周波数 - /// - private double[] AHertz { get; } = new double[] - { - // A0からA - 27.5d, - 55.0d, - 110.0d, - 220.0d, - 440.0d, - 880.0d, - 1760.0d, - 3520.0d - }; /// /// scale of the note. 音の高さ /// @@ -125,23 +110,20 @@ public int Volume } } - private void CheckArgument(Scale scale, int scaleNumber) + private static void CheckArgument(Scale scale, int scaleNumber) { var message = "'scale' and 'scaleNumber' must be only the range of sound that the piano can produce."; - if (scaleNumber is >= 9 or <= (-1)) - { - throw new ArgumentException(message); - } - if (scale != Scale.A && scale != Scale.B && scale != Scale.ASharp && scaleNumber == 0) - { - throw new ArgumentException(message); - } - if (scale != Scale.C && scaleNumber == 8) + if (!IsValidScale(scale, scaleNumber)) { throw new ArgumentException(message); } } + internal static bool IsValidScale(Scale scale, int scaleNumber) + { + return _hertz.ContainsKey((scale, scaleNumber)); + } + public override ushort[] GenerateWave(SoundFormat format, int tempo, int length, WaveTypeBase waveType) { return waveType.GenerateWave(format, length, Volume, Hertz); From 7a7a824d4b6062542f0e38448e13898b3c965acb Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:18:16 +0900 Subject: [PATCH 14/23] :recycle: Refactoring the Note class --- src/SoundMaker/Sounds/Score/Note.cs | 48 ++----------------- .../Sounds/Score/ScaleHertzDictionary.cs | 1 + 2 files changed, 4 insertions(+), 45 deletions(-) create mode 100644 src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs diff --git a/src/SoundMaker/Sounds/Score/Note.cs b/src/SoundMaker/Sounds/Score/Note.cs index ee8697b..4313a22 100644 --- a/src/SoundMaker/Sounds/Score/Note.cs +++ b/src/SoundMaker/Sounds/Score/Note.cs @@ -6,43 +6,6 @@ namespace SoundMaker.Sounds.Score; ///
public class Note : BasicSoundComponentBase { - private static readonly IReadOnlyDictionary<(Scale, int), double> _hertz = new Dictionary<(Scale, int), double>() - { - // A0 ~ - {(Scale.A, 0), 27.500 }, - {(Scale.ASharp, 0), 29.135 }, - {(Scale.B, 0), 30.868 }, - // C1 ~ C2 ~ C3 ~ C4 ~ - {(Scale.C, 1), 32.703 }, {(Scale.C, 2), 65.406 }, {(Scale.C, 3), 130.813 }, {(Scale.C, 4), 261.626 }, - {(Scale.CSharp, 1), 34.648 }, {(Scale.CSharp, 2), 69.296 }, {(Scale.CSharp, 3), 138.591 }, {(Scale.CSharp, 4), 277.183 }, - {(Scale.D, 1), 36.708 }, {(Scale.D, 2), 73.416 }, {(Scale.D, 3), 146.832 }, {(Scale.D, 4), 293.665 }, - {(Scale.DSharp, 1), 38.891 }, {(Scale.DSharp, 2), 77.782 }, {(Scale.DSharp, 3), 155.563 }, {(Scale.DSharp, 4), 311.127 }, - {(Scale.E, 1), 41.203 }, {(Scale.E, 2), 82.407 }, {(Scale.E, 3), 164.814 }, {(Scale.E, 4), 329.628 }, - {(Scale.F, 1), 43.654 }, {(Scale.F, 2), 87.307 }, {(Scale.F, 3), 174.614 }, {(Scale.F, 4), 349.228 }, - {(Scale.FSharp, 1), 46.249 }, {(Scale.FSharp, 2), 92.499 }, {(Scale.FSharp, 3), 184.997 }, {(Scale.FSharp, 4), 369.994 }, - {(Scale.G, 1), 48.999 }, {(Scale.G, 2), 97.999 }, {(Scale.G, 3), 195.998 }, {(Scale.G, 4), 391.995 }, - {(Scale.GSharp, 1), 51.913 }, {(Scale.GSharp, 2), 103.826 }, {(Scale.GSharp, 3), 207.652 }, {(Scale.GSharp, 4), 415.305 }, - {(Scale.A, 1), 55.000 }, {(Scale.A, 2), 110.000 }, {(Scale.A, 3), 220.000 }, {(Scale.A, 4), 440.000 }, - {(Scale.ASharp, 1), 58.270 }, {(Scale.ASharp, 2), 116.541 }, {(Scale.ASharp, 3), 233.082 }, {(Scale.ASharp, 4), 466.164 }, - {(Scale.B, 1), 61.735 }, {(Scale.B, 2), 123.471 }, {(Scale.B, 3), 246.942 }, {(Scale.B, 4), 493.883 }, - // C5 ~ C6 ~ C7 ~ - {(Scale.C, 5), 523.251 }, {(Scale.C, 6), 1046.502 }, {(Scale.C, 7), 2093.005 }, - {(Scale.CSharp, 5), 554.365 }, {(Scale.CSharp, 6), 1108.731 }, {(Scale.CSharp, 7), 2217.461 }, - {(Scale.D, 5), 587.330 }, {(Scale.D, 6), 1174.659 }, {(Scale.D, 7), 2349.318 }, - {(Scale.DSharp, 5), 622.254 }, {(Scale.DSharp, 6), 1244.508 }, {(Scale.DSharp, 7), 2489.016 }, - {(Scale.E, 5), 659.255 }, {(Scale.E, 6), 1318.510 }, {(Scale.E, 7), 2637.020 }, - {(Scale.F, 5), 698.456 }, {(Scale.F, 6), 1396.913 }, {(Scale.F, 7), 2793.826 }, - {(Scale.FSharp, 5), 739.989 }, {(Scale.FSharp, 6), 1479.978 }, {(Scale.FSharp, 7), 2959.955 }, - {(Scale.G, 5), 783.991 }, {(Scale.G, 6), 1567.982 }, {(Scale.G, 7), 3135.963 }, - {(Scale.GSharp, 5), 830.609 }, {(Scale.GSharp, 6), 1661.219 }, {(Scale.GSharp, 7), 3322.438 }, - {(Scale.A, 5), 880.000 }, {(Scale.A, 6), 1760.000 }, {(Scale.A, 7), 3520.000 }, - {(Scale.ASharp, 5), 932.328 }, {(Scale.ASharp, 6), 1864.655 }, {(Scale.ASharp, 7), 3729.310 }, - {(Scale.B, 5), 987.767 }, {(Scale.B, 6), 1975.533 }, {(Scale.B, 7), 3951.066 }, - // C8 - {(Scale.C, 8), 4186.009 }, - }; - - /// /// constructor コンストラクタ /// @@ -55,7 +18,7 @@ public Note(Scale scale, int scaleNumber, LengthType length, bool isDotted = fal : base(length, isDotted) { CheckArgument(scale, scaleNumber); - Hertz += _hertz[(scale, scaleNumber)]; + Hertz += ScaleHertzDictionary.GetHertz(scale, scaleNumber); Scale = scale; ScaleNumber = scaleNumber; } @@ -69,7 +32,7 @@ public Note(LengthType length, bool isDotted = false) : base(length, isDotted) { var scale = Scale.A; var scaleNumber = 4; - Hertz += _hertz[(scale, scaleNumber)]; + Hertz += ScaleHertzDictionary.GetHertz(scale, scaleNumber); Scale = scale; ScaleNumber = scaleNumber; } @@ -113,17 +76,12 @@ public int Volume private static void CheckArgument(Scale scale, int scaleNumber) { var message = "'scale' and 'scaleNumber' must be only the range of sound that the piano can produce."; - if (!IsValidScale(scale, scaleNumber)) + if (!ScaleHertzDictionary.IsValidScale(scale, scaleNumber)) { throw new ArgumentException(message); } } - internal static bool IsValidScale(Scale scale, int scaleNumber) - { - return _hertz.ContainsKey((scale, scaleNumber)); - } - public override ushort[] GenerateWave(SoundFormat format, int tempo, int length, WaveTypeBase waveType) { return waveType.GenerateWave(format, length, Volume, Hertz); diff --git a/src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs b/src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs new file mode 100644 index 0000000..e6f12f0 --- /dev/null +++ b/src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs @@ -0,0 +1 @@ +namespace SoundMaker.Sounds.Score; internal static class ScaleHertzDictionary { private static readonly IReadOnlyDictionary<(Scale, int), double> _hertz = new Dictionary<(Scale, int), double>() { // A0 ~ {(Scale.A, 0), 27.500 }, {(Scale.ASharp, 0), 29.135 }, {(Scale.B, 0), 30.868 }, // C1 ~ C2 ~ C3 ~ C4 ~ {(Scale.C, 1), 32.703 }, {(Scale.C, 2), 65.406 }, {(Scale.C, 3), 130.813 }, {(Scale.C, 4), 261.626 }, {(Scale.CSharp, 1), 34.648 }, {(Scale.CSharp, 2), 69.296 }, {(Scale.CSharp, 3), 138.591 }, {(Scale.CSharp, 4), 277.183 }, {(Scale.D, 1), 36.708 }, {(Scale.D, 2), 73.416 }, {(Scale.D, 3), 146.832 }, {(Scale.D, 4), 293.665 }, {(Scale.DSharp, 1), 38.891 }, {(Scale.DSharp, 2), 77.782 }, {(Scale.DSharp, 3), 155.563 }, {(Scale.DSharp, 4), 311.127 }, {(Scale.E, 1), 41.203 }, {(Scale.E, 2), 82.407 }, {(Scale.E, 3), 164.814 }, {(Scale.E, 4), 329.628 }, {(Scale.F, 1), 43.654 }, {(Scale.F, 2), 87.307 }, {(Scale.F, 3), 174.614 }, {(Scale.F, 4), 349.228 }, {(Scale.FSharp, 1), 46.249 }, {(Scale.FSharp, 2), 92.499 }, {(Scale.FSharp, 3), 184.997 }, {(Scale.FSharp, 4), 369.994 }, {(Scale.G, 1), 48.999 }, {(Scale.G, 2), 97.999 }, {(Scale.G, 3), 195.998 }, {(Scale.G, 4), 391.995 }, {(Scale.GSharp, 1), 51.913 }, {(Scale.GSharp, 2), 103.826 }, {(Scale.GSharp, 3), 207.652 }, {(Scale.GSharp, 4), 415.305 }, {(Scale.A, 1), 55.000 }, {(Scale.A, 2), 110.000 }, {(Scale.A, 3), 220.000 }, {(Scale.A, 4), 440.000 }, {(Scale.ASharp, 1), 58.270 }, {(Scale.ASharp, 2), 116.541 }, {(Scale.ASharp, 3), 233.082 }, {(Scale.ASharp, 4), 466.164 }, {(Scale.B, 1), 61.735 }, {(Scale.B, 2), 123.471 }, {(Scale.B, 3), 246.942 }, {(Scale.B, 4), 493.883 }, // C5 ~ C6 ~ C7 ~ {(Scale.C, 5), 523.251 }, {(Scale.C, 6), 1046.502 }, {(Scale.C, 7), 2093.005 }, {(Scale.CSharp, 5), 554.365 }, {(Scale.CSharp, 6), 1108.731 }, {(Scale.CSharp, 7), 2217.461 }, {(Scale.D, 5), 587.330 }, {(Scale.D, 6), 1174.659 }, {(Scale.D, 7), 2349.318 }, {(Scale.DSharp, 5), 622.254 }, {(Scale.DSharp, 6), 1244.508 }, {(Scale.DSharp, 7), 2489.016 }, {(Scale.E, 5), 659.255 }, {(Scale.E, 6), 1318.510 }, {(Scale.E, 7), 2637.020 }, {(Scale.F, 5), 698.456 }, {(Scale.F, 6), 1396.913 }, {(Scale.F, 7), 2793.826 }, {(Scale.FSharp, 5), 739.989 }, {(Scale.FSharp, 6), 1479.978 }, {(Scale.FSharp, 7), 2959.955 }, {(Scale.G, 5), 783.991 }, {(Scale.G, 6), 1567.982 }, {(Scale.G, 7), 3135.963 }, {(Scale.GSharp, 5), 830.609 }, {(Scale.GSharp, 6), 1661.219 }, {(Scale.GSharp, 7), 3322.438 }, {(Scale.A, 5), 880.000 }, {(Scale.A, 6), 1760.000 }, {(Scale.A, 7), 3520.000 }, {(Scale.ASharp, 5), 932.328 }, {(Scale.ASharp, 6), 1864.655 }, {(Scale.ASharp, 7), 3729.310 }, {(Scale.B, 5), 987.767 }, {(Scale.B, 6), 1975.533 }, {(Scale.B, 7), 3951.066 }, // C8 {(Scale.C, 8), 4186.009 }, }; public static double GetHertz(Scale scale, int scaleNumber) { return _hertz[(scale, scaleNumber)]; } public static bool IsValidScale(Scale scale, int scaleNumber) { return _hertz.ContainsKey((scale, scaleNumber)); } } \ No newline at end of file From 3dadc113d88ca836b9df9aceff8e20f9cd5d8452 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Wed, 13 Mar 2024 23:46:00 +0900 Subject: [PATCH 15/23] :recycle: Change from CRLF to LF --- .editorconfig | 2 +- .../{AssemblyInfo.cs => ForTests.cs} | 0 .../Sounds/Score/ScaleHertzDictionary.cs | 50 ++++++++++- test/Experiment.cs | 84 ------------------- 4 files changed, 50 insertions(+), 86 deletions(-) rename src/SoundMaker/{AssemblyInfo.cs => ForTests.cs} (100%) delete mode 100644 test/Experiment.cs diff --git a/.editorconfig b/.editorconfig index 8419f59..8e233c6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -241,7 +241,7 @@ dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent dotnet_style_operator_placement_when_wrapping = beginning_of_line -end_of_line = crlf +end_of_line = lf dotnet_code_quality_unused_parameters = all:suggestion dotnet_style_readonly_field = true:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent diff --git a/src/SoundMaker/AssemblyInfo.cs b/src/SoundMaker/ForTests.cs similarity index 100% rename from src/SoundMaker/AssemblyInfo.cs rename to src/SoundMaker/ForTests.cs diff --git a/src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs b/src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs index e6f12f0..7e9b072 100644 --- a/src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs +++ b/src/SoundMaker/Sounds/Score/ScaleHertzDictionary.cs @@ -1 +1,49 @@ -namespace SoundMaker.Sounds.Score; internal static class ScaleHertzDictionary { private static readonly IReadOnlyDictionary<(Scale, int), double> _hertz = new Dictionary<(Scale, int), double>() { // A0 ~ {(Scale.A, 0), 27.500 }, {(Scale.ASharp, 0), 29.135 }, {(Scale.B, 0), 30.868 }, // C1 ~ C2 ~ C3 ~ C4 ~ {(Scale.C, 1), 32.703 }, {(Scale.C, 2), 65.406 }, {(Scale.C, 3), 130.813 }, {(Scale.C, 4), 261.626 }, {(Scale.CSharp, 1), 34.648 }, {(Scale.CSharp, 2), 69.296 }, {(Scale.CSharp, 3), 138.591 }, {(Scale.CSharp, 4), 277.183 }, {(Scale.D, 1), 36.708 }, {(Scale.D, 2), 73.416 }, {(Scale.D, 3), 146.832 }, {(Scale.D, 4), 293.665 }, {(Scale.DSharp, 1), 38.891 }, {(Scale.DSharp, 2), 77.782 }, {(Scale.DSharp, 3), 155.563 }, {(Scale.DSharp, 4), 311.127 }, {(Scale.E, 1), 41.203 }, {(Scale.E, 2), 82.407 }, {(Scale.E, 3), 164.814 }, {(Scale.E, 4), 329.628 }, {(Scale.F, 1), 43.654 }, {(Scale.F, 2), 87.307 }, {(Scale.F, 3), 174.614 }, {(Scale.F, 4), 349.228 }, {(Scale.FSharp, 1), 46.249 }, {(Scale.FSharp, 2), 92.499 }, {(Scale.FSharp, 3), 184.997 }, {(Scale.FSharp, 4), 369.994 }, {(Scale.G, 1), 48.999 }, {(Scale.G, 2), 97.999 }, {(Scale.G, 3), 195.998 }, {(Scale.G, 4), 391.995 }, {(Scale.GSharp, 1), 51.913 }, {(Scale.GSharp, 2), 103.826 }, {(Scale.GSharp, 3), 207.652 }, {(Scale.GSharp, 4), 415.305 }, {(Scale.A, 1), 55.000 }, {(Scale.A, 2), 110.000 }, {(Scale.A, 3), 220.000 }, {(Scale.A, 4), 440.000 }, {(Scale.ASharp, 1), 58.270 }, {(Scale.ASharp, 2), 116.541 }, {(Scale.ASharp, 3), 233.082 }, {(Scale.ASharp, 4), 466.164 }, {(Scale.B, 1), 61.735 }, {(Scale.B, 2), 123.471 }, {(Scale.B, 3), 246.942 }, {(Scale.B, 4), 493.883 }, // C5 ~ C6 ~ C7 ~ {(Scale.C, 5), 523.251 }, {(Scale.C, 6), 1046.502 }, {(Scale.C, 7), 2093.005 }, {(Scale.CSharp, 5), 554.365 }, {(Scale.CSharp, 6), 1108.731 }, {(Scale.CSharp, 7), 2217.461 }, {(Scale.D, 5), 587.330 }, {(Scale.D, 6), 1174.659 }, {(Scale.D, 7), 2349.318 }, {(Scale.DSharp, 5), 622.254 }, {(Scale.DSharp, 6), 1244.508 }, {(Scale.DSharp, 7), 2489.016 }, {(Scale.E, 5), 659.255 }, {(Scale.E, 6), 1318.510 }, {(Scale.E, 7), 2637.020 }, {(Scale.F, 5), 698.456 }, {(Scale.F, 6), 1396.913 }, {(Scale.F, 7), 2793.826 }, {(Scale.FSharp, 5), 739.989 }, {(Scale.FSharp, 6), 1479.978 }, {(Scale.FSharp, 7), 2959.955 }, {(Scale.G, 5), 783.991 }, {(Scale.G, 6), 1567.982 }, {(Scale.G, 7), 3135.963 }, {(Scale.GSharp, 5), 830.609 }, {(Scale.GSharp, 6), 1661.219 }, {(Scale.GSharp, 7), 3322.438 }, {(Scale.A, 5), 880.000 }, {(Scale.A, 6), 1760.000 }, {(Scale.A, 7), 3520.000 }, {(Scale.ASharp, 5), 932.328 }, {(Scale.ASharp, 6), 1864.655 }, {(Scale.ASharp, 7), 3729.310 }, {(Scale.B, 5), 987.767 }, {(Scale.B, 6), 1975.533 }, {(Scale.B, 7), 3951.066 }, // C8 {(Scale.C, 8), 4186.009 }, }; public static double GetHertz(Scale scale, int scaleNumber) { return _hertz[(scale, scaleNumber)]; } public static bool IsValidScale(Scale scale, int scaleNumber) { return _hertz.ContainsKey((scale, scaleNumber)); } } \ No newline at end of file +namespace SoundMaker.Sounds.Score; +internal static class ScaleHertzDictionary +{ + private static readonly IReadOnlyDictionary<(Scale, int), double> _hertz = new Dictionary<(Scale, int), double>() + { + // A0 ~ + {(Scale.A, 0), 27.500 }, + {(Scale.ASharp, 0), 29.135 }, + {(Scale.B, 0), 30.868 }, + // C1 ~ C2 ~ C3 ~ C4 ~ + {(Scale.C, 1), 32.703 }, {(Scale.C, 2), 65.406 }, {(Scale.C, 3), 130.813 }, {(Scale.C, 4), 261.626 }, + {(Scale.CSharp, 1), 34.648 }, {(Scale.CSharp, 2), 69.296 }, {(Scale.CSharp, 3), 138.591 }, {(Scale.CSharp, 4), 277.183 }, + {(Scale.D, 1), 36.708 }, {(Scale.D, 2), 73.416 }, {(Scale.D, 3), 146.832 }, {(Scale.D, 4), 293.665 }, + {(Scale.DSharp, 1), 38.891 }, {(Scale.DSharp, 2), 77.782 }, {(Scale.DSharp, 3), 155.563 }, {(Scale.DSharp, 4), 311.127 }, + {(Scale.E, 1), 41.203 }, {(Scale.E, 2), 82.407 }, {(Scale.E, 3), 164.814 }, {(Scale.E, 4), 329.628 }, + {(Scale.F, 1), 43.654 }, {(Scale.F, 2), 87.307 }, {(Scale.F, 3), 174.614 }, {(Scale.F, 4), 349.228 }, + {(Scale.FSharp, 1), 46.249 }, {(Scale.FSharp, 2), 92.499 }, {(Scale.FSharp, 3), 184.997 }, {(Scale.FSharp, 4), 369.994 }, + {(Scale.G, 1), 48.999 }, {(Scale.G, 2), 97.999 }, {(Scale.G, 3), 195.998 }, {(Scale.G, 4), 391.995 }, + {(Scale.GSharp, 1), 51.913 }, {(Scale.GSharp, 2), 103.826 }, {(Scale.GSharp, 3), 207.652 }, {(Scale.GSharp, 4), 415.305 }, + {(Scale.A, 1), 55.000 }, {(Scale.A, 2), 110.000 }, {(Scale.A, 3), 220.000 }, {(Scale.A, 4), 440.000 }, + {(Scale.ASharp, 1), 58.270 }, {(Scale.ASharp, 2), 116.541 }, {(Scale.ASharp, 3), 233.082 }, {(Scale.ASharp, 4), 466.164 }, + {(Scale.B, 1), 61.735 }, {(Scale.B, 2), 123.471 }, {(Scale.B, 3), 246.942 }, {(Scale.B, 4), 493.883 }, + // C5 ~ C6 ~ C7 ~ + {(Scale.C, 5), 523.251 }, {(Scale.C, 6), 1046.502 }, {(Scale.C, 7), 2093.005 }, + {(Scale.CSharp, 5), 554.365 }, {(Scale.CSharp, 6), 1108.731 }, {(Scale.CSharp, 7), 2217.461 }, + {(Scale.D, 5), 587.330 }, {(Scale.D, 6), 1174.659 }, {(Scale.D, 7), 2349.318 }, + {(Scale.DSharp, 5), 622.254 }, {(Scale.DSharp, 6), 1244.508 }, {(Scale.DSharp, 7), 2489.016 }, + {(Scale.E, 5), 659.255 }, {(Scale.E, 6), 1318.510 }, {(Scale.E, 7), 2637.020 }, + {(Scale.F, 5), 698.456 }, {(Scale.F, 6), 1396.913 }, {(Scale.F, 7), 2793.826 }, + {(Scale.FSharp, 5), 739.989 }, {(Scale.FSharp, 6), 1479.978 }, {(Scale.FSharp, 7), 2959.955 }, + {(Scale.G, 5), 783.991 }, {(Scale.G, 6), 1567.982 }, {(Scale.G, 7), 3135.963 }, + {(Scale.GSharp, 5), 830.609 }, {(Scale.GSharp, 6), 1661.219 }, {(Scale.GSharp, 7), 3322.438 }, + {(Scale.A, 5), 880.000 }, {(Scale.A, 6), 1760.000 }, {(Scale.A, 7), 3520.000 }, + {(Scale.ASharp, 5), 932.328 }, {(Scale.ASharp, 6), 1864.655 }, {(Scale.ASharp, 7), 3729.310 }, + {(Scale.B, 5), 987.767 }, {(Scale.B, 6), 1975.533 }, {(Scale.B, 7), 3951.066 }, + // C8 + {(Scale.C, 8), 4186.009 }, + }; + + public static double GetHertz(Scale scale, int scaleNumber) + { + return _hertz[(scale, scaleNumber)]; + } + + public static bool IsValidScale(Scale scale, int scaleNumber) + { + return _hertz.ContainsKey((scale, scaleNumber)); + } +} diff --git a/test/Experiment.cs b/test/Experiment.cs deleted file mode 100644 index b814fa2..0000000 --- a/test/Experiment.cs +++ /dev/null @@ -1,84 +0,0 @@ -using SoundMaker; -using SoundMaker.Sounds; -using SoundMaker.Sounds.Score; -using SoundMaker.Sounds.SoundChannels; -using SoundMaker.WaveFile; -using Xunit.Abstractions; - -namespace SoundMakerTests; -public class Experiment -{ - public Experiment(ITestOutputHelper output) - { - Output = output; - } - - private ITestOutputHelper Output { get; } - - [Fact(DisplayName = "README.md̃Tv")] - public void TestReflectedVolume() - { - Main(); - } - - private void Main() - { - // Create a sound format. - var builder = FormatBuilder.Create() - .WithFrequency(48000) - .WithBitDepth(16) - .WithChannelCount(2); - - var soundFormat = builder.ToSoundFormat(); - var wave = MakeStereoWave(soundFormat); - - // Write to a file. - var sound = new SoundWaveChunk(wave.GetBytes(soundFormat.BitRate)); - var waveFileFormat = builder.ToFormatChunk(); - var writer = new WaveWriter(waveFileFormat, sound); - var filePath = "sample.wav"; - writer.Write(filePath); - } - - private StereoWave MakeStereoWave(SoundFormat format) - { - // The number of quarter notes per minute - var tempo = 100; - // First, you need to create sound channels. - // Currently, it supports square wave, triangle wave, pseudo-triangle wave, and low-bit noise. - var rightChannel = new SquareSoundChannel(tempo, format, SquareWaveRatio.Point25, PanType.Right) - { - // Add objects of classes that implement ISoundComponent to the channel. - // Currently, you can use normal notes, rests, ties, and tuplets. - new Note(Scale.C, 5, LengthType.Eighth, isDotted: true), - new Tie(new Note(Scale.D, 5, LengthType.Eighth), LengthType.Eighth), - new Tuplet(GetComponents(), LengthType.Quarter) - }; - var rightChannel2 = new SquareSoundChannel(tempo, format, SquareWaveRatio.Point125, PanType.Right) - { - new Note(Scale.C, 4, LengthType.Eighth, isDotted: true), - new Note(Scale.D, 4, LengthType.Quarter), - new Rest(LengthType.Quarter) - }; - var leftChannel = new TriangleSoundChannel(tempo, format, PanType.Left) - { - new Note(Scale.C, 3, LengthType.Eighth, isDotted: true), - new Note(Scale.D, 3, LengthType.Quarter), - new Rest(LengthType.Quarter) - }; - - var channels = new List() { rightChannel, rightChannel2, leftChannel }; - // Mixing is done by the 'StereoMixer' class. - return new StereoMixer(channels).Mix(); - } - - private IReadOnlyList GetComponents() - { - return new List() - { - new Note(Scale.E, 5, LengthType.Eighth), - new Note(Scale.F, 5, LengthType.Eighth), - new Note(Scale.G, 5, LengthType.Eighth), - }; - } -} From 2259ee26ffaaf5cc59fc5a30a6c42e1c2d332976 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:01:24 +0900 Subject: [PATCH 16/23] :+1: Change the syntax of SMSC --- src/SoundMaker/ScoreData/SMSC/Lexer.cs | 15 +++- src/SoundMaker/ScoreData/SMSC/Parser.cs | 80 ++++++++++++++++------ src/SoundMaker/ScoreData/SMSC/SMSC.bnf | 7 +- src/SoundMaker/ScoreData/SMSC/TokenType.cs | 11 +-- test/UnitTests/ScoreData/SMSC/LexerTest.cs | 6 ++ 5 files changed, 89 insertions(+), 30 deletions(-) diff --git a/src/SoundMaker/ScoreData/SMSC/Lexer.cs b/src/SoundMaker/ScoreData/SMSC/Lexer.cs index 7e29102..9c693f6 100644 --- a/src/SoundMaker/ScoreData/SMSC/Lexer.cs +++ b/src/SoundMaker/ScoreData/SMSC/Lexer.cs @@ -17,10 +17,18 @@ public List ReadAll() { var tokens = new List(); var data = _data.Replace("\r\n", "\n").Replace('\r', '\n'); - var lines = data.Split('\n'); + // 空白、空文字列、コメントだけの行をあらかじめ除外する + var lines = data.Split('\n').ToArray(); for (var i = 0; i < lines.Length; i++) { var line = lines[i]; + + // 空白またはコメントだけの行はスキップする + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) + { + continue; + } + var lineNumber = i + 1; var chars = line.ToCharArray(); var otherTypeLiteralBuilder = new StringBuilder(); @@ -88,6 +96,7 @@ public List ReadAll() '(' => new(TokenType.LeftParentheses, "(", lineNumber), ')' => new(TokenType.RightParentheses, ")", lineNumber), ',' => new(TokenType.Comma, ",", lineNumber), + ';' => new(TokenType.Semicolon, ";", lineNumber), _ => new(TokenType.Unknown, chars[j].ToString(), lineNumber), }; tokens.Add(token); @@ -107,6 +116,8 @@ public List ReadAll() tokens.Add(new(type, literal, lineNumber)); _ = otherTypeLiteralBuilder.Clear(); } + + tokens.Add(new(TokenType.LineBreak, "\n", lineNumber)); } return tokens; } @@ -146,6 +157,6 @@ private bool IsCommentPrefix(char character, char? nextCharacter) private bool IsSymbol(char character) { - return character is '.' or '#' or '(' or ')' or ','; + return character is '.' or '#' or '(' or ')' or ',' or ';'; } } diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs index 7342486..c8409b5 100644 --- a/src/SoundMaker/ScoreData/SMSC/Parser.cs +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -73,11 +73,22 @@ public SMSCReadResult Parse() var errors = new List(); while (_tokens.Count > 0) { - var statementResult = ParseStatement(); - if (statementResult.TryGetValue(out var statement)) + var statementResultNullable = ParseStatement(); + + // 文末だった場合はスキップする。 + if (statementResultNullable is null) + { + continue; + } + + if (statementResultNullable.TryGetValue(out var statement)) { statements.Add(statement); } + else if (statementResultNullable.Error is not null) + { + errors.Add(statementResultNullable.Error); + } } var result = errors.Any() ? SMSCReadResult.Failure(errors) : @@ -88,23 +99,39 @@ public SMSCReadResult Parse() /// /// 文を解析する /// - /// 解析結果 - private ParseResult ParseStatement() + /// 解析結果(文末だった場合はnull) + private ParseResult? ParseStatement() { // 現在のトークンの種類を見たいだけなのでPeekする。 if (!_tokens.TryPeek(out var current)) { return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); } - // 文の解析を現在のトークンに基づいて行う。 - var statementResult = current.Type switch + + ParseResult? statementResult = null; + // 文末ではない場合のみ解析を行う。 + if (!IsEndOfStatement(current)) { - TokenType.Tie => ParseTie(), - TokenType.Tuplet => ParseTuplet(), - TokenType.Rest => ParseRest(), - // 上記に当てはまらない場合は音符として解析する。 - _ => ParseNote() - }; + // 文の解析を現在のトークンに基づいて行う。 + statementResult = current.Type switch + { + TokenType.Tie => ParseTie(), + TokenType.Tuplet => ParseTuplet(), + TokenType.Rest => ParseRest(), + // 上記に当てはまらない場合は音符として解析する。 + _ => ParseNote() + }; + } + + // 文の終わりまで進める + while (_tokens.TryDequeue(out current)) + { + if (IsEndOfStatement(current)) + { + break; + } + } + return statementResult; } @@ -131,10 +158,10 @@ private ParseResult ParseTie() return new(null, scaleResult.Error); } - // 引数内の長さを表す情報を解析する。トークンがなくなるか')'になるまで解析する。 + // 引数内の長さを表す情報を解析する。トークンがなくなるか')'や文の終わりになるまで解析する。 // 長さの情報は可変長引数として取る。 var notes = new List(); - while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses) + while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses && !IsEndOfStatement(current)) { // ','の場合はDequeueする。tie(音程, 長さ, 長さ, ...)という並びなので、while文の先頭にこの処理を置く。 // 音程解析はwhileの前で実装済みなので、", 長さ, 長さ, ..."という部分をここで解析する。 @@ -158,11 +185,12 @@ private ParseResult ParseTie() var note = new Note(scale.Scale, scale.ScaleNumber, length.LengthType, length.IsDotted); notes.Add(note); } - // トークンが空になっている場合、')'が存在していないので、エラーを出力。 - if (_tokens.Count == 0) + // ')'でない場合、エラーを出力。 + if (!_tokens.TryPeek(out current) || current.Type is not TokenType.RightParentheses) { return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken?.LineNumber ?? 0)); } + // ')'を破棄 _ = _tokens.Dequeue(); @@ -208,9 +236,9 @@ private ParseResult ParseTuplet() // 連符にするSoundComponentを解析する var components = new List(); - // 引数内の長さを表す情報を解析する。トークンがなくなるか')'になるまで解析する。 + // 引数内の長さを表す情報を解析する。トークンがなくなるか')'や文の終わりになるまで解析する。 // 情報は可変長引数として取る。 - while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses) + while (_tokens.TryPeek(out current) && current.Type is not TokenType.RightParentheses && !IsEndOfStatement(current)) { // ','の場合はDequeueする。tup(長さ, 音の部品, 音の部品, ...)という並びなので、while文の先頭にこの処理を置く。 // 長さの解析はwhileの前で実装済みなので、", 音の部品, 音の部品, ..."という部分をここで解析する。 @@ -287,11 +315,12 @@ private ParseResult ParseTuplet() } } - // トークンが空になっている場合、')'が存在していない - if (_tokens.Count == 0) + // ')'でない場合、エラーを出力。 + if (!_tokens.TryPeek(out current) || current.Type is not TokenType.RightParentheses) { return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken?.LineNumber ?? 0)); } + // ')'を破棄 _ = _tokens.Dequeue(); @@ -430,7 +459,11 @@ private ParseResult ParseScale() } var scaleResult = new ScaleResult(scaleNullable.Value, scaleNumber); - return new(scaleResult, null); + + if (ScaleHertzDictionary.IsValidScale(scaleResult.Scale, scaleResult.ScaleNumber)) + { + return new(scaleResult, null); + } } // 上記条件に当てはまらない場合はエラー return new(null, new(SMSCReadErrorType.InvalidScale, current.LineNumber)); @@ -485,4 +518,9 @@ private ParseResult ParseLength() var length = new LengthResult(lengthTypeNullable.Value, isDotted); return new(length, null); } + + private static bool IsEndOfStatement(Token token) + { + return token.Type is TokenType.LineBreak or TokenType.Semicolon; + } } diff --git a/src/SoundMaker/ScoreData/SMSC/SMSC.bnf b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf index ec2c43b..1bd40bd 100644 --- a/src/SoundMaker/ScoreData/SMSC/SMSC.bnf +++ b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf @@ -23,10 +23,13 @@ rest ::= 'rest' ',' tie ::= 'tie' '(' ',' |'rest' {',' |'rest'}* ')' # 連符の表記方法 - Tuplet notation -tuplet ::= 'tup' '(' ',' { '.'?}|{'rest' '.'?}| {',' '.'?}* ')' +tuplet ::= 'tup' '(' ',' { '.'?}|{'rest' '.'?}| {',' { '.'?}|{'rest' '.'?}|}* ')' + +# 文末 - End of statement +endOfStatement ::= ';'|\n|\r|\r\n # 文の定義 - Sentence definition -statement ::= ||| +statement ::= { ||| }? # SMSC(SoundMaker SCore)データ - SMSC(SoundMaker SCore) data smsc ::= * diff --git a/src/SoundMaker/ScoreData/SMSC/TokenType.cs b/src/SoundMaker/ScoreData/SMSC/TokenType.cs index 46a6fb2..c015ec8 100644 --- a/src/SoundMaker/ScoreData/SMSC/TokenType.cs +++ b/src/SoundMaker/ScoreData/SMSC/TokenType.cs @@ -2,11 +2,12 @@ internal enum TokenType { // Symbols - Dot, // . - Sharp, // # - LeftParentheses, // ( - RightParentheses, // ) - Comma, // , + Dot, + Sharp, + LeftParentheses, + RightParentheses, + Semicolon, + Comma, // Characters Alphabet, Number, diff --git a/test/UnitTests/ScoreData/SMSC/LexerTest.cs b/test/UnitTests/ScoreData/SMSC/LexerTest.cs index ec88de0..128999b 100644 --- a/test/UnitTests/ScoreData/SMSC/LexerTest.cs +++ b/test/UnitTests/ScoreData/SMSC/LexerTest.cs @@ -26,6 +26,7 @@ public void TestReadAll_Note() new(TokenType.Number, "4", 2), new(TokenType.Comma, ",", 2), new(TokenType.Number, "16", 2), + new(TokenType.LineBreak, "\n", 2), }; var tokens = lexer.ReadAll(); Assert.Equal(expected.Count, tokens.Count); @@ -34,6 +35,7 @@ public void TestReadAll_Note() AssertToken(expected[1], tokens[1]); AssertToken(expected[2], tokens[2]); AssertToken(expected[3], tokens[3]); + AssertToken(expected[4], tokens[4]); } [Fact(DisplayName = "付点音符を解析できるか")] @@ -50,6 +52,7 @@ public void TestReadAll_DottedNote() new(TokenType.Comma, ",", 2), new(TokenType.Number, "16", 2), new(TokenType.Dot, ".", 2), + new(TokenType.LineBreak, "\n", 2), }; var tokens = lexer.ReadAll(); Assert.Equal(expected.Count, tokens.Count); @@ -59,6 +62,7 @@ public void TestReadAll_DottedNote() AssertToken(expected[2], tokens[2]); AssertToken(expected[3], tokens[3]); AssertToken(expected[4], tokens[4]); + AssertToken(expected[5], tokens[5]); } [Fact(DisplayName = "半音上の音符を解析できるか")] @@ -75,6 +79,7 @@ public void TestReadAll_SharpNote() new(TokenType.Number, "4", 2), new(TokenType.Comma, ",", 2), new(TokenType.Number, "16", 2), + new(TokenType.LineBreak, "\n", 2), }; var tokens = lexer.ReadAll(); Assert.Equal(expected.Count, tokens.Count); @@ -84,6 +89,7 @@ public void TestReadAll_SharpNote() AssertToken(expected[2], tokens[2]); AssertToken(expected[3], tokens[3]); AssertToken(expected[4], tokens[4]); + AssertToken(expected[5], tokens[5]); } private void AssertToken(Token expected, Token actual) From d10b62556aae5862d629476facff988b48046ff5 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:05:02 +0900 Subject: [PATCH 17/23] :arrow_double_down: Add the function to import sound components into channel. --- .../Sounds/SoundChannels/SoundChannelBase.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs b/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs index decfca4..5610f1d 100644 --- a/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs +++ b/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs @@ -55,7 +55,7 @@ public SoundChannelBase(int tempo, SoundFormat format, PanType panType) /// /// サウンドコンポーネントのリスト /// - protected List SoundComponents { get; } = new List(); + protected List SoundComponents { get; private set; } = new List(); public SoundFormat Format { get; } @@ -107,6 +107,16 @@ public void RemoveAt(int index) _ = SoundComponents.Remove(component); } + /// + /// Import sound components. サウンドコンポーネントをインポートする。 + /// + /// Sound components. サウンドコンポーネント + public void Import(IEnumerable components) + { + SoundComponents = new List(components); + WaveArrayLength = components.Sum(component => component.GetWaveArrayLength(Format, Tempo)); + } + public abstract ushort[] GenerateWave(); public IEnumerator GetEnumerator() From 0c5acbf8524c27f60ca83f7b18765dfc68c3d11a Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:05:27 +0900 Subject: [PATCH 18/23] :+1: Add tests of errors. --- test/UnitTests/ScoreData/SMSC/ParserTest.cs | 74 ++++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/test/UnitTests/ScoreData/SMSC/ParserTest.cs b/test/UnitTests/ScoreData/SMSC/ParserTest.cs index a8d2230..dead838 100644 --- a/test/UnitTests/ScoreData/SMSC/ParserTest.cs +++ b/test/UnitTests/ScoreData/SMSC/ParserTest.cs @@ -130,7 +130,7 @@ public void TestParse_DottedRest() [Fact(DisplayName = "タイを解析できるか")] public void TestParse_Tie() { - var data = @"tie(C#4, 16, 8, 8, 8.)"; + var data = @"tie(C#4, 16, 8.)"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -139,8 +139,6 @@ public void TestParse_Tie() { new Tie(new(Scale.CSharp, 4, LengthType.Sixteenth), new List() { - new(LengthType.Eighth), - new(LengthType.Eighth), new(LengthType.Eighth, true), }), }; @@ -215,4 +213,74 @@ public void TestParse_TupletInTupletAndTie() var actual = Assert.IsType(actualCollection[0]); Assert.Equal(expected.Count, actual.Count); } + + // ------------------エラーのテスト--------------------------- + + [Fact(DisplayName = "空の文でエラーが発生しないか")] + public void TestEmptyStatment() + { + var data = ";;;"; + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + var actual = parser.Parse().Errors; + Assert.Empty(actual); + } + + [Theory(DisplayName = "音程解析エラーのテスト")] + [InlineData("C100,1\n D8,1 // A0~C8のみ許可する", 2)] + [InlineData("A0,1; C8,1", 0)] + public void Test_ScaleErrors(string data, int expectedCount) + { + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + var actual = parser.Parse().Errors.Count(err => err.Type is SMSCReadErrorType.InvalidScale); + Assert.Equal(expectedCount, actual); + } + + [Theory(DisplayName = "長さ解析エラーのテスト")] + [InlineData("C4,1; C4,2; C4,4; C4,8; C4,16; C4,32; C4,1; C4,64", 0)] + [InlineData("C4,0; C4,65; C4,", 3)] + public void Test_LengthErrors(string data, int expectedCount) + { + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + var actual = parser.Parse().Errors.Count(err => err.Type is SMSCReadErrorType.InvalidLength); + Assert.Equal(expectedCount, actual); + } + + [Theory(DisplayName = "カンマが見つからないエラーのテスト")] + [InlineData("C4.1; tie(C4.1)", 2)] + public void Test_NotFoundCommaErrors(string data, int expectedCount) + { + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + var actual = parser.Parse().Errors.Count(err => err.Type is SMSCReadErrorType.NotFoundComma); + Assert.Equal(expectedCount, actual); + } + + [Theory(DisplayName = ")が見つからないエラーのテスト")] + [InlineData("tie(C4,1;tup(4.,C4.", 2)] + public void TestNotFoundRightParenthesesErrors(string data, int expectedCount) + { + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + var actual = parser.Parse().Errors.Count(err => err.Type is SMSCReadErrorType.NotFoundRightParentheses); + Assert.Equal(expectedCount, actual); + } + + [Theory(DisplayName = "(が見つからないエラーのテスト")] + [InlineData("tie)C4,1;tup4.,C4.", 2)] + public void TestNotFoundLeftParenthesesErrors(string data, int expectedCount) + { + var lexer = new Lexer(data); + var tokens = lexer.ReadAll(); + var parser = new Parser(tokens); + var actual = parser.Parse().Errors.Count(err => err.Type is SMSCReadErrorType.NotFoundLeftParentheses); + Assert.Equal(expectedCount, actual); + } } From ae555899e7e28fd7c6d1ba6119c00ef7b3f4b063 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:08:42 +0900 Subject: [PATCH 19/23] :+1: Fix a definition of statement --- src/SoundMaker/ScoreData/SMSC/SMSC.bnf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SoundMaker/ScoreData/SMSC/SMSC.bnf b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf index 1bd40bd..33142aa 100644 --- a/src/SoundMaker/ScoreData/SMSC/SMSC.bnf +++ b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf @@ -29,7 +29,7 @@ tuplet ::= 'tup' '(' ',' { '.'?}|{'rest' '.'?}| {',' {||| }? +statement ::= { ||| }? # SMSC(SoundMaker SCore)データ - SMSC(SoundMaker SCore) data smsc ::= * From d5d2df7f792ebb7e5505f5e989ae164f8198f777 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:59:39 +0900 Subject: [PATCH 20/23] :recycle: refatoring --- src/SoundMaker/ScoreData/SMSC/Parser.cs | 34 ------------------- src/SoundMaker/ScoreData/SMSC/TokenQueue.cs | 36 +++++++++++++++++++++ test/UnitTests/ScoreData/SMSC/ParserTest.cs | 22 ++++++------- 3 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 src/SoundMaker/ScoreData/SMSC/TokenQueue.cs diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs index c8409b5..afbfcba 100644 --- a/src/SoundMaker/ScoreData/SMSC/Parser.cs +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -2,40 +2,6 @@ using System.Diagnostics.CodeAnalysis; namespace SoundMaker.ScoreData.SMSC; - -internal class TokenQueue -{ - private readonly Queue _tokens; - - public TokenQueue(IEnumerable tokens) - { - _tokens = new(tokens); - } - - public Token? PrevToken { get; private set; } - - public int Count => _tokens.Count; - - public bool TryDequeue([MaybeNullWhen(false)] out Token token) - { - if (_tokens.TryDequeue(out token)) - { - PrevToken = token; - return true; - } - return false; - } - - public Token Dequeue() - { - return _tokens.Dequeue(); - } - - public bool TryPeek([MaybeNullWhen(false)] out Token token) - { - return _tokens.TryPeek(out token); - } -} internal class Parser { private record LengthResult(LengthType LengthType, bool IsDotted); diff --git a/src/SoundMaker/ScoreData/SMSC/TokenQueue.cs b/src/SoundMaker/ScoreData/SMSC/TokenQueue.cs new file mode 100644 index 0000000..066af4a --- /dev/null +++ b/src/SoundMaker/ScoreData/SMSC/TokenQueue.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; + +namespace SoundMaker.ScoreData.SMSC; +internal class TokenQueue +{ + private readonly Queue _tokens; + + public TokenQueue(IEnumerable tokens) + { + _tokens = new(tokens); + } + + public Token? PrevToken { get; private set; } + + public int Count => _tokens.Count; + + public bool TryDequeue([MaybeNullWhen(false)] out Token token) + { + if (_tokens.TryDequeue(out token)) + { + PrevToken = token; + return true; + } + return false; + } + + public Token Dequeue() + { + return _tokens.Dequeue(); + } + + public bool TryPeek([MaybeNullWhen(false)] out Token token) + { + return _tokens.TryPeek(out token); + } +} diff --git a/test/UnitTests/ScoreData/SMSC/ParserTest.cs b/test/UnitTests/ScoreData/SMSC/ParserTest.cs index dead838..9608c3b 100644 --- a/test/UnitTests/ScoreData/SMSC/ParserTest.cs +++ b/test/UnitTests/ScoreData/SMSC/ParserTest.cs @@ -8,7 +8,7 @@ public class ParserTest [Fact(DisplayName = "普通の音符を解析できるか")] public void TestParse_Note() { - var data = @"C4, 16"; + var data = "C4, 16"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -34,7 +34,7 @@ public void TestParse_Note() [Fact(DisplayName = "付点音符を解析できるか")] public void TestParse_DottedNote() { - var data = @"C4, 16."; + var data = "C4, 16."; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -59,7 +59,7 @@ public void TestParse_DottedNote() [Fact(DisplayName = "半音上の音符を解析できるか")] public void TestParse_SharpNote() { - var data = @"C#4, 16"; + var data = "C#4, 16"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -84,7 +84,7 @@ public void TestParse_SharpNote() [Fact(DisplayName = "普通の休符を解析できるか")] public void TestParse_Rest() { - var data = @"rest, 16"; + var data = "rest, 16"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -107,7 +107,7 @@ public void TestParse_Rest() [Fact(DisplayName = "付点休符を解析できるか")] public void TestParse_DottedRest() { - var data = @"rest, 16."; + var data = "rest, 16."; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -130,7 +130,7 @@ public void TestParse_DottedRest() [Fact(DisplayName = "タイを解析できるか")] public void TestParse_Tie() { - var data = @"tie(C#4, 16, 8.)"; + var data = "tie(C#4, 16, 8.)"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -155,7 +155,7 @@ public void TestParse_Tie() [Fact(DisplayName = "連符を解析できるか")] public void TestParse_Tuplet() { - var data = @"tup(16, C#4, rest, E4.)"; + var data = "tup(16, C#4, rest, E4.)"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -182,7 +182,7 @@ public void TestParse_Tuplet() [Fact(DisplayName = "連符内のタイや連符を解析できるか")] public void TestParse_TupletInTupletAndTie() { - var data = @"tup(16, tup(16, rest, rest), tie(C#4, 16, 8, 8, 8.))"; + var data = "tup(16, tup(16, rest, rest), tie(C#4, 16, 8, 8, 8.))"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -217,7 +217,7 @@ public void TestParse_TupletInTupletAndTie() // ------------------エラーのテスト--------------------------- [Fact(DisplayName = "空の文でエラーが発生しないか")] - public void TestEmptyStatment() + public void Test_EmptyStatment() { var data = ";;;"; var lexer = new Lexer(data); @@ -264,7 +264,7 @@ public void Test_NotFoundCommaErrors(string data, int expectedCount) [Theory(DisplayName = ")が見つからないエラーのテスト")] [InlineData("tie(C4,1;tup(4.,C4.", 2)] - public void TestNotFoundRightParenthesesErrors(string data, int expectedCount) + public void Test_NotFoundRightParenthesesErrors(string data, int expectedCount) { var lexer = new Lexer(data); var tokens = lexer.ReadAll(); @@ -275,7 +275,7 @@ public void TestNotFoundRightParenthesesErrors(string data, int expectedCount) [Theory(DisplayName = "(が見つからないエラーのテスト")] [InlineData("tie)C4,1;tup4.,C4.", 2)] - public void TestNotFoundLeftParenthesesErrors(string data, int expectedCount) + public void Test_NotFoundLeftParenthesesErrors(string data, int expectedCount) { var lexer = new Lexer(data); var tokens = lexer.ReadAll(); From 2ce4002fa373164ea24cff8c4e6e3bdffc90a8b1 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:03:59 +0900 Subject: [PATCH 21/23] :+1: Remove dotted component in tuplet --- src/SoundMaker/ScoreData/SMSC/Parser.cs | 18 ++---------------- src/SoundMaker/ScoreData/SMSC/SMSC.bnf | 2 +- .../ScoreData/SMSC/SMSCSerializer.cs | 2 +- test/UnitTests/ScoreData/SMSC/ParserTest.cs | 6 +++--- .../ScoreData/SMSC/SMSCSerializerTest.cs | 6 +++--- 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs index afbfcba..dfa5b65 100644 --- a/src/SoundMaker/ScoreData/SMSC/Parser.cs +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -248,15 +248,8 @@ private ParseResult ParseTuplet() { // 'rest'のトークンを破棄 _ = _tokens.Dequeue(); - var isDottedRest = false; - // '.'の場合は付点休符とする - if (_tokens.TryPeek(out var dotToken) && dotToken.Type is TokenType.Dot) - { - _ = _tokens.Dequeue(); - isDottedRest = true; - } // 連符なので長さは適当に入れる - var rest = new Rest(LengthType.Whole, isDottedRest); + var rest = new Rest(LengthType.Whole, false); components.Add(rest); } // 上記以外は音符として解析するが、連符専用の書き方なのでここで実装する。 @@ -268,15 +261,8 @@ private ParseResult ParseTuplet() { return new(null, scaleResult.Error); } - var isDottedNote = false; - // '.'の場合は付点音符とする - if (_tokens.TryPeek(out var dotToken) && dotToken.Type is TokenType.Dot) - { - _ = _tokens.Dequeue(); - isDottedNote = true; - } // 連符なので長さは適当に入れる - var note = new Note(scale.Scale, scale.ScaleNumber, LengthType.Whole, isDottedNote); + var note = new Note(scale.Scale, scale.ScaleNumber, LengthType.Whole, false); components.Add(note); } } diff --git a/src/SoundMaker/ScoreData/SMSC/SMSC.bnf b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf index 33142aa..cb432d1 100644 --- a/src/SoundMaker/ScoreData/SMSC/SMSC.bnf +++ b/src/SoundMaker/ScoreData/SMSC/SMSC.bnf @@ -23,7 +23,7 @@ rest ::= 'rest' ',' tie ::= 'tie' '(' ',' |'rest' {',' |'rest'}* ')' # 連符の表記方法 - Tuplet notation -tuplet ::= 'tup' '(' ',' { '.'?}|{'rest' '.'?}| {',' { '.'?}|{'rest' '.'?}|}* ')' +tuplet ::= 'tup' '(' ',' |'rest'| {',' |'rest'|}* ')' # 文末 - End of statement endOfStatement ::= ';'|\n|\r|\r\n diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs b/src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs index e5d177d..4492671 100644 --- a/src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs +++ b/src/SoundMaker/ScoreData/SMSC/SMSCSerializer.cs @@ -17,7 +17,7 @@ public static string Serialize(IEnumerable components) Tuplet tuplet => SerializeTuplet(tuplet), _ => "", }; - _ = smscBuilder.AppendLine(line); + _ = smscBuilder.Append(line).Append('\n'); } return smscBuilder.ToString(); } diff --git a/test/UnitTests/ScoreData/SMSC/ParserTest.cs b/test/UnitTests/ScoreData/SMSC/ParserTest.cs index 9608c3b..2a986e1 100644 --- a/test/UnitTests/ScoreData/SMSC/ParserTest.cs +++ b/test/UnitTests/ScoreData/SMSC/ParserTest.cs @@ -155,7 +155,7 @@ public void TestParse_Tie() [Fact(DisplayName = "連符を解析できるか")] public void TestParse_Tuplet() { - var data = "tup(16, C#4, rest, E4.)"; + var data = "tup(16, C#4, rest, E4)"; var lexer = new Lexer(data); var tokens = lexer.ReadAll(); var parser = new Parser(tokens); @@ -263,7 +263,7 @@ public void Test_NotFoundCommaErrors(string data, int expectedCount) } [Theory(DisplayName = ")が見つからないエラーのテスト")] - [InlineData("tie(C4,1;tup(4.,C4.", 2)] + [InlineData("tie(C4,1;tup(4,C4", 2)] public void Test_NotFoundRightParenthesesErrors(string data, int expectedCount) { var lexer = new Lexer(data); @@ -274,7 +274,7 @@ public void Test_NotFoundRightParenthesesErrors(string data, int expectedCount) } [Theory(DisplayName = "(が見つからないエラーのテスト")] - [InlineData("tie)C4,1;tup4.,C4.", 2)] + [InlineData("tie)C4,1;tup.4,C4", 2)] public void Test_NotFoundLeftParenthesesErrors(string data, int expectedCount) { var lexer = new Lexer(data); diff --git a/test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs b/test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs index 63b6907..10a3ef4 100644 --- a/test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs +++ b/test/UnitTests/ScoreData/SMSC/SMSCSerializerTest.cs @@ -8,11 +8,11 @@ public class SMSCSerializerTest public void TestSerialize() { var note = new Note(Scale.C, 4, LengthType.Whole, true); - + var tupletNote = new Note(Scale.C, 4, LengthType.Whole); var expected = @"C4,1. rest,4 tie(C4,1.,1.,1.) -tup(2,C4.,C4.,C4.) +tup(2,C4,C4,C4) "; var components = new List() @@ -23,7 +23,7 @@ public void TestSerialize() { note, note }), - new Tuplet(new List() {note, note, note}, LengthType.Half, false) + new Tuplet(new List() { tupletNote, tupletNote, tupletNote}, LengthType.Half, false) }; var actual = SMSCSerializer.Serialize(components); From 33088c529c810d593c76a89a0b2a333f6382ff2c Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:19:08 +0900 Subject: [PATCH 22/23] :recycle: Rename workflow name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01116e7..9f17d8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: .NET Core Desktop +name: Build and Test on: push: From 73af19519e5eb7ede015f19770f7b2309489a8f4 Mon Sep 17 00:00:00 2001 From: AutumnSky1010 <66455966+AutumnSky1010@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:43:10 +0900 Subject: [PATCH 23/23] :+1: Add comments --- src/SoundMaker/ScoreData/SMSC/Error.cs | 28 ++++++++++- src/SoundMaker/ScoreData/SMSC/Parser.cs | 48 +++++++++---------- .../ScoreData/SMSC/SMSCReadErrorType.cs | 4 ++ .../ScoreData/SMSC/SMSCReadResult.cs | 5 ++ 4 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/SoundMaker/ScoreData/SMSC/Error.cs b/src/SoundMaker/ScoreData/SMSC/Error.cs index 7fa3297..bfcde3b 100644 --- a/src/SoundMaker/ScoreData/SMSC/Error.cs +++ b/src/SoundMaker/ScoreData/SMSC/Error.cs @@ -1,2 +1,28 @@ namespace SoundMaker.ScoreData.SMSC; -public record Error(SMSCReadErrorType Type, int LineNumber); +public record Error +{ + internal Error(SMSCReadErrorType type, Token? token) + { + Type = type; + LineNumber = token?.LineNumber ?? 0; + Literal = token?.Literal ?? ""; + } + + /// + /// Type of errors
+ /// エラーの種類 + ///
+ public SMSCReadErrorType Type { get; } + + /// + /// String of error location
+ /// エラー箇所の文字列 + ///
+ public string Literal { get; } + + /// + /// Line number of error.
+ /// エラー箇所の行番号 + ///
+ public int LineNumber { get; } +} diff --git a/src/SoundMaker/ScoreData/SMSC/Parser.cs b/src/SoundMaker/ScoreData/SMSC/Parser.cs index dfa5b65..14db2d4 100644 --- a/src/SoundMaker/ScoreData/SMSC/Parser.cs +++ b/src/SoundMaker/ScoreData/SMSC/Parser.cs @@ -71,7 +71,7 @@ public SMSCReadResult Parse() // 現在のトークンの種類を見たいだけなのでPeekする。 if (!_tokens.TryPeek(out var current)) { - return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken)); } ParseResult? statementResult = null; @@ -110,12 +110,12 @@ private ParseResult ParseTie() // トークンが'tie'かを確認する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Tie) { - return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken)); } // トークンが'('かを確認する if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.LeftParentheses) { - return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken)); } // 音程を解析する var scaleResult = ParseScale(); @@ -137,7 +137,7 @@ private ParseResult ParseTie() } else { - return new(null, new(SMSCReadErrorType.NotFoundComma, current.LineNumber)); + return new(null, new(SMSCReadErrorType.NotFoundComma, current)); } // 長さを解析する。 @@ -154,7 +154,7 @@ private ParseResult ParseTie() // ')'でない場合、エラーを出力。 if (!_tokens.TryPeek(out current) || current.Type is not TokenType.RightParentheses) { - return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken)); } // ')'を破棄 @@ -165,7 +165,7 @@ private ParseResult ParseTie() // 音符が0個の場合は解析できていない。 if (notes.Count == 0) { - return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken)); } // 個数が1個の場合はadditionalNotesに空配列を渡す else if (notes.Count == 1) @@ -190,12 +190,12 @@ private ParseResult ParseTuplet() // トークンが'tup'かを確認する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Tuplet) { - return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken)); } // トークンが'('かを確認する if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.LeftParentheses) { - return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.NotFoundLeftParentheses, _tokens.PrevToken)); } // 長さを解析する var lengthResult = ParseLength(); @@ -214,13 +214,13 @@ private ParseResult ParseTuplet() } else { - return new(null, new(SMSCReadErrorType.NotFoundComma, current.LineNumber)); + return new(null, new(SMSCReadErrorType.NotFoundComma, current)); } // 条件分岐の為にトークンをピークする if (!_tokens.TryPeek(out current)) { - return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken)); } // タイの場合はタイを解析する @@ -270,7 +270,7 @@ private ParseResult ParseTuplet() // ')'でない場合、エラーを出力。 if (!_tokens.TryPeek(out current) || current.Type is not TokenType.RightParentheses) { - return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.NotFoundRightParentheses, _tokens.PrevToken)); } // ')'を破棄 @@ -299,7 +299,7 @@ private ParseResult ParseNote() // ','かを判定する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Comma) { - return new(null, new Error(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new Error(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken)); } // 長さを解析する var lengthResult = ParseLength(); @@ -320,12 +320,12 @@ private ParseResult ParseRest() // 'rest'かを判定する if (!_tokens.TryDequeue(out var current) || current.Type is not TokenType.Rest) { - return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.UndefinedStatement, _tokens.PrevToken)); } // ','かを判定する if (!_tokens.TryDequeue(out current) || current.Type is not TokenType.Comma) { - return new(null, new(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new(SMSCReadErrorType.NotFoundComma, _tokens.PrevToken)); } // 長さを解析する var lengthResult = ParseLength(); @@ -346,14 +346,14 @@ private ParseResult ParseScale() // 最低でもトークンが2つ取れないと解析できないので、ここで判定する。 if (!_tokens.TryDequeue(out var current) || !_tokens.TryDequeue(out var next)) { - return new(null, new Error(SMSCReadErrorType.InvalidScale, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, _tokens.PrevToken)); } Scale? scaleNullable; // 音程はアルファベットで書かれている if (current.Type is not TokenType.Alphabet) { - return new(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, current)); } int scaleNumber; @@ -362,11 +362,11 @@ private ParseResult ParseScale() { if (!_tokens.TryDequeue(out var nextNext)) { - return new(null, new Error(SMSCReadErrorType.InvalidScale, next.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, next)); } if (nextNext.Type is not TokenType.Number) { - return new(null, new Error(SMSCReadErrorType.InvalidScale, nextNext.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, nextNext)); } scaleNullable = current.Literal switch @@ -381,7 +381,7 @@ private ParseResult ParseScale() if (scaleNullable is null) { - return new(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, current)); } scaleNumber = int.Parse(nextNext.Literal); @@ -407,7 +407,7 @@ private ParseResult ParseScale() if (scaleNullable is null) { - return new(null, new Error(SMSCReadErrorType.InvalidScale, current.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidScale, current)); } var scaleResult = new ScaleResult(scaleNullable.Value, scaleNumber); @@ -418,7 +418,7 @@ private ParseResult ParseScale() } } // 上記条件に当てはまらない場合はエラー - return new(null, new(SMSCReadErrorType.InvalidScale, current.LineNumber)); + return new(null, new(SMSCReadErrorType.InvalidScale, current)); } /// @@ -432,13 +432,13 @@ private ParseResult ParseLength() if (!_tokens.TryDequeue(out var current)) { - return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken)); } // 数字かを判定する if (current.Type is not TokenType.Number) { - return new(null, new Error(SMSCReadErrorType.InvalidLength, current.LineNumber)); + return new(null, new Error(SMSCReadErrorType.InvalidLength, current)); } lengthTypeNullable = int.Parse(current.Literal) switch @@ -464,7 +464,7 @@ private ParseResult ParseLength() // 不正な長さの場合 if (lengthTypeNullable is null) { - return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken?.LineNumber ?? 0)); + return new(null, new Error(SMSCReadErrorType.InvalidLength, _tokens.PrevToken)); } var length = new LengthResult(lengthTypeNullable.Value, isDotted); diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs b/src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs index 7e98b45..e42b0d4 100644 --- a/src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs +++ b/src/SoundMaker/ScoreData/SMSC/SMSCReadErrorType.cs @@ -1,5 +1,9 @@ namespace SoundMaker.ScoreData.SMSC; +/// +/// Errors of reading SMSC data
+/// SMSCデータ読み込み時のエラー +///
public enum SMSCReadErrorType { // OOが無い diff --git a/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs b/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs index 461234d..8885fe4 100644 --- a/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs +++ b/src/SoundMaker/ScoreData/SMSC/SMSCReadResult.cs @@ -1,6 +1,11 @@ using SoundMaker.Sounds.Score; namespace SoundMaker.ScoreData.SMSC; + +/// +/// Result of reading SMSC data
+/// SMSCデータの読み取り結果 +///
public class SMSCReadResult { private readonly IReadOnlyList _value;