diff --git a/CHANGELOG.md b/CHANGELOG.md index 137d1394..1756849c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,65 +1,6 @@ -## 0.1.0 +## 0.5.0 -* Initial release. - -## 0.1.1 - -* Fix third-party repos in Android build. Now they are downloaded by the Gradle build if they don't exist. - -## 0.1.2 - -* SampleDescriptor uses default values for most properties. -* README is updated to explain how to build a sampler without an SFZ. -* "pitch" has been replaced with "noteNumber" in all the public APIs. -* Minor refactoring in sfz_parser.dart - -## 0.1.3 - -* Merged PR to expose presetIndex: https://github.com/mikeperri/flutter_sequencer/pull/2 - -## 0.1.4 - -* Merge PR to fix presetIndex assignment: https://github.com/mikeperri/flutter_sequencer/pull/7 -* Fix iOS release mode issue: https://github.com/mikeperri/flutter_sequencer/issues/8 - -## 0.2.0 - -* Migrate to null safety -* Upgrade lint rules - -## 0.2.1 - -* Change AudioKit branch name - -## 0.3.0 - -* Send MIDI note off for all notes when a track is stopped -* Prevent rounding errors in getIsOver() by using frames instead of beats -* Add Track.addNoteOn and .addNoteOff methods - -## 0.3.1 -* Clone third party repos with JGit library instead of deprecated Gradle plugin in Android build -* Update Kotlin version to get rid of warnings in Android build - -## 0.3.2 -* Set Xcode STRIP_STYLE to "non-global" in podspec - -## 0.4.0 -* Replaced AudioKit Sampler with [sfizz](https://sfz.tools/sfizz/) because it supports many more SFZ opcodes, including filters and effects, and can stream samples from disk instead of loading them all into RAM. -* Exposed APIs for scheduling MIDI CC and pitch bend events -* BREAKING CHANGE - Replaced SamplerInstrument with RuntimeSfzInstrument. Instead of creating SampleDescriptors, now you have to build an Sfz object. -* BREAKING CHANGE - Assets are copied to context.filesDir on Android for SFZ instruments. See the README for more info. -* BREAKING CHANGE - SFZ player no longer handles URL-decoding asset paths - -## 0.4.1 -* Update README - -## 0.4.2 -* Remove hard-coded FLUTTER_ROOT in example iOS app -* Run `flutter format` - -## 0.4.3 -* Fix for sfizz.hpp file not found on ios release build - -## 0.4.4 -* Add ENABLE_TESTABILITY back to pod target xcconfig to fix iOS release mode +* Tested on Flutter 3.41.9 +* Analysis rules enforced +* Refactoring +* C++ static flag \ No newline at end of file diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml new file mode 100644 index 00000000..64d37e60 --- /dev/null +++ b/all_lint_rules.yaml @@ -0,0 +1,237 @@ +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_specify_types + - always_use_package_imports + - annotate_overrides + - annotate_redeclares + - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_dynamic_calls + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_futureor_void + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + - document_ignores + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - invalid_runtime_check_with_js_interop_types + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - matching_super_parameters + - missing_code_block_language_in_doc_comment + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - omit_obvious_local_variable_types + - omit_obvious_property_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + # - public_member_api_docs + - recursive_getters + - remove_deprecations_in_breaking_versions + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - specify_nonobvious_local_variable_types + - specify_nonobvious_property_types + - strict_top_level_inference + - switch_on_type + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + # - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + - unintended_html_in_doc_comment + - unnecessary_async + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_ignore + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_library_name + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unnecessary_unawaited + - unnecessary_underscores + - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_variance + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_null_aware_elements + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - use_truncating_division + - valid_regexps + - void_checks \ No newline at end of file diff --git a/analysis_app.yaml b/analysis_app.yaml new file mode 100644 index 00000000..ecc578cc --- /dev/null +++ b/analysis_app.yaml @@ -0,0 +1,203 @@ +analysis_options.yamlinclude: + - package:flutter_lints/flutter.yaml + - package:lints/recommended.yaml + - ./analysis_options.yaml + +linter: + rules: + unnecessary_ignore: false + diagnostic_describe_all_properties: false + always_declare_return_types: true + always_put_required_named_parameters_first: true + always_use_package_imports: true + annotate_overrides: true + avoid_bool_literals_in_conditional_expressions: true + avoid_catching_errors: true + avoid_double_and_int_checks: true + avoid_dynamic_calls: true + avoid_empty_else: true + avoid_escaping_inner_quotes: true + avoid_field_initializers_in_const_classes: true + avoid_final_parameters: true + avoid_function_literals_in_foreach_calls: true + avoid_implementing_value_types: true + avoid_init_to_null: true + avoid_multiple_declarations_per_line: true + avoid_null_checks_in_equality_operators: true + avoid_positional_boolean_parameters: true + avoid_print: true + avoid_private_typedef_functions: true + avoid_redundant_argument_values: true + avoid_relative_lib_imports: true + avoid_renaming_method_parameters: true + avoid_return_types_on_setters: true + avoid_returning_null_for_void: true + avoid_setters_without_getters: true + avoid_shadowing_type_parameters: true + avoid_single_cascade_in_expression_statements: true + avoid_type_to_string: true + avoid_types_as_parameter_names: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + camel_case_extensions: true + camel_case_types: true + cancel_subscriptions: true + cast_nullable_to_non_nullable: true + close_sinks: true + comment_references: true + conditional_uri_does_not_exist: true + constant_identifier_names: true + control_flow_in_finally: true + curly_braces_in_flow_control_structures: true + depend_on_referenced_packages: true + deprecated_consistency: true + directives_ordering: true + empty_catches: true + empty_constructor_bodies: true + empty_statements: true + eol_at_end_of_file: true + exhaustive_cases: true + file_names: true + hash_and_equals: true + implementation_imports: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + library_names: true + library_prefixes: true + literal_only_boolean_expressions: true + no_adjacent_strings_in_list: true + no_duplicate_case_values: true + no_leading_underscores_for_library_prefixes: true + no_leading_underscores_for_local_identifiers: true + no_logic_in_create_state: true + no_runtimeType_toString: true + non_constant_identifier_names: true + noop_primitive_operations: true + null_check_on_nullable_type_parameter: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_adjacent_string_concatenation: true + prefer_asserts_in_initializer_lists: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_contains: true + prefer_final_fields: true + prefer_final_in_for_each: true + prefer_final_locals: true + prefer_for_elements_to_map_fromIterable: true + prefer_function_declarations_over_variables: true + prefer_generic_function_type_aliases: true + prefer_if_elements_to_conditional_expressions: true + prefer_if_null_operators: true + prefer_initializing_formals: true + prefer_inlined_adds: true + prefer_interpolation_to_compose_strings: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_is_not_operator: true + prefer_iterable_whereType: true + prefer_null_aware_method_calls: true + prefer_null_aware_operators: true + prefer_spread_collections: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + recursive_getters: true + require_trailing_commas: true + secure_pubspec_urls: true + sized_box_for_whitespace: true + sized_box_shrink_expand: true + slash_for_doc_comments: true + sort_child_properties_last: true + sort_pub_dependencies: true + sort_unnamed_constructors_first: true + test_types_in_equals: true + throw_in_finally: true + tighten_type_of_initializing_formals: true + type_annotate_public_apis: false + type_init_formals: true + unawaited_futures: true + unnecessary_await_in_return: true + unnecessary_brace_in_string_interps: true + unnecessary_breaks: true + unnecessary_const: true + unnecessary_getters_setters: true + unnecessary_lambdas: true + unnecessary_new: true + unnecessary_null_aware_assignments: true + unnecessary_null_checks: true + unnecessary_null_in_if_null_operators: true + unnecessary_nullable_for_final_variable_declarations: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + unnecessary_this: true + unnecessary_to_list_in_spreads: true + unrelated_type_equality_checks: true + use_build_context_synchronously: true + use_colored_box: true + use_decorated_box: true + use_full_hex_values_for_flutter_colors: true + use_function_type_syntax_for_parameters: true + use_if_null_to_convert_nulls_to_bools: true + use_is_even_rather_than_modulo: true + use_named_constants: true + use_raw_strings: true + use_rethrow_when_possible: true + use_setters_to_change_properties: true + use_string_buffers: true + use_super_parameters: true + use_test_throws_matchers: true + valid_regexps: true + void_checks: true + prefer_single_quotes: true + public_member_api_docs: false + +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + inference_failure_on_instance_creation: ignore + inference_failure_on_function_invocation: ignore + inference_failure_on_untyped_parameter: ignore + strict_raw_type: ignore + prefer_relative_imports: ignore + prefer_foreach: ignore + + exclude: + - '**/*.g.dart' + - '**/*.gr.dart' + + # Flutter + - 'lib/generated_plugin_registrant.dart' + + # mockito + - '*.mocks.dart' + - '**/*.mocks.dart' + + # freezed + - '**/*.freezed.dart' + + # protobuf + - '**/*.pb.dart' + - '**/*.pbenum.dart' + - '**/*.pbjson.dart' + + # test_coverage + - test/.test_coverage.dart + +plugins: diff --git a/analysis_options.yaml b/analysis_options.yaml index f49fa824..3716bd46 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,4 @@ -include: package:pedantic/analysis_options.1.11.0.yaml \ No newline at end of file +include: analysis_options_without_plugins.yaml + +plugins: +# riverpod_lint: ^3.1.3 \ No newline at end of file diff --git a/analysis_options_without_plugins.yaml b/analysis_options_without_plugins.yaml new file mode 100644 index 00000000..c2d06858 --- /dev/null +++ b/analysis_options_without_plugins.yaml @@ -0,0 +1,114 @@ +include: all_lint_rules.yaml +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + errors: + # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. + # We explicitly enabled even conflicting rules and are fixing the conflict + # in this file + included_file_warning: ignore + # false positive when using Freezed + invalid_annotation_target: ignore + # I prefer specifying a parameter on a widget even if they are unused (such as Key) + # for the sake of consistency. + unused_element_parameter: false + unrecognized_error_code: ignore + # Remove once we require analyzer 8.0.0 + # We currently support both 7.0 and 8.0, so we can't migrate yet. + deprecated_member_use: ignore + # Analyzer API is experimental, but kind of necessary for analyzer plugins + experimental_member_use: ignore + +linter: + rules: + # No good way to "fix" the diagnostic. Cf https://github.com/dart-lang/sdk/issues/61976 + unsafe_variance: false + + # Conflicts with having to specify types on public properties + omit_obvious_property_types: false + + # Using it on purpose for signaling that an API can be both async and sync + avoid_futureor_void: false + + # I hate it + specify_nonobvious_property_types: false + specify_nonobvious_local_variable_types: false + + # Clashes with newer dartfmt + require_trailing_commas: false + + # Maybe later. Too many to fix right now. + document_ignores: false + + # Not an issue if using dartfmt + no_adjacent_strings_in_list: false + + # False positive for custom enum-like classes (such as Flutter's "Colors") + avoid_classes_with_only_static_members: false + + # False positive when the future is returned by the function + discarded_futures: false + + # Low value and lacks a quick fix + combinators_ordering: false + + # Conflicts with unused variables + no_leading_underscores_for_local_identifiers: false + + # false positive + one_member_abstracts: false + + # too verbose + prefer_final_parameters: false + + # Too verbose with little value, and this is taken care of by the Flutter devtool anyway. + diagnostic_describe_all_properties: false + + # Personal preference. I prefer "if (bool) return;" over having it in multiple lines + always_put_control_body_on_new_line: false + + # Personal preference. I don't find it more readable + cascade_invocations: false + + # Conflicts with `prefer_single_quotes` + # Single quotes are easier to type and don't compromise on readability. + prefer_double_quotes: false + + # Conflicts with `omit_local_variable_types` and other rules. + # As per Dart guidelines, we want to avoid unnecessary types to make the code + # more readable. + # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables + always_specify_types: false + + # Incompatible with `prefer_final_locals` + # Having immutable local variables makes larger functions more predictable + # so we will use `prefer_final_locals` instead. + unnecessary_final: false + + # Not quite suitable for Flutter, which may have a `build` method with a single + # return, but that return is still complex enough that a "body" is worth it. + prefer_expression_function_bodies: false + + # Conflicts with the convention used by flutter, which puts `Key key` + # and `@required Widget child` last. + always_put_required_named_parameters_first: false + + # This project doesn't use Flutter-style todos + flutter_style_todos: false + + # There are situations where we voluntarily want to catch everything, + # especially as a library. + avoid_catches_without_on_clauses: false + + # Boring as it sometimes force a line of 81 characters to be split in two. + # As long as we try to respect that 80 characters limit, going slightly + # above is fine. + lines_longer_than_80_chars: false + + # Conflicts with disabling `implicit-dynamic` + avoid_annotating_with_dynamic: false + + # conflicts with `prefer_relative_imports` + always_use_package_imports: false diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index ad655bb9..197cd0e3 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -1,6 +1,9 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.18.1) + +set(ANDROID_STL c++_static) + PROJECT(Sequencer) -CMAKE_MINIMUM_REQUIRED(VERSION 3.18.1) SET(CMAKE_CXX_STANDARD 17) SET(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -26,7 +29,7 @@ TARGET_INCLUDE_DIRECTORIES(flutter_sequencer PRIVATE ../ios/Classes/CallbackManager ../ios/Classes/Scheduler ../ios/Classes/IInstrument - ) +) TARGET_SOURCES(flutter_sequencer PRIVATE ../ios/Classes/CallbackManager/CallbackManager.h ../ios/Classes/CallbackManager/CallbackManager.cpp @@ -45,8 +48,14 @@ TARGET_SOURCES(flutter_sequencer PRIVATE ./src/main/cpp/Utils/Logging.h ./src/main/cpp/Utils/OptionArray.h ./src/main/cpp/Plugin.cpp - ) +) FIND_LIBRARY(android-lib android) -TARGET_LINK_LIBRARIES(flutter_sequencer ${android-lib} oboe sfizz_static) +# Fixed target link libraries with C++ standard library +TARGET_LINK_LIBRARIES(flutter_sequencer + ${android-lib} + oboe + sfizz_static + c++_static +) \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 9941b80a..1c79efa6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,22 +4,23 @@ group 'com.michaeljperri.flutter_sequencer' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '2.2.20' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:8.11.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.eclipse.jgit:org.eclipse.jgit:5.12.0+" + classpath "org.eclipse.jgit:org.eclipse.jgit:7.0.0.202409031743-r" } } rootProject.allprojects { repositories { google() + mavenCentral() } } @@ -27,30 +28,48 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 29 + namespace "com.michaeljperri.flutter_sequencer" + + compileSdk = 36 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + +// ndkVersion = "27.0.12077973" sourceSets { main.java.srcDirs += 'src/main/kotlin' } + defaultConfig { - minSdkVersion 16 + minSdk 24 externalNativeBuild { cmake { - cppFlags '-std=c++17', '-frelaxed-template-template-args' + arguments "-DCMAKE_CXX_STANDARD=17", "-DCMAKE_CXX_STANDARD_REQUIRED=ON" + cppFlags '-frelaxed-template-template-args' } } + + ndk { + abiFilters "arm64-v8a" + } } lintOptions { disable 'InvalidPackage' } - ndkVersion "22.1.7171670" + externalNativeBuild { cmake { - version "3.18.1" path "CMakeLists.txt" } } + } void cloneThirdPartyRepo(String name, String uri, String commit) { @@ -60,12 +79,12 @@ void cloneThirdPartyRepo(String name, String uri, String commit) { if (!dir.exists()) { dir.mkdirs() def repo = - Git.cloneRepository() - .setDirectory(dir) - .setURI(uri) - .setRemote('origin') - .setCloneSubmodules(true) - .call() + Git.cloneRepository() + .setDirectory(dir) + .setURI(uri) + .setRemote('origin') + .setCloneSubmodules(true) + .call() repo.checkout().setName(commit).call() repo.submoduleUpdate() @@ -74,13 +93,9 @@ void cloneThirdPartyRepo(String name, String uri, String commit) { } task cloneThirdPartyRepos { - cloneThirdPartyRepo('oboe', 'https://github.com/google/oboe', '06ec23e4f6bc00ba7eea9b84e299f9200a598838') - cloneThirdPartyRepo('TinySoundFont', 'https://github.com/schellingb/TinySoundFont.git', 'bf574519e601202c3a9d27a74f345921277eed39') - cloneThirdPartyRepo('sfizz', 'https://github.com/sfztools/sfizz.git', 'fc1f0451cebd8996992cbc4f983fcf76b03295c5') + cloneThirdPartyRepo('oboe', 'https://github.com/google/oboe', 'e40043061cc94df965fda802938b0989f201e86c') + cloneThirdPartyRepo('TinySoundFont', 'https://github.com/schellingb/TinySoundFont.git', '790a219810cb0fca5defa8cdbd88e2487e5efc7a') + cloneThirdPartyRepo('sfizz', 'https://github.com/PROGrand/sfizz.git', '040be90a34cfa14493a3b51887161d5393616638') } preBuild.dependsOn cloneThirdPartyRepos - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index abd7239b..00000000 --- a/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true -android.enablePrefab=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef..00000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3c9d0852..00000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index 9d82f789..00000000 --- a/android/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index aec99730..00000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 91156f8a..a0a94fb7 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + package="com.michaeljperri.flutter_sequencer" +> diff --git a/android/src/main/kotlin/com/michaeljperri/flutter_sequencer/FlutterSequencerPlugin.kt b/android/src/main/kotlin/com/michaeljperri/flutter_sequencer/FlutterSequencerPlugin.kt index 8661e275..74b84d77 100644 --- a/android/src/main/kotlin/com/michaeljperri/flutter_sequencer/FlutterSequencerPlugin.kt +++ b/android/src/main/kotlin/com/michaeljperri/flutter_sequencer/FlutterSequencerPlugin.kt @@ -1,130 +1,111 @@ package com.michaeljperri.flutter_sequencer import android.content.Context -import android.content.res.AssetManager; -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat - +import android.content.res.AssetManager import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar import java.io.File -import java.io.FileInputStream import java.io.FileOutputStream import java.net.URLDecoder const val flutterAssetRoot = "flutter_assets" /** FlutterSequencerPlugin */ -public class FlutterSequencerPlugin: FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel : MethodChannel - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_sequencer") - channel.setMethodCallHandler(this); - context = flutterPluginBinding.applicationContext - } - - // This static function is optional and equivalent to onAttachedToEngine. It supports the old - // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting - // plugin registration via this function while apps migrate to use the new Android APIs - // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. - // - // It is encouraged to share logic between onAttachedToEngine and registerWith to keep - // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called - // depending on the user's project. onAttachedToEngine or registerWith must both be defined - // in the same class. - companion object { - private lateinit var context : Context - - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), "flutter_sequencer") - channel.setMethodCallHandler(FlutterSequencerPlugin()) - context = registrar.context() +class FlutterSequencerPlugin : FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + private lateinit var context: Context + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_sequencer") + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext } - init { - System.loadLibrary("flutter_sequencer") + companion object { + init { + System.loadLibrary("flutter_sequencer") + } } - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else if (call.method == "setupAssetManager") { - setupAssetManager(context.assets) - result.success(null) - } else if (call.method == "normalizeAssetDir") { - val assetDir = call.argument("assetDir")!! - val filesDir = context.filesDir - val isSuccess = copyAssetDirOrFile(assetDir, filesDir) - - if (isSuccess) { - val copiedDir = filesDir.resolve(assetDir).absolutePath - result.success(copiedDir) - } else { - result.success(null) - } - } else if (call.method == "listAudioUnits") { - result.success(emptyList()) - } else { - result.notImplemented() + + private var isLibraryLoaded = false + + override fun onMethodCall(call: MethodCall, result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else if (call.method == "setupAssetManager") { + setupAssetManager(context.assets) + result.success(null) + } else if (call.method == "normalizeAssetDir") { + val assetDir = call.argument("assetDir")!! + val filesDir = context.filesDir + val isSuccess = copyAssetDirOrFile(assetDir, filesDir) + + if (isSuccess) { + val copiedDir = filesDir.resolve(assetDir).absolutePath + result.success(copiedDir) + } else { + result.success(null) + } + } else if (call.method == "listAudioUnits") { + result.success(emptyList()) + } else { + result.notImplemented() + } } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - private fun copyAssetFile(assetFilePath: String, outputDir: File): Boolean { - val inputStream = context.assets.open("$flutterAssetRoot/$assetFilePath") - val decodedAssetFilePath = URLDecoder.decode(assetFilePath, "UTF-8") - val outputFile = outputDir.resolve(decodedAssetFilePath) - var outputStream: FileOutputStream? = null - - try { - outputFile.parentFile.mkdirs() - outputFile.createNewFile() - - outputStream = FileOutputStream(outputFile) - inputStream.copyTo(outputStream, 1024) - } catch (e: SecurityException) { - return false; - } catch (e: java.io.IOException) { - return false; - } finally { - inputStream.close() - outputStream?.flush() - outputStream?.close() + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) } - return true; - } - - private fun copyAssetDirOrFile(assetPath: String, outputDir: File): Boolean { - val paths = context.assets.list("$flutterAssetRoot/$assetPath")!! - var isSuccess = true; - - if (paths.isEmpty()) { - // It's a file. - isSuccess = isSuccess && copyAssetFile(assetPath, outputDir) - } else { - // It's a directory. - paths.forEach { - isSuccess = isSuccess && copyAssetDirOrFile("$assetPath/$it", outputDir) - } + private fun copyAssetFile(assetFilePath: String, outputDir: File): Boolean { + val inputStream = context.assets.open("$flutterAssetRoot/$assetFilePath") + val decodedAssetFilePath = URLDecoder.decode(assetFilePath, "UTF-8") + val outputFile = outputDir.resolve(decodedAssetFilePath) + var outputStream: FileOutputStream? = null + + try { + outputFile.parentFile!!.mkdirs() + outputFile.createNewFile() + + outputStream = FileOutputStream(outputFile) + inputStream.copyTo(outputStream, 1024) + } catch (e: SecurityException) { + return false + } catch (e: java.io.IOException) { + return false + } finally { + inputStream.close() + outputStream?.flush() + outputStream?.close() + } + + return true } - return isSuccess - } + private fun copyAssetDirOrFile(assetPath: String, outputDir: File): Boolean { + val paths = context.assets.list("$flutterAssetRoot/$assetPath")!! + var isSuccess = true + + if (paths.isEmpty()) { + // It's a file. + isSuccess = isSuccess && copyAssetFile(assetPath, outputDir) + } else { + // It's a directory. + paths.forEach { + isSuccess = isSuccess && copyAssetDirOrFile("$assetPath/$it", outputDir) + } + } + + return isSuccess + } - private external fun setupAssetManager(assetManager: AssetManager) + private external fun setupAssetManager(assetManager: AssetManager) } diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index e69de29b..9e3db072 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -0,0 +1,2 @@ +include: + - ../analysis_app.yaml diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index c98e3289..00000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,63 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 29 - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.michaeljperri.flutter_sequencer_example" - minSdkVersion 16 - targetSdkVersion 29 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 00000000..0bf491bb --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("com.android.application") + id("kotlin-android") + id("dev.flutter.flutter-gradle-plugin") +} +android { + namespace = "com.michaeljperri.flutter_sequencer_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + + defaultConfig { + applicationId = "com.michaeljperri.flutter_sequencer_example" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + + ndk { + abiFilters.add("arm64-v8a") + } + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index ed4504e8..b8d0e479 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + - - + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> @@ -44,7 +35,13 @@ + android:name="flutterEmbedding" + android:value="2"/> + + + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle deleted file mode 100644 index f797cfb7..00000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - ext.kotlin_version = '1.5.21' - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 00000000..89176ef4 --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 94adc3a3..fbee1d8c 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,2 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 596278c4..9470727f 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jul 24 13:54:59 EDT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip \ No newline at end of file diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index d3b6a401..00000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -include ':app' - -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 00000000..da20e6da --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle deleted file mode 100644 index e7b4def4..00000000 --- a/example/android/settings_aar.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app' diff --git a/example/lib/components/drum_machine/drum_machine.dart b/example/lib/components/drum_machine/drum_machine.dart index dacc72b6..0f9aa184 100644 --- a/example/lib/components/drum_machine/drum_machine.dart +++ b/example/lib/components/drum_machine/drum_machine.dart @@ -1,15 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_sequencer/track.dart'; - +import 'package:flutter_sequencer_example/components/drum_machine/grid/grid.dart'; +import 'package:flutter_sequencer_example/components/drum_machine/volume_slider.dart'; import 'package:flutter_sequencer_example/models/step_sequencer_state.dart'; -import 'volume_slider.dart'; -import 'grid/grid.dart'; - class DrumMachineWidget extends StatefulWidget { const DrumMachineWidget({ - Key? key, required this.track, required this.stepCount, required this.currentStep, @@ -19,7 +16,8 @@ class DrumMachineWidget extends StatefulWidget { required this.stepSequencerState, required this.handleVolumeChange, required this.handleVelocitiesChange, - }) : super(key: key); + super.key, + }); final Track track; final int stepCount; @@ -32,26 +30,18 @@ class DrumMachineWidget extends StatefulWidget { final Function(int, int, int, double) handleVelocitiesChange; @override - _DrumMachineWidgetState createState() => _DrumMachineWidgetState(); + State createState() => _DrumMachineWidgetState(); } -class _DrumMachineWidgetState extends State - with SingleTickerProviderStateMixin { +class _DrumMachineWidgetState extends State with SingleTickerProviderStateMixin { Ticker? ticker; - @override - void dispose() { - super.dispose(); - } - double? getVelocity(int step, int col) { - return widget.stepSequencerState! - .getVelocity(step, widget.columnPitches[col]); + return widget.stepSequencerState!.getVelocity(step, widget.columnPitches[col]); } void handleVelocityChange(int col, int step, double velocity) { - widget.handleVelocitiesChange( - widget.track.id, step, widget.columnPitches[col], velocity); + widget.handleVelocitiesChange(widget.track.id, step, widget.columnPitches[col], velocity); } void handleVolumeChange(double nextVolume) { @@ -59,8 +49,7 @@ class _DrumMachineWidgetState extends State } void handleNoteOn(int col) { - widget.track - .startNoteNow(noteNumber: widget.columnPitches[col], velocity: .75); + widget.track.startNoteNow(noteNumber: widget.columnPitches[col], velocity: .75); } void handleNoteOff(int col) { @@ -70,22 +59,26 @@ class _DrumMachineWidgetState extends State @override Widget build(BuildContext context) { return Expanded( - child: Container( - padding: EdgeInsets.fromLTRB(32, 16, 32, 0), - decoration: BoxDecoration( - color: Colors.black54, + child: Container( + padding: const EdgeInsets.fromLTRB(32, 16, 32, 0), + decoration: const BoxDecoration(color: Colors.black54), + child: Column( + children: [ + VolumeSlider(value: widget.volume, onChange: handleVolumeChange), + Expanded( + child: Grid( + columnLabels: widget.rowLabels, + getVelocity: getVelocity, + stepCount: widget.stepCount, + currentStep: widget.currentStep, + onChange: handleVelocityChange, + onNoteOn: handleNoteOn, + onNoteOff: handleNoteOff, + ), ), - child: Column(children: [ - VolumeSlider(value: widget.volume, onChange: handleVolumeChange), - Expanded( - child: Grid( - columnLabels: widget.rowLabels, - getVelocity: getVelocity, - stepCount: widget.stepCount, - currentStep: widget.currentStep, - onChange: handleVelocityChange, - onNoteOn: handleNoteOn, - onNoteOff: handleNoteOff)), - ]))); + ], + ), + ), + ); } } diff --git a/example/lib/components/drum_machine/grid/cell.dart b/example/lib/components/drum_machine/grid/cell.dart index 94acfcf5..8abbe553 100644 --- a/example/lib/components/drum_machine/grid/cell.dart +++ b/example/lib/components/drum_machine/grid/cell.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_sequencer_example/constants.dart'; class Cell extends StatelessWidget { - Cell({ - Key? key, + const Cell({ required this.size, required this.velocity, required this.isCurrentStep, required this.onChange, - }) : super(key: key); + super.key, + }); final double size; final double velocity; @@ -22,27 +21,32 @@ class Cell extends StatelessWidget { width: size, height: size, decoration: BoxDecoration( - color: Color.lerp(isCurrentStep ? Colors.white30 : Colors.black, - isCurrentStep ? Colors.blue : Colors.pink, velocity), + color: Color.lerp( + isCurrentStep ? Colors.white30 : Colors.black, + isCurrentStep ? Colors.blue : Colors.pink, + velocity, + ), border: Border.all(color: Colors.white70), ), child: Transform( - transform: - Matrix4.translationValues(0, (-1 * size * velocity) + 2, 0), - child: Container( - width: size, - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.white))))), + transform: Matrix4.translationValues(0, (-1 * size * velocity) + 2, 0), + child: Container( + width: size, + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.white)), + ), + ), + ), ); return GestureDetector( onTap: () { - final nextVelocity = velocity == 0.0 ? DEFAULT_VELOCITY : 0.0; + final nextVelocity = velocity == 0.0 ? kDefaultVelocity : 0.0; onChange(nextVelocity); }, onVerticalDragUpdate: (details) { - final renderBox = context.findRenderObject() as RenderBox; + final renderBox = context.findRenderObject()! as RenderBox; final yPos = renderBox.globalToLocal(details.globalPosition).dy; final nextVelocity = 1.0 - (yPos / size).clamp(0.0, 1.0); diff --git a/example/lib/components/drum_machine/grid/grid.dart b/example/lib/components/drum_machine/grid/grid.dart index 51bd4409..9f3d0522 100644 --- a/example/lib/components/drum_machine/grid/grid.dart +++ b/example/lib/components/drum_machine/grid/grid.dart @@ -2,12 +2,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; - -import 'cell.dart'; +import 'package:flutter_sequencer_example/components/drum_machine/grid/cell.dart'; class Grid extends StatelessWidget { - Grid({ - Key? key, + const Grid({ required this.getVelocity, required this.columnLabels, required this.stepCount, @@ -15,7 +13,8 @@ class Grid extends StatelessWidget { required this.onChange, required this.onNoteOn, required this.onNoteOff, - }) : super(key: key); + super.key, + }); final Function(int step, int col) getVelocity; final List columnLabels; @@ -33,26 +32,28 @@ class Grid extends StatelessWidget { final cellSize = min(constraints.maxWidth / columnLabels.length, 50.0); return ListView.builder( - padding: EdgeInsets.fromLTRB(0, 0, 0, 16), - shrinkWrap: true, - itemCount: stepCount, - itemBuilder: (BuildContext context, int step) { - final List cellWidgets = []; - - for (var col = 0; col < columnsCount; col++) { - final velocity = getVelocity(step, col); - - final cellWidget = Cell( - size: cellSize, - velocity: velocity, - isCurrentStep: step == currentStep, - onChange: (velocity) => onChange(col, step, velocity), - ); - - cellWidgets.add(cellWidget); - } - return Row(children: cellWidgets); - }); + // ignore: prefer_const_constructors + padding: EdgeInsets.fromLTRB(0, 0, 0, 16), + shrinkWrap: true, + itemCount: stepCount, + itemBuilder: (BuildContext context, int step) { + final List cellWidgets = []; + + for (var col = 0; col < columnsCount; col++) { + final velocity = getVelocity(step, col); + + final cellWidget = Cell( + size: cellSize, + velocity: velocity, + isCurrentStep: step == currentStep, + onChange: (velocity) => onChange(col, step, velocity), + ); + + cellWidgets.add(cellWidget); + } + return Row(children: cellWidgets); + }, + ); }, ); } diff --git a/example/lib/components/drum_machine/grid/label_row.dart b/example/lib/components/drum_machine/grid/label_row.dart index 8740b254..cb0e2070 100644 --- a/example/lib/components/drum_machine/grid/label_row.dart +++ b/example/lib/components/drum_machine/grid/label_row.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class LabelRow extends StatelessWidget { - LabelRow({ - Key? key, + const LabelRow({ required this.columnLabels, required this.cellSize, required this.onNoteOn, required this.onNoteOff, - }) : super(key: key); + super.key, + }); final List columnLabels; final Function(int) onNoteOn; diff --git a/example/lib/components/drum_machine/volume_slider.dart b/example/lib/components/drum_machine/volume_slider.dart index ecaccd60..35dfd1dd 100644 --- a/example/lib/components/drum_machine/volume_slider.dart +++ b/example/lib/components/drum_machine/volume_slider.dart @@ -1,26 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class VolumeSlider extends StatelessWidget { - VolumeSlider({ - Key? key, - required this.value, - required this.onChange, - }); + const VolumeSlider({required this.value, required this.onChange, super.key}); final double value; final Function(double) onChange; @override Widget build(BuildContext context) { - return Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Volume:'), - Slider( - min: 0, - max: 1, - value: value, - onChanged: onChange, - ), - ]); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Volume:'), + Slider(value: value, onChanged: onChange), + ], + ); } } diff --git a/example/lib/components/position_view.dart b/example/lib/components/position_view.dart index 24bbc1d9..bb5d4ad3 100644 --- a/example/lib/components/position_view.dart +++ b/example/lib/components/position_view.dart @@ -1,13 +1,14 @@ import 'package:flutter/widgets.dart'; class PositionView extends StatelessWidget { - PositionView({required this.position}); + const PositionView({required this.position, super.key}); final double position; @override Widget build(BuildContext context) { return Container( + // ignore: prefer_const_constructors margin: EdgeInsets.fromLTRB(0, 0, 0, 8.0), child: Text('Position: ${position.toStringAsFixed(3)}'), ); diff --git a/example/lib/components/step_count_selector.dart b/example/lib/components/step_count_selector.dart index 28327fbb..3b0f41c2 100644 --- a/example/lib/components/step_count_selector.dart +++ b/example/lib/components/step_count_selector.dart @@ -1,21 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class StepCountSelector extends StatelessWidget { - const StepCountSelector({ - Key? key, - required this.stepCount, - required this.onChange, - }) : super(key: key); + const StepCountSelector({required this.stepCount, required this.onChange, super.key}); final int stepCount; final Function(int) onChange; - handleLess() { + void handleLess() { onChange(stepCount - 1); } - handleMore() { + void handleMore() { onChange(stepCount + 1); } @@ -24,17 +19,10 @@ class StepCountSelector extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Steps'), - IconButton( - icon: Icon(Icons.arrow_back), - onPressed: handleLess, - ), - Text(stepCount.toString(), - style: TextStyle(fontWeight: FontWeight.bold)), - IconButton( - icon: Icon(Icons.arrow_forward), - onPressed: handleMore, - ), + const Text('Steps'), + IconButton(icon: const Icon(Icons.arrow_back), onPressed: handleLess), + Text(stepCount.toString(), style: const TextStyle(fontWeight: FontWeight.bold)), + IconButton(icon: const Icon(Icons.arrow_forward), onPressed: handleMore), ], ); } diff --git a/example/lib/components/tempo_selector.dart b/example/lib/components/tempo_selector.dart index d3b6b81f..0b8dd2ab 100644 --- a/example/lib/components/tempo_selector.dart +++ b/example/lib/components/tempo_selector.dart @@ -1,18 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; class TempoSelector extends StatefulWidget { - TempoSelector({ - required this.selectedTempo, - required this.handleChange, - }); + TempoSelector({required this.selectedTempo, required this.handleChange, super.key}) { + // TODO: implement TempoSelector + // throw UnimplementedError(); + } final double selectedTempo; final Function(double nextTempo) handleChange; @override - _TempoSelectorState createState() => _TempoSelectorState(); + State createState() => _TempoSelectorState(); } class _TempoSelectorState extends State { @@ -30,11 +29,10 @@ class _TempoSelectorState extends State { @override void initState() { super.initState(); - controller = - TextEditingController(text: widget.selectedTempo.toInt().toString()); + controller = TextEditingController(text: widget.selectedTempo.toInt().toString()); } - handleTextChange(String input) { + void handleTextChange(String input) { final parsedValue = double.tryParse(input); if (parsedValue != null && parsedValue > 0) { @@ -50,23 +48,22 @@ class _TempoSelectorState extends State { @override Widget build(BuildContext context) { - return Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - child: Text('Tempo:'), - margin: EdgeInsets.only(right: 16.0), - ), - Container( - width: 50, - height: 50, - child: TextField( - controller: controller, - maxLines: 1, - keyboardType: TextInputType.number, - onSubmitted: handleTextChange, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: InputDecoration(hintText: "..."), + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container(margin: const EdgeInsets.only(right: 16.0), child: const Text('Tempo:')), + SizedBox( + width: 50, + height: 50, + child: TextField( + controller: controller, + keyboardType: TextInputType.number, + onSubmitted: handleTextChange, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration(hintText: '...'), + ), ), - ), - ]); + ], + ); } } diff --git a/example/lib/components/track_selector.dart b/example/lib/components/track_selector.dart index acb37685..b53f5a3e 100644 --- a/example/lib/components/track_selector.dart +++ b/example/lib/components/track_selector.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_sequencer/track.dart'; class TrackSelector extends StatelessWidget { - TrackSelector({ + const TrackSelector({ required this.selectedTrack, required this.tracks, required this.handleChange, + super.key, }); final Track? selectedTrack; @@ -16,11 +16,11 @@ class TrackSelector extends StatelessWidget { @override Widget build(BuildContext context) { return DropdownButton( - value: selectedTrack, - onChanged: handleChange, - items: tracks.map((track) { - return DropdownMenuItem( - value: track, child: Text(track.instrument.displayName)); - }).toList()); + value: selectedTrack, + onChanged: handleChange, + items: tracks.map((track) { + return DropdownMenuItem(value: track, child: Text(track.instrument.displayName)); + }).toList(), + ); } } diff --git a/example/lib/components/transport.dart b/example/lib/components/transport.dart index 80038271..b09ec61c 100644 --- a/example/lib/components/transport.dart +++ b/example/lib/components/transport.dart @@ -1,15 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class Transport extends StatelessWidget { const Transport({ - Key? key, - required this.isPlaying, - required this.isLooping, - required this.onTogglePlayPause, - required this.onStop, - required this.onToggleLoop, - }) : super(key: key); + required this.isPlaying, required this.isLooping, required this.onTogglePlayPause, required this.onStop, required this.onToggleLoop, super.key, + }); final bool isPlaying; final bool isLooping; @@ -23,16 +17,13 @@ class Transport extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( - onPressed: onTogglePlayPause, - color: Colors.pink, - icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow)), - IconButton( - icon: Icon(Icons.stop), - onPressed: onStop, + onPressed: onTogglePlayPause, color: Colors.pink, + icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), ), + IconButton(icon: const Icon(Icons.stop), onPressed: onStop, color: Colors.pink), IconButton( - icon: Icon(Icons.repeat), + icon: const Icon(Icons.repeat), onPressed: onToggleLoop, color: isLooping ? Colors.pink : Colors.black54, ), diff --git a/example/lib/constants.dart b/example/lib/constants.dart index 68a6117a..1814ba05 100644 --- a/example/lib/constants.dart +++ b/example/lib/constants.dart @@ -1,10 +1,10 @@ -const INITIAL_STEP_COUNT = 8; -const INITIAL_TEMPO = 240.0; -const INITIAL_IS_LOOPING = false; -const DEFAULT_VELOCITY = 0.75; +const kInitialStepCount = 8; +const kInitialTempo = 240.0; +const kInitialIsLooping = false; +const kDefaultVelocity = 0.75; -const ROW_LABELS_DRUMS = ['HH', 'S', 'K', 'CB']; -const ROW_PITCHES_DRUMS = [44, 38, 36, 56]; +const kRowLabelsDrums = ['HH', 'S', 'K', 'CB']; +const kRowPitchesDrums = [44, 38, 36, 56]; -const ROW_LABELS_PIANO = ['B', 'C3', 'D', 'E', 'F', 'G', 'A', 'B', 'C']; -const ROW_PITCHES_PIANO = [59, 60, 62, 64, 65, 67, 69, 71, 72]; +const kRowLabelsPiano = ['B', 'C3', 'D', 'E', 'F', 'G', 'A', 'B', 'C']; +const kRowPitchesPiano = [59, 60, 62, 64, 65, 67, 69, 71, 72]; diff --git a/example/lib/main.dart b/example/lib/main.dart index f43c6f48..08c39d32 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,112 +1,122 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_sequencer/global_state.dart'; -import 'package:flutter_sequencer/models/sfz.dart'; import 'package:flutter_sequencer/models/instrument.dart'; +import 'package:flutter_sequencer/models/sfz.dart'; import 'package:flutter_sequencer/sequence.dart'; import 'package:flutter_sequencer/track.dart'; - -import 'components/drum_machine/drum_machine.dart'; -import 'components/position_view.dart'; -import 'components/step_count_selector.dart'; -import 'components/tempo_selector.dart'; -import 'components/track_selector.dart'; -import 'components/transport.dart'; -import 'models/project_state.dart'; -import 'models/step_sequencer_state.dart'; -import 'constants.dart'; +import 'package:flutter_sequencer_example/components/drum_machine/drum_machine.dart'; +import 'package:flutter_sequencer_example/components/position_view.dart'; +import 'package:flutter_sequencer_example/components/step_count_selector.dart'; +import 'package:flutter_sequencer_example/components/tempo_selector.dart'; +import 'package:flutter_sequencer_example/components/track_selector.dart'; +import 'package:flutter_sequencer_example/components/transport.dart'; +import 'package:flutter_sequencer_example/constants.dart'; +import 'package:flutter_sequencer_example/models/project_state.dart'; +import 'package:flutter_sequencer_example/models/step_sequencer_state.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatefulWidget { + const MyApp({super.key}); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State with SingleTickerProviderStateMixin { - final sequence = - Sequence(tempo: INITIAL_TEMPO, endBeat: INITIAL_STEP_COUNT.toDouble()); + final sequence = Sequence(tempo: kInitialTempo, endBeat: kInitialStepCount.toDouble()); Map trackStepSequencerStates = {}; List tracks = []; Map trackVolumes = {}; Track? selectedTrack; late Ticker ticker; - double tempo = INITIAL_TEMPO; - int stepCount = INITIAL_STEP_COUNT; + double tempo = kInitialTempo; + int stepCount = kInitialStepCount; double position = 0.0; bool isPlaying = false; - bool isLooping = INITIAL_IS_LOOPING; + bool isLooping = kInitialIsLooping; @override void initState() { super.initState(); - GlobalState().setKeepEngineRunning(true); + GlobalState().keepEngineRunning = true; final instruments = [ - Sf2Instrument(path: "assets/sf2/TR-808.sf2", isAsset: true), - SfzInstrument( - path: "assets/sfz/GMPiano.sfz", + const Sf2Instrument(path: 'assets/sf2/TR-808.sf2', isAsset: true), + const SfzInstrument( + path: 'assets/sfz/GMPiano.sfz', isAsset: true, - tuningPath: "assets/sfz/meanquar.scl", + tuningPath: 'assets/sfz/meanquar.scl', ), RuntimeSfzInstrument( - id: "Sampled Synth", - sampleRoot: "assets/wav", - isAsset: true, - sfz: Sfz(groups: [ - SfzGroup(regions: [ - SfzRegion(sample: "D3.wav", key: 62), - SfzRegion(sample: "F3.wav", key: 65), - SfzRegion(sample: "Gsharp3.wav", key: 68), - ]) - ])), + id: 'Sampled Synth', + sampleRoot: 'assets/wav', + isAsset: true, + sfz: Sfz( + groups: [ + SfzGroup( + regions: [ + SfzRegion(sample: 'D3.wav', key: 62), + SfzRegion(sample: 'F3.wav', key: 65), + SfzRegion(sample: 'Gsharp3.wav', key: 68), + ], + ), + ], + ), + ), RuntimeSfzInstrument( - id: "Generated Synth", - // This SFZ doesn't use any sample files, so just put "/" as a placeholder. - sampleRoot: "/", - isAsset: false, - // Based on the Unison Oscillator example here: - // https://sfz.tools/sfizz/quick_reference#unison-oscillator - sfz: Sfz(groups: [ - SfzGroup(regions: [ - SfzRegion(sample: "*saw", otherOpcodes: { - "oscillator_multi": "5", - "oscillator_detune": "50", - }) - ]) - ])), + id: 'Generated Synth', + // This SFZ doesn't use any sample files, so just put "/" as a placeholder. + sampleRoot: '/', + isAsset: false, + // Based on the Unison Oscillator example here: + // https://sfz.tools/sfizz/quick_reference#unison-oscillator + sfz: Sfz( + groups: [ + SfzGroup( + regions: [ + SfzRegion( + sample: '*saw', + otherOpcodes: {'oscillator_multi': '5', 'oscillator_detune': '50'}, + ), + ], + ), + ], + ), + ), ]; sequence.createTracks(instruments).then((tracks) { this.tracks = tracks; - tracks.forEach((track) { + for (final track in tracks) { trackVolumes[track.id] = 0.0; trackStepSequencerStates[track.id] = StepSequencerState(); - }); + } setState(() { - this.selectedTrack = tracks[0]; + selectedTrack = tracks[0]; }); }); - ticker = this.createTicker((Duration elapsed) { + ticker = createTicker((Duration elapsed) { setState(() { tempo = sequence.getTempo(); position = sequence.getBeat(); isPlaying = sequence.getIsPlaying(); - tracks.forEach((track) { + for (final track in tracks) { trackVolumes[track.id] = track.getVolume(); - }); + } }); }); ticker.start(); } - handleTogglePlayPause() { + void handleTogglePlayPause() { if (isPlaying) { sequence.pause(); } else { @@ -114,11 +124,11 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { } } - handleStop() { + void handleStop() { sequence.stop(); } - handleSetLoop(bool nextIsLooping) { + void handleSetLoop({required bool nextIsLooping}) { if (nextIsLooping) { sequence.setLoop(0, stepCount.toDouble()); } else { @@ -130,16 +140,16 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { }); } - handleToggleLoop() { + void handleToggleLoop() { final nextIsLooping = !isLooping; - handleSetLoop(nextIsLooping); + handleSetLoop(nextIsLooping: nextIsLooping); } - handleStepCountChange(int nextStepCount) { + void handleStepCountChange(int nextStepCount) { if (nextStepCount < 1) return; - sequence.setEndBeat(nextStepCount.toDouble()); + sequence.endBeat = nextStepCount.toDouble(); if (isLooping) { final nextLoopEndBeat = nextStepCount.toDouble(); @@ -149,29 +159,30 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { setState(() { stepCount = nextStepCount; - tracks.forEach((track) => syncTrack(track)); + for (final track in tracks) { + syncTrack(track); + } }); } - handleTempoChange(double nextTempo) { + void handleTempoChange(double nextTempo) { if (nextTempo <= 0) return; sequence.setTempo(nextTempo); } - handleTrackChange(Track? nextTrack) { + void handleTrackChange(Track? nextTrack) { setState(() { selectedTrack = nextTrack; }); } - handleVolumeChange(double nextVolume) { + void handleVolumeChange(double nextVolume) { if (selectedTrack != null) { selectedTrack!.changeVolumeNow(volume: nextVolume); } } - handleVelocitiesChange( - int trackId, int step, int noteNumber, double velocity) { + void handleVelocitiesChange(int trackId, int step, int noteNumber, double velocity) { final track = tracks.firstWhere((track) => track.id == trackId); trackStepSequencerStates[trackId]!.setVelocity(step, noteNumber, velocity); @@ -179,22 +190,22 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { syncTrack(track); } - syncTrack(track) { + void syncTrack(Track track) { track.clearEvents(); - trackStepSequencerStates[track.id]! - .iterateEvents((step, noteNumber, velocity) { + trackStepSequencerStates[track.id]!.iterateEvents((step, noteNumber, velocity) { if (step < stepCount) { track.addNote( - noteNumber: noteNumber, - velocity: velocity, - startBeat: step.toDouble(), - durationBeats: 1.0); + noteNumber: noteNumber, + velocity: velocity, + startBeat: step.toDouble(), + durationBeats: 1.0, + ); } }); track.syncBuffer(); } - loadProjectState(ProjectState projectState) { + void loadProjectState(ProjectState projectState) { handleStop(); trackStepSequencerStates[tracks[0].id] = projectState.drumState; @@ -204,75 +215,72 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { handleStepCountChange(projectState.stepCount); handleTempoChange(projectState.tempo); - handleSetLoop(projectState.isLooping); + handleSetLoop(nextIsLooping: projectState.isLooping); tracks.forEach(syncTrack); } - handleReset() { + void handleReset() { loadProjectState(ProjectState.empty()); } - handleLoadDemo() { + void handleLoadDemo() { loadProjectState(ProjectState.demo()); } Widget _getMainView() { - if (selectedTrack == null) return Text('Loading...'); + if (selectedTrack == null) return const Text('Loading...'); final isDrumTrackSelected = selectedTrack == tracks[0]; return Center( - child: Column(children: [ - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Transport( - isPlaying: isPlaying, - isLooping: isLooping, - onTogglePlayPause: handleTogglePlayPause, - onStop: handleStop, - onToggleLoop: handleToggleLoop, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Transport( + isPlaying: isPlaying, + isLooping: isLooping, + onTogglePlayPause: handleTogglePlayPause, + onStop: handleStop, + onToggleLoop: handleToggleLoop, + ), + PositionView(position: position), + ], ), - PositionView(position: position), - ]), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - StepCountSelector( - stepCount: stepCount, onChange: handleStepCountChange), - TempoSelector( - selectedTempo: tempo, - handleChange: handleTempoChange, - ), - ], - ), - TrackSelector( - tracks: tracks, - selectedTrack: selectedTrack, - handleChange: handleTrackChange, - ), - Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - MaterialButton( - child: Text('Reset'), - onPressed: handleReset, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + StepCountSelector(stepCount: stepCount, onChange: handleStepCountChange), + TempoSelector(selectedTempo: tempo, handleChange: handleTempoChange), + ], ), - MaterialButton( - child: Text('Load Demo'), - onPressed: handleLoadDemo, + TrackSelector( + tracks: tracks, + selectedTrack: selectedTrack, + handleChange: handleTrackChange, ), - ]), - DrumMachineWidget( - track: selectedTrack!, - stepCount: stepCount, - currentStep: position.floor(), - rowLabels: isDrumTrackSelected ? ROW_LABELS_DRUMS : ROW_LABELS_PIANO, - columnPitches: - isDrumTrackSelected ? ROW_PITCHES_DRUMS : ROW_PITCHES_PIANO, - volume: trackVolumes[selectedTrack!.id] ?? 0.0, - stepSequencerState: trackStepSequencerStates[selectedTrack!.id], - handleVolumeChange: handleVolumeChange, - handleVelocitiesChange: handleVelocitiesChange, - ), - ]), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MaterialButton(onPressed: handleReset, child: const Text('Reset')), + MaterialButton(onPressed: handleLoadDemo, child: const Text('Load Demo')), + ], + ), + DrumMachineWidget( + track: selectedTrack!, + stepCount: stepCount, + currentStep: position.floor(), + rowLabels: isDrumTrackSelected ? kRowLabelsDrums : kRowLabelsPiano, + columnPitches: isDrumTrackSelected ? kRowPitchesDrums : kRowPitchesPiano, + volume: trackVolumes[selectedTrack!.id] ?? 0.0, + stepSequencerState: trackStepSequencerStates[selectedTrack!.id], + handleVolumeChange: handleVolumeChange, + handleVelocitiesChange: handleVelocitiesChange, + ), + ], + ), ); } @@ -280,9 +288,9 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( - colorScheme: ColorScheme.dark(), - textTheme: - Theme.of(context).textTheme.apply(bodyColor: Colors.white)), + colorScheme: const ColorScheme.dark(), + textTheme: Theme.of(context).textTheme.apply(bodyColor: Colors.white), + ), home: Scaffold( appBar: AppBar(title: const Text('Drum machine example')), body: _getMainView(), diff --git a/example/lib/models/project_state.dart b/example/lib/models/project_state.dart index 5431974a..0b78eb6c 100644 --- a/example/lib/models/project_state.dart +++ b/example/lib/models/project_state.dart @@ -1,6 +1,6 @@ import 'package:flutter_sequencer_example/constants.dart'; -import 'step_sequencer_state.dart'; +import 'package:flutter_sequencer_example/models/step_sequencer_state.dart'; class ProjectState { ProjectState({ @@ -13,19 +13,11 @@ class ProjectState { required this.synthState, }); - final int stepCount; - final double tempo; - final bool isLooping; - final StepSequencerState drumState; - final StepSequencerState pianoState; - final StepSequencerState bassState; - final StepSequencerState synthState; - - static ProjectState empty() { + factory ProjectState.empty() { return ProjectState( - stepCount: INITIAL_STEP_COUNT, - tempo: INITIAL_TEMPO, - isLooping: INITIAL_IS_LOOPING, + stepCount: kInitialStepCount, + tempo: kInitialTempo, + isLooping: kInitialIsLooping, drumState: StepSequencerState(), pianoState: StepSequencerState(), bassState: StepSequencerState(), @@ -33,7 +25,7 @@ class ProjectState { ); } - static ProjectState demo() { + factory ProjectState.demo() { final drumState = StepSequencerState(); drumState.setVelocity(0, 44, 0.75); @@ -163,4 +155,12 @@ class ProjectState { synthState: synthState, ); } + + final int stepCount; + final double tempo; + final bool isLooping; + final StepSequencerState drumState; + final StepSequencerState pianoState; + final StepSequencerState bassState; + final StepSequencerState synthState; } diff --git a/example/pubspec.lock b/example/pubspec.lock index e41ecfbc..aeaa16f1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,164 +5,232 @@ packages: dependency: "direct main" description: name: async - url: "https://pub.dartlang.org" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.4.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "1.0.9" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.2.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_sequencer: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.4.3" + version: "0.5.50" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.17.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "2.1.8" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" source: hosted - version: "0.4.3" - typed_data: + version: "0.7.10" + vector_math: dependency: transitive description: - name: typed_data - url: "https://pub.dartlang.org" + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" source: hosted - version: "1.3.0" - vector_math: + version: "2.2.0" + vm_service: dependency: transitive description: - name: vector_math - url: "https://pub.dartlang.org" + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "15.2.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.10.0" + dart: ">=3.11.5 <4.0.0" + flutter: ">=3.41.9" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 81ca7798..91d6f546 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,43 +1,24 @@ name: flutter_sequencer_example description: Demonstrates how to use the flutter_sequencer plugin. - -# The following line prevents the package from being accidentally published to -# pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=3.11.5 <4.0.0' dependencies: + async: + cupertino_icons: flutter: sdk: flutter - async: "^2.4.1" - flutter_sequencer: - # When depending on this package from a real application you should use: - # flutter_sequencer: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. path: ../ - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.3 - dev_dependencies: + flutter_lints: flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true assets: @@ -45,29 +26,3 @@ flutter: - assets/sfz/samples/ - assets/sf2/ - assets/wav/ - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index bb9aa80b..b4754da2 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -5,23 +5,8 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_sequencer_example/main.dart'; - void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); + testWidgets('Verify Platform version', (WidgetTester tester) async {}); } diff --git a/flutter_sequencer.iml b/flutter_sequencer.iml index 9a198d28..0e5878cf 100644 --- a/flutter_sequencer.iml +++ b/flutter_sequencer.iml @@ -3,18 +3,17 @@ - - - + + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 4ffe0da6..ab9109d3 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,15 +1,15 @@ /// Seconds per microsecond -const SECONDS_PER_US = 1 / 1000000; +const kSecondsPerUs = 1 / 1000000; /// The size of the event buffer in the native backend -const BUFFER_SIZE = 1024; +const kBufferSize = 1024; /// Interval to "top off" each track's buffer, in milliseconds -const TOP_OFF_PERIOD_MS = 1000; +const kTopOffPeriodMs = 1000; /// "Lead frames" account for the fact that it may take some time to build the /// events and sync them with the native sequencer engine. -const LEAD_FRAMES = 1024; +const kLeadFrames = 1024; /// The patch number to select from a sf2 file. -const DEFAULT_PATCH_NUMBER = 0; +const kDefaultPatchNumber = 0; diff --git a/lib/flutter_sequencer.dart b/lib/flutter_sequencer.dart new file mode 100644 index 00000000..94ea35e2 --- /dev/null +++ b/lib/flutter_sequencer.dart @@ -0,0 +1,9 @@ +import 'flutter_sequencer_platform_interface.dart'; + +class FlutterSequencer { + Future getPlatformVersion() { + return FlutterSequencerPlatform.instance.getPlatformVersion(); + } + + Future doSetup() => FlutterSequencerPlatform.instance.doSetup(); +} diff --git a/lib/flutter_sequencer_method_channel.dart b/lib/flutter_sequencer_method_channel.dart new file mode 100644 index 00000000..ffdc0d49 --- /dev/null +++ b/lib/flutter_sequencer_method_channel.dart @@ -0,0 +1,29 @@ +import 'dart:ffi'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'flutter_sequencer_platform_interface.dart'; +import 'native_bridge.dart'; +import 'utils/isolate.dart'; + +/// An implementation of [FlutterSequencerPlatform] that uses method channels. +class MethodChannelFlutterSequencer extends FlutterSequencerPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('flutter_sequencer'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } + + @override + Future doSetup() async { + await methodChannel.invokeMethod('setupAssetManager'); + nRegisterPostCObject(NativeApi.postCObject); + + return singleResponseFuture((port) => nSetupEngine(port.nativePort)); + } +} diff --git a/lib/flutter_sequencer_platform_interface.dart b/lib/flutter_sequencer_platform_interface.dart new file mode 100644 index 00000000..8d11375c --- /dev/null +++ b/lib/flutter_sequencer_platform_interface.dart @@ -0,0 +1,33 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'flutter_sequencer_method_channel.dart'; + +abstract class FlutterSequencerPlatform extends PlatformInterface { + /// Constructs a FlutterSequencerPlatform. + FlutterSequencerPlatform() : super(token: _token); + + static final Object _token = Object(); + + static FlutterSequencerPlatform _instance = MethodChannelFlutterSequencer(); + + /// The default instance of [FlutterSequencerPlatform] to use. + /// + /// Defaults to [MethodChannelFlutterSequencer]. + static FlutterSequencerPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterSequencerPlatform] when + /// they register themselves. + static set instance(FlutterSequencerPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + Future doSetup() { + throw UnimplementedError('doSetup() has not been implemented.'); + } +} diff --git a/lib/global_state.dart b/lib/global_state.dart index 63ef8021..a6f784b4 100644 --- a/lib/global_state.dart +++ b/lib/global_state.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'package:flutter/services.dart'; + import 'constants.dart'; +import 'flutter_sequencer.dart'; import 'native_bridge.dart'; import 'sequence.dart'; import 'track.dart'; @@ -9,15 +12,15 @@ import 'track.dart'; /// responsible for setting up, starting, and stopping the engine. It also /// maintains the timer for "topping off" the buffers. class GlobalState { - static final GlobalState _globalState = GlobalState._internal(); + factory GlobalState() { + return _globalState; + } GlobalState._internal() { _setupEngine(); } - factory GlobalState() { - return _globalState; - } + static final GlobalState _globalState = GlobalState._internal(); var keepEngineRunning = false; final sequenceIdMap = {}; @@ -25,11 +28,11 @@ class GlobalState { var isEngineReady = false; Timer? _topOffTimer; int lastTickInBuffer = 0; - final onEngineReadyCallbacks = []; + final onEngineReadyCallbacks = []; /// Calls a function when the sequencer engine is ready. Trying to play the /// sequence won't do anything until the engine is ready. - void onEngineReady(Function() callback) { + void onEngineReady(void Function() callback) { if (isEngineReady) { callback(); } else { @@ -37,17 +40,6 @@ class GlobalState { } } - /// Set this to true in your app's initState to leave the audio engine running - /// even when there is no sequence playing. This may consume more energy. - /// With this setting enabled, you can use Track.startNoteNow etc to play - /// an instrument in real time. - void setKeepEngineRunning(bool nextValue) { - keepEngineRunning = nextValue; - } - - /// {@template flutter_sequencer_library_private} - /// For internal use only. - /// {@endtemplate} /// Registers the sequence with the underlying engine. int registerSequence(Sequence sequence) { var nextId = 0; @@ -76,9 +68,8 @@ class GlobalState { final shouldPlayEngine = !_getIsPlaying(); sequence.isPlaying = true; - sequence.engineStartFrame = LEAD_FRAMES + - NativeBridge.getPosition() - - sequence.beatToFrames(sequence.pauseBeat); + sequence.engineStartFrame = + kLeadFrames + NativeBridge.getPosition() - sequence.beatToFrames(sequence.pauseBeat); _syncAllBuffers(); @@ -110,19 +101,32 @@ class GlobalState { /// {@macro flutter_sequencer_library_private} int usToFrames(int us) { if (sampleRate == null) return 0; - return (us * SECONDS_PER_US * sampleRate!).round(); + return (us * kSecondsPerUs * sampleRate!).round(); } /// {@macro flutter_sequencer_library_private} int framesToUs(int frames) { if (sampleRate == null) return 0; - return (frames / (SECONDS_PER_US * sampleRate!)).round(); + return (frames / (kSecondsPerUs * sampleRate!)).round(); } - void _setupEngine() async { - sampleRate = await NativeBridge.doSetup(); + Future _setupEngine() async { + final _flutterSequencerPlugin = FlutterSequencer(); + String platformVersion; + try { + platformVersion = + await _flutterSequencerPlugin.getPlatformVersion() ?? 'Unknown platform version'; + } catch (_) { + platformVersion = 'Failed to get platform version.'; + } + + print('XXX platform_version:$platformVersion'); + + sampleRate = await _flutterSequencerPlugin.doSetup(); isEngineReady = true; - onEngineReadyCallbacks.forEach((callback) => callback()); + for (final callback in onEngineReadyCallbacks) { + callback(); + } if (keepEngineRunning) { NativeBridge.play(); @@ -138,10 +142,12 @@ class GlobalState { if (!keepEngineRunning) NativeBridge.play(); if (_topOffTimer != null) _topOffTimer!.cancel(); - _topOffTimer = Timer.periodic(Duration(milliseconds: 1000), (_) { + _topOffTimer = Timer.periodic(const Duration(milliseconds: 1000), (_) { _topOffAllBuffers(); - sequenceIdMap.values.forEach((sequence) => sequence.checkIsOver()); + for (final sequence in sequenceIdMap.values) { + sequence.checkIsOver(); + } }); } @@ -156,9 +162,7 @@ class GlobalState { final tracks = []; sequenceIdMap.forEach((_, sequence) { - sequence.getTracks().forEach((track) { - tracks.add(track); - }); + sequence.getTracks().forEach(tracks.add); }); return tracks; @@ -171,8 +175,7 @@ class GlobalState { }); } - void _syncAllBuffers( - [int? absoluteStartFrame, int maxEventsToSync = BUFFER_SIZE]) { + void _syncAllBuffers([int? absoluteStartFrame, int maxEventsToSync = kBufferSize]) { _getAllTracks().forEach((track) { track.syncBuffer(absoluteStartFrame, maxEventsToSync); }); diff --git a/lib/models/events.dart b/lib/models/events.dart index 80905fc3..4955f372 100644 --- a/lib/models/events.dart +++ b/lib/models/events.dart @@ -1,28 +1,25 @@ import 'dart:typed_data'; -const SCHEDULER_EVENT_SIZE = 16; -const SCHEDULER_EVENT_DATA_OFFSET = 8; -const MIDI_STATUS_NOTE_ON = 144; -const MIDI_STATUS_NOTE_OFF = 128; +const kSchedulerEventSize = 16; +const kSchedulerEventDataOffset = 8; +const kMidiStatusNoteOn = 144; +const kMidiStatusNoteOff = 128; /// Remember to keep SchedulerEvent.cpp in sync with this file. /// The base class for Events. All events have a beat, which is used determine /// when they will be handled. abstract class SchedulerEvent { - static const MIDI_EVENT = 0; - static const VOLUME_EVENT = 1; + const SchedulerEvent({required this.beat, required this.type}); - SchedulerEvent({ - required this.beat, - required this.type, - }); + static const midiEvent = 0; + static const volumeEvent = 1; - double beat; + final double beat; final int type; ByteData serializeBytes(int sampleRate, double tempo, int correctionFrames) { - final data = ByteData(SCHEDULER_EVENT_SIZE); + final data = ByteData(kSchedulerEventSize); final us = ((1 / tempo) * beat * 60000000).round(); final frame = ((us * sampleRate) / 1000000).round() + correctionFrames; @@ -35,116 +32,83 @@ abstract class SchedulerEvent { /// Describes an event that will trigger a MIDI event. class MidiEvent extends SchedulerEvent { - MidiEvent({ - required double beat, + const MidiEvent({ + required super.beat, required this.midiStatus, required this.midiData1, required this.midiData2, - }) : super(beat: beat, type: SchedulerEvent.MIDI_EVENT); - - final int midiStatus; - final int midiData1; - final int midiData2; - - @override - ByteData serializeBytes(int sampleRate, double tempo, int correctionFrames) { - final data = super.serializeBytes(sampleRate, tempo, correctionFrames); - - data.setUint8(SCHEDULER_EVENT_DATA_OFFSET, midiStatus); - data.setUint8(SCHEDULER_EVENT_DATA_OFFSET + 1, midiData1); - data.setUint8(SCHEDULER_EVENT_DATA_OFFSET + 2, midiData2); + }) : super(type: SchedulerEvent.midiEvent); - return data; - } - - static MidiEvent ofNoteOn({ + factory MidiEvent.ofNoteOn({ required double beat, required int noteNumber, required int velocity, }) { - if (noteNumber > 127 || noteNumber < 0) - throw 'noteNumber must be in range 0-127'; - if (velocity > 127 || velocity < 0) throw 'Velocity must be in range 0-127'; - - return MidiEvent( - beat: beat, - midiStatus: 144, - midiData1: noteNumber, - midiData2: velocity, - ); + if (noteNumber > 127 || noteNumber < 0) { + throw Exception('noteNumber must be in range 0-127'); + } + if (velocity > 127 || velocity < 0) throw Exception('Velocity must be in range 0-127'); + + return MidiEvent(beat: beat, midiStatus: 144, midiData1: noteNumber, midiData2: velocity); } - static MidiEvent ofNoteOff({ - required double beat, - required int noteNumber, - }) { - if (noteNumber > 127 || noteNumber < 0) - throw 'noteNumber must be in range 0-127'; - - return MidiEvent( - beat: beat, - midiStatus: 128, - midiData1: noteNumber, - midiData2: 0, - ); + factory MidiEvent.ofNoteOff({required double beat, required int noteNumber}) { + if (noteNumber > 127 || noteNumber < 0) { + throw Exception('noteNumber must be in range 0-127'); + } + + return MidiEvent(beat: beat, midiStatus: 128, midiData1: noteNumber, midiData2: 0); } - static MidiEvent cc({ - required double beat, - required int ccNumber, - required int ccValue, - }) { - if (ccNumber > 127 || ccNumber < 0) throw 'ccNumber must be in range 0-127'; - if (ccValue > 127 || ccValue < 0) throw 'ccValue must be in range 0-127'; - - return MidiEvent( - beat: beat, - midiStatus: 0xB0, - midiData1: ccNumber, - midiData2: ccValue, - ); + factory MidiEvent.cc({required double beat, required int ccNumber, required int ccValue}) { + if (ccNumber > 127 || ccNumber < 0) throw Exception('ccNumber must be in range 0-127'); + if (ccValue > 127 || ccValue < 0) throw Exception('ccValue must be in range 0-127'); + + return MidiEvent(beat: beat, midiStatus: 0xB0, midiData1: ccNumber, midiData2: ccValue); } - static MidiEvent pitchBend({ - required double beat, - required double value, - }) { - if (value > 1 || value < -1) throw 'value must be in range -1 to 1'; + factory MidiEvent.pitchBend({required double beat, required double value}) { + if (value > 1 || value < -1) throw Exception('value must be in range -1 to 1'); final intValue = (((value + 1) / 2) * 16383).round(); final midiData1 = intValue >> 7; final midiData2 = intValue & 0x7F; - return MidiEvent( - beat: beat, - midiStatus: 0xE0, - midiData1: midiData1, - midiData2: midiData2, - ); + return MidiEvent(beat: beat, midiStatus: 0xE0, midiData1: midiData1, midiData2: midiData2); + } + + final int midiStatus; + final int midiData1; + final int midiData2; + + @override + ByteData serializeBytes(int sampleRate, double tempo, int correctionFrames) { + final data = super.serializeBytes(sampleRate, tempo, correctionFrames); + + data.setUint8(kSchedulerEventDataOffset, midiStatus); + data.setUint8(kSchedulerEventDataOffset + 1, midiData1); + data.setUint8(kSchedulerEventDataOffset + 2, midiData2); + + return data; } } /// Describes an event that will trigger a volume change. class VolumeEvent extends SchedulerEvent { - VolumeEvent({ - required double beat, - required this.volume, - }) : super(beat: beat, type: SchedulerEvent.VOLUME_EVENT); + VolumeEvent({required super.beat, required this.volume}) + : super(type: SchedulerEvent.volumeEvent); final double? volume; VolumeEvent withFrame(int frame) { - return VolumeEvent( - beat: beat, - volume: volume, - ); + return VolumeEvent(beat: beat, volume: volume); } @override - ByteData serializeBytes(int sampleRate, double beat, int correctionFrames) { - final data = super.serializeBytes(sampleRate, beat, correctionFrames); + ByteData serializeBytes(int sampleRate, double tempo, int correctionFrames) { + final data = super.serializeBytes(sampleRate, tempo, correctionFrames); - data.setFloat32(SCHEDULER_EVENT_DATA_OFFSET, volume!, Endian.host); + data.setFloat32(kSchedulerEventDataOffset, volume!, Endian.host); return data; } diff --git a/lib/models/instrument.dart b/lib/models/instrument.dart index 5a9a6e54..39e08359 100644 --- a/lib/models/instrument.dart +++ b/lib/models/instrument.dart @@ -3,15 +3,14 @@ import 'sfz.dart'; /// The base class for Instruments. abstract class Instrument { + const Instrument(this.idOrPath, {required this.isAsset, this.presetIndex = kDefaultPatchNumber}); + final String idOrPath; final bool isAsset; final int presetIndex; - Instrument(this.idOrPath, this.isAsset, - {this.presetIndex = DEFAULT_PATCH_NUMBER}); - String get displayName { - return idOrPath.split(RegExp('[\\\\/]')).last; + return idOrPath.split(RegExp(r'[\\/]')).last; } } @@ -21,10 +20,9 @@ abstract class Instrument { /// an alternate tuning, set tuningPath to the path to a /// [Scala](http://www.huygens-fokker.org/scala/scl_format.html) tuning file. class SfzInstrument extends Instrument { + const SfzInstrument({required String path, required bool isAsset, this.tuningPath}) + : super(path, isAsset: isAsset); final String? tuningPath; - - SfzInstrument({required String path, required bool isAsset, this.tuningPath}) - : super(path, isAsset); } /// Describes an instrument in SFZ format. The SFZ will be played by @@ -35,33 +33,31 @@ class SfzInstrument extends Instrument { /// Note that if you want to change the SFZ, you'll have to recreate the /// instrument. class RuntimeSfzInstrument extends Instrument { - final String sampleRoot; + const RuntimeSfzInstrument({ + required String id, + required bool isAsset, + required this.sampleRoot, + required this.sfz, + this.tuningString, + }) : super(id, isAsset: isAsset); + final String sampleRoot; final Sfz sfz; final String? tuningString; - - RuntimeSfzInstrument( - {required String id, - required bool isAsset, - required this.sampleRoot, - required this.sfz, - this.tuningString}) - : super(id, isAsset); } /// Describes an instrument in SF2 format. Will be played by the SoundFont /// player for the current platform. class Sf2Instrument extends Instrument { - Sf2Instrument( - {required String path, - required bool isAsset, - int presetIndex = DEFAULT_PATCH_NUMBER}) - : super(path, isAsset, presetIndex: presetIndex); + const Sf2Instrument({ + required String path, + required bool isAsset, + int presetIndex = kDefaultPatchNumber, + }) : super(path, isAsset: isAsset, presetIndex: presetIndex); } /// Describes an AudioUnit instrument (Apple platforms only.) class AudioUnitInstrument extends Instrument { - AudioUnitInstrument( - {required String manufacturerName, required String componentName}) - : super('$manufacturerName.$componentName', false); + const AudioUnitInstrument({required String manufacturerName, required String componentName}) + : super('$manufacturerName.$componentName', isAsset: false); } diff --git a/lib/models/sfz.dart b/lib/models/sfz.dart index b68fc2bb..48dcb283 100644 --- a/lib/models/sfz.dart +++ b/lib/models/sfz.dart @@ -1,12 +1,9 @@ /// Learn more about the SFZ format here: - String opcodeMapToString(Map? opcodeMap) { if (opcodeMap == null) { return ''; } else { - return opcodeMap.entries - .map((entry) => '${entry.key}=${entry.value}\n') - .join(''); + return opcodeMap.entries.map((entry) => '${entry.key}=${entry.value}\n').join(); } } @@ -14,10 +11,10 @@ class SfzRegion { SfzRegion({ this.sample, this.key, - this.lokey, - this.hikey, - this.lovel, - this.hivel, + this.loKey, + this.hiKey, + this.loVel, + this.hiVel, this.loopStart, this.loopEnd, this.otherOpcodes, @@ -25,99 +22,73 @@ class SfzRegion { String? sample; int? key; - int? lokey, hikey; - int? lovel, hivel; - double? loopStart, loopEnd; + int? loKey; + int? hiKey; + int? loVel; + int? hiVel; + double? loopStart; + double? loopEnd; Map? otherOpcodes; - String buildString() { - return '\n' + - (sample != null ? 'sample=$sample\n' : '') + - (key != null ? 'key=$key\n' : '') + - (lokey != null ? 'lokey=$lokey\n' : '') + - (hikey != null ? 'hikey=$hikey\n' : '') + - (lovel != null ? 'lovel=$lovel\n' : '') + - (hivel != null ? 'hivel=$hivel\n' : '') + - (loopStart != null ? 'loop_start=$loopStart\n' : '') + - (loopEnd != null ? 'loop_end=$loopEnd\n' : '') + - (opcodeMapToString(otherOpcodes)); - } + String buildString() => + '\n${sample != null ? 'sample=$sample\n' : ''}${key != null ? 'key=$key\n' : ''}\n${loKey != null ? 'loKey=$loKey\n' : ''}${hiKey != null ? 'hiKey=$hiKey\n' : ''}${loVel != null ? 'loVel=$loVel\n' : ''}${hiVel != null ? 'hiVel=$hiVel\n' : ''}${loopStart != null ? 'loop_start=$loopStart\n' : ''}${loopEnd != null ? 'loop_end=$loopEnd\n' : ''}${opcodeMapToString(otherOpcodes)}'; } class SfzGroup { - SfzGroup({ - this.opcodes, - required this.regions, - }); + const SfzGroup({this.opcodes, required this.regions}); - Map? opcodes; - List regions; + final Map? opcodes; + final List regions; String buildString() { - return '\n' + - (opcodeMapToString(opcodes)) + - regions.map((r) => r.buildString()).join(''); + return '\n${opcodeMapToString(opcodes)}${regions.map((r) => r.buildString()).join()}'; } } class SfzControl { - SfzControl({ - this.opcodes, - }); + const SfzControl({this.opcodes}); - Map? opcodes; + final Map? opcodes; String buildString() { - return '\n' + (opcodeMapToString(opcodes)); + return '\n${opcodeMapToString(opcodes)}'; } } class SfzGlobal { - SfzGlobal({ - this.opcodes, - }); + const SfzGlobal({this.opcodes}); - Map? opcodes; + final Map? opcodes; String buildString() { - return '\n' + (opcodeMapToString(opcodes)); + return '\n${opcodeMapToString(opcodes)}'; } } class SfzEffect { - SfzEffect({ - this.opcodes, - }); + const SfzEffect({this.opcodes}); - Map? opcodes; + final Map? opcodes; String buildString() { - return '\n' + (opcodeMapToString(opcodes)); + return '\n${opcodeMapToString(opcodes)}'; } } class SfzCurve { - SfzCurve({ - this.opcodes, - }); + const SfzCurve({this.opcodes}); - Map? opcodes; + final Map? opcodes; String buildString() { - return '\n' + (opcodeMapToString(opcodes)); + return '\n${opcodeMapToString(opcodes)}'; } } -/// Used to build an SFZ. Note that if lokey or hikey are not set on a given +/// Used to build an SFZ. Note that if loKey or hiKey are not set on a given /// region, they will be set automatically. class Sfz { - final List groups; - final List controls; - final List effects; - final List curves; - final SfzGlobal? global; - - Sfz({ + const Sfz({ required this.groups, this.controls = const [], this.effects = const [], @@ -125,30 +96,37 @@ class Sfz { this.global, }); + final List groups; + final List controls; + final List effects; + final List curves; + final SfzGlobal? global; + void _setNoteRanges() { - final allRegions = []; + final allRegions = []; - groups.forEach((g) => allRegions.addAll(g.regions)); + for (final g in groups) { + allRegions.addAll(g.regions); + } - allRegions.sort((a, b) => a.key - b.key); + allRegions.sort((a, b) => (a.key ?? 0) - (b.key ?? 0)); allRegions.asMap().forEach((index, sd) { final prevSd = index > 0 ? allRegions[index - 1] : null; - final nextSd = - index < allRegions.length - 1 ? allRegions[index + 1] : null; + final nextSd = index < allRegions.length - 1 ? allRegions[index + 1] : null; - if (sd.lokey == null) { + if (sd.loKey == null) { if (prevSd == null) { - sd.lokey = 0; + sd.loKey = 0; } else { - sd.lokey = ((sd.key + prevSd.key) / 2).floor() + 1; + sd.loKey = (((sd.key ?? 0) + (prevSd.key ?? 0)) / 2).floor() + 1; } } - if (sd.hikey == null) { + if (sd.hiKey == null) { if (nextSd == null) { - sd.hikey = 127; + sd.hiKey = 127; } else { - sd.hikey = ((nextSd.key + sd.key) / 2).floor(); + sd.hiKey = (((nextSd.key ?? 0) + (sd.key ?? 0)) / 2).floor(); } } }); @@ -158,9 +136,9 @@ class Sfz { _setNoteRanges(); return (global?.buildString() ?? '') + - controls.map((c) => c.buildString()).join('') + - effects.map((e) => e.buildString()).join('') + - curves.map((c) => c.buildString()).join('') + - groups.map((g) => g.buildString()).join(''); + controls.map((c) => c.buildString()).join() + + effects.map((e) => e.buildString()).join() + + curves.map((c) => c.buildString()).join() + + groups.map((g) => g.buildString()).join(); } } diff --git a/lib/native_bridge.dart b/lib/native_bridge.dart index f58213e8..af821942 100644 --- a/lib/native_bridge.dart +++ b/lib/native_bridge.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:io'; + import 'package:ffi/ffi.dart'; import 'package:flutter/services.dart'; @@ -11,70 +12,79 @@ final DynamicLibrary nativeLib = Platform.isAndroid ? DynamicLibrary.open('libflutter_sequencer.so') : DynamicLibrary.executable(); -final nRegisterPostCObject = nativeLib.lookupFunction< - Void Function( - Pointer)>> - functionPointer), - void Function( - Pointer)>> - functionPointer)>('RegisterDart_PostCObject'); +final nRegisterPostCObject = nativeLib + .lookupFunction< + Void Function( + Pointer)>> functionPointer, + ), + void Function( + Pointer)>> functionPointer, + ) + >('RegisterDart_PostCObject'); -final nSetupEngine = nativeLib - .lookupFunction('setup_engine'); +final nSetupEngine = nativeLib.lookupFunction( + 'setup_engine', +); -final nDestroyEngine = nativeLib - .lookupFunction('destroy_engine'); +final nDestroyEngine = nativeLib.lookupFunction('destroy_engine'); -final nAddTrackSf2 = nativeLib.lookupFunction< - Void Function(Pointer, Int8, Int32, Int64), - void Function(Pointer, int, int, int)>('add_track_sf2'); +final nAddTrackSf2 = nativeLib + .lookupFunction< + Void Function(Pointer, Int8, Int32, Int64), + void Function(Pointer, int, int, int) + >('add_track_sf2'); -final nAddTrackSfz = nativeLib.lookupFunction< - Void Function(Pointer, Pointer, Int64), - void Function(Pointer, Pointer, int)>('add_track_sfz'); +final nAddTrackSfz = nativeLib + .lookupFunction< + Void Function(Pointer, Pointer, Int64), + void Function(Pointer, Pointer, int) + >('add_track_sfz'); -final nAddTrackSfzString = nativeLib.lookupFunction< - Void Function(Pointer, Pointer, Pointer, Int64), - void Function(Pointer, Pointer, Pointer, - int)>('add_track_sfz_string'); +final nAddTrackSfzString = nativeLib + .lookupFunction< + Void Function(Pointer, Pointer, Pointer, Int64), + void Function(Pointer, Pointer, Pointer, int) + >('add_track_sfz_string'); -final nRemoveTrack = nativeLib - .lookupFunction('remove_track'); +final nRemoveTrack = nativeLib.lookupFunction( + 'remove_track', +); -final nResetTrack = nativeLib - .lookupFunction('reset_track'); +final nResetTrack = nativeLib.lookupFunction( + 'reset_track', +); -final nGetPosition = - nativeLib.lookupFunction('get_position'); +final nGetPosition = nativeLib.lookupFunction('get_position'); -final nGetTrackVolume = - nativeLib.lookupFunction( - 'get_track_volume'); +final nGetTrackVolume = nativeLib.lookupFunction( + 'get_track_volume', +); -final nGetLastRenderTimeUs = - nativeLib.lookupFunction( - 'get_last_render_time_us'); +final nGetLastRenderTimeUs = nativeLib.lookupFunction( + 'get_last_render_time_us', +); -final nGetBufferAvailableCount = - nativeLib.lookupFunction( - 'get_buffer_available_count'); +final nGetBufferAvailableCount = nativeLib + .lookupFunction('get_buffer_available_count'); -final nHandleEventsNow = nativeLib.lookupFunction< - Uint32 Function(Int32?, Pointer?, Uint32), - int Function(int?, Pointer?, int)>('handle_events_now'); +final nHandleEventsNow = nativeLib + .lookupFunction< + Uint32 Function(Int32?, Pointer?, Uint32), + int Function(int, Pointer?, int) + >('handle_events_now'); -final nScheduleEvents = nativeLib.lookupFunction< - Uint32 Function(Int32?, Pointer?, Uint32), - int Function(int?, Pointer?, int)>('schedule_events'); +final nScheduleEvents = nativeLib + .lookupFunction< + Uint32 Function(Int32?, Pointer?, Uint32), + int Function(int, Pointer?, int) + >('schedule_events'); -final nClearEvents = nativeLib.lookupFunction('clear_events'); +final nClearEvents = nativeLib + .lookupFunction('clear_events'); -final nPlay = - nativeLib.lookupFunction('engine_play'); +final nPlay = nativeLib.lookupFunction('engine_play'); -final nPause = - nativeLib.lookupFunction('engine_pause'); +final nPause = nativeLib.lookupFunction('engine_pause'); /// {@macro flutter_sequencer_library_private} /// This class encapsulates the boilerplate code needed to call into native code @@ -83,73 +93,59 @@ final nPause = class NativeBridge { static const MethodChannel _channel = MethodChannel('flutter_sequencer'); - // Must be called once, before any other method - static Future doSetup() async { - await _channel.invokeMethod('setupAssetManager'); - nRegisterPostCObject(NativeApi.postCObject); - - return singleResponseFuture((port) => nSetupEngine(port.nativePort)); - } - /// On Android, this will copy the asset dir from the AssetManager into /// context.filesDir, and return the filesystem path to the newly created - /// dir. Pathnames will be URL-decoded. + /// dir. Path names will be URL-decoded. /// On iOS, this will return the filesystem path to the asset dir. static Future normalizeAssetDir(String assetDir) async { - final args = { - 'assetDir': assetDir, - }; - final result = await _channel.invokeMethod('normalizeAssetDir', args); - final String? path = result; - - return path; + final args = {'assetDir': assetDir}; + return await _channel.invokeMethod('normalizeAssetDir', args); } static Future?> listAudioUnits() async { - final result = await _channel.invokeMethod('listAudioUnits'); - final List? audioUnitIds = result.cast(); + final result = await _channel.invokeMethod>('listAudioUnits'); + final audioUnitIds = result?.cast(); return audioUnitIds; } - static Future addTrackSf2( - String filename, bool isAsset, int patchNumber) { + static Future addTrackSf2(String filename, int patchNumber, {required bool isAsset}) { final filenameUtf8Ptr = filename.toNativeUtf8(); - return singleResponseFuture((port) => nAddTrackSf2( - filenameUtf8Ptr, isAsset ? 1 : 0, patchNumber, port.nativePort)); + return singleResponseFuture( + (port) => nAddTrackSf2(filenameUtf8Ptr, isAsset ? 1 : 0, patchNumber, port.nativePort), + ); } static Future addTrackSfz(String sfzPath, String? tuningPath) { final sfzPathUtf8Ptr = sfzPath.toNativeUtf8(); - final tuningPathUtf8Ptr = - tuningPath?.toNativeUtf8() ?? Pointer.fromAddress(0); + final tuningPathUtf8Ptr = tuningPath?.toNativeUtf8() ?? Pointer.fromAddress(0); - return singleResponseFuture((port) => - nAddTrackSfz(sfzPathUtf8Ptr, tuningPathUtf8Ptr, port.nativePort)); + return singleResponseFuture( + (port) => nAddTrackSfz(sfzPathUtf8Ptr, tuningPathUtf8Ptr, port.nativePort), + ); } - static Future addTrackSfzString( - String sampleRoot, String sfzContent, String? tuningString) { + static Future addTrackSfzString(String sampleRoot, String sfzContent, String? tuningString) { final sampleRootUtf8Ptr = sampleRoot.toNativeUtf8(); final sfzContentUtf8Ptr = sfzContent.toNativeUtf8(); - final tuningStringUtf8Ptr = - tuningString?.toNativeUtf8() ?? Pointer.fromAddress(0); + final tuningStringUtf8Ptr = tuningString?.toNativeUtf8() ?? Pointer.fromAddress(0); - return singleResponseFuture((port) => nAddTrackSfzString( + return singleResponseFuture( + (port) => nAddTrackSfzString( sampleRootUtf8Ptr, sfzContentUtf8Ptr, tuningStringUtf8Ptr, - port.nativePort)); + port.nativePort, + ), + ); } static Future addTrackAudioUnit(String id) async { if (Platform.isAndroid) return -1; - final args = { - 'id': id, - }; + final args = {'id': id}; - return await _channel.invokeMethod('addTrackAudioUnit', args); + return _channel.invokeMethod('addTrackAudioUnit', args); } static void removeTrack(int trackIndex) { @@ -176,34 +172,39 @@ class NativeBridge { return nGetBufferAvailableCount(trackIndex); } - static int handleEventsNow(int trackIndex, List events, - int sampleRate, double tempo) { + static int handleEventsNow( + int trackIndex, + List events, + int sampleRate, + double tempo, + ) { if (events.isEmpty) return 0; - final Pointer? nativeArray = - calloc(events.length * SCHEDULER_EVENT_SIZE); + final nativeArray = calloc(events.length * kSchedulerEventSize); events.asMap().forEach((eventIndex, e) { final byteData = e.serializeBytes(sampleRate, tempo, 0); for (var byteIndex = 0; byteIndex < byteData.lengthInBytes; byteIndex++) { - nativeArray![eventIndex * SCHEDULER_EVENT_SIZE + byteIndex] = - byteData.getUint8(byteIndex); + nativeArray[eventIndex * kSchedulerEventSize + byteIndex] = byteData.getUint8(byteIndex); } }); return nHandleEventsNow(trackIndex, nativeArray, events.length); } - static int scheduleEvents(int trackIndex, List events, - int sampleRate, double tempo, int frameOffset) { + static int scheduleEvents( + int trackIndex, + List events, + int sampleRate, + double tempo, + int frameOffset, + ) { if (events.isEmpty) return 0; - final Pointer? nativeArray = - calloc(events.length * SCHEDULER_EVENT_SIZE); + final nativeArray = calloc(events.length * kSchedulerEventSize); events.asMap().forEach((eventIndex, e) { final byteData = e.serializeBytes(sampleRate, tempo, frameOffset); for (var byteIndex = 0; byteIndex < byteData.lengthInBytes; byteIndex++) { - nativeArray![eventIndex * SCHEDULER_EVENT_SIZE + byteIndex] = - byteData.getUint8(byteIndex); + nativeArray[eventIndex * kSchedulerEventSize + byteIndex] = byteData.getUint8(byteIndex); } }); diff --git a/lib/sequence.dart b/lib/sequence.dart index f6179385..9cb9a0bd 100644 --- a/lib/sequence.dart +++ b/lib/sequence.dart @@ -8,28 +8,21 @@ import 'native_bridge.dart'; import 'track.dart'; /// {@macro flutter_sequencer_library_private} -enum LoopState { - Off, - BeforeLoopEnd, - AfterLoopEnd, -} +enum LoopState { off, beforeLoopEnd, afterLoopEnd } /// Represents a collection of tracks, play/pause state, position, loop state, /// and tempo. Play the sequence to schedule the events on its tracks. class Sequence { - static final GlobalState globalState = GlobalState(); - - Sequence({ - required this.tempo, - required this.endBeat, - }) { + Sequence({required this.tempo, required this.endBeat}) { id = globalState.registerSequence(this); } + static final GlobalState globalState = GlobalState(); + /// Call this to remove this sequence and its tracks from the global sequencer /// engine. void destroy() { - _tracks.values.forEach((track) => deleteTrack(track)); + _tracks.values.forEach(deleteTrack); globalState.unregisterSequence(this); } @@ -42,7 +35,7 @@ class Sequence { double endBeat; double pauseBeat = 0; int engineStartFrame = 0; - LoopState loopState = LoopState.Off; + LoopState loopState = LoopState.off; double loopStartBeat = 0; double loopEndBeat = 0; @@ -52,7 +45,7 @@ class Sequence { } /// Creates tracks in the underlying sequencer engine. - Future> createTracks(List instruments) async { + Future> createTracks(List instruments) { if (globalState.isEngineReady) { return _createTracks(instruments); } else { @@ -70,7 +63,7 @@ class Sequence { /// Removes a track from the underlying sequencer engine. List deleteTrack(Track track) { - final keysToRemove = []; + final keysToRemove = []; _tracks.forEach((key, value) { if (value == track) { @@ -78,10 +71,10 @@ class Sequence { } }); - keysToRemove.forEach((key) { + for (final key in keysToRemove) { NativeBridge.removeTrack(key); _tracks.remove(key); - }); + } return _tracks.values.toList(); } @@ -92,7 +85,7 @@ class Sequence { if (!globalState.isEngineReady) return; if (getIsOver()) { - setBeat(0.0); + setBeat(0); } globalState.playSequence(id); @@ -103,27 +96,27 @@ class Sequence { void pause() { if (!globalState.isEngineReady) return; - _tracks.values.forEach((track) { + for (final track in _tracks.values) { NativeBridge.resetTrack(track.id); - }); + } globalState.pauseSequence(id); } /// Stops playback of this sequence and resets its position to the beginning. void stop() { pause(); - setBeat(0.0); - _tracks.values.forEach((track) { + setBeat(0); + for (final track in _tracks.values) { List.generate(128, (noteNumber) { track.stopNoteNow(noteNumber: noteNumber); }); - }); + } } /// Sets the tempo. void setTempo(double nextTempo) { // Update engine start frame to remove excess loops - final loopsElapsed = loopState == LoopState.BeforeLoopEnd + final loopsElapsed = loopState == LoopState.beforeLoopEnd ? getLoopsElapsed(_getFramesRendered()) : 0; engineStartFrame += loopsElapsed * getLoopLengthFrames(); @@ -148,7 +141,7 @@ class Sequence { checkIsOver(); // Update engine start frame to remove excess loops - final loopsElapsed = loopState == LoopState.BeforeLoopEnd + final loopsElapsed = loopState == LoopState.beforeLoopEnd ? getLoopsElapsed(_getFramesRendered()) : 0; engineStartFrame += loopsElapsed * getLoopLengthFrames(); @@ -158,9 +151,9 @@ class Sequence { final currentFrame = _getFrame(false); if (currentFrame <= loopEndFrame) { - loopState = LoopState.BeforeLoopEnd; + loopState = LoopState.beforeLoopEnd; } else { - loopState = LoopState.AfterLoopEnd; + loopState = LoopState.afterLoopEnd; } this.loopStartBeat = loopStartBeat; @@ -171,7 +164,7 @@ class Sequence { /// Disables looping for the sequence. void unsetLoop() { - if (loopState == LoopState.BeforeLoopEnd) { + if (loopState == LoopState.beforeLoopEnd) { final loopsElapsed = getLoopsElapsed(_getFramesRendered()); engineStartFrame += loopsElapsed * getLoopLengthFrames(); @@ -179,27 +172,20 @@ class Sequence { loopStartBeat = 0; loopEndBeat = 0; - loopState = LoopState.Off; + loopState = LoopState.off; getTracks().forEach((track) => track.syncBuffer()); } - /// Sets the beat at which the sequence will end. Events after the end beat - /// won't be scheduled. - void setEndBeat(double beat) { - endBeat = beat; - } - /// Immediately changes the position of the sequence to the given beat. void setBeat(double beat) { if (!globalState.isEngineReady) return; - _tracks.values.forEach((track) { + for (final track in _tracks.values) { NativeBridge.resetTrack(track.id); - }); + } - final leadFrames = - getIsPlaying() ? min(_getFramesRendered(), LEAD_FRAMES) : 0; + final leadFrames = getIsPlaying() ? min(_getFramesRendered(), kLeadFrames) : 0; final frame = beatToFrames(beat) - leadFrames; @@ -210,11 +196,9 @@ class Sequence { track.syncBuffer(engineStartFrame); }); - if (loopState != LoopState.Off) { + if (loopState != LoopState.off) { final loopEndFrame = beatToFrames(loopEndBeat); - loopState = frame < loopEndFrame - ? LoopState.BeforeLoopEnd - : LoopState.AfterLoopEnd; + loopState = frame < loopEndFrame ? LoopState.beforeLoopEnd : LoopState.afterLoopEnd; } } @@ -225,13 +209,13 @@ class Sequence { /// Returns true if the sequence is at its end beat. bool getIsOver() { - return _getFrame(true) == beatToFrames(endBeat); + return _getFrame() == beatToFrames(endBeat); } /// Gets the current beat. Returns a value based on the number of frames /// rendered and the time elapsed since the last render callback. To omit /// the time elapsed since the last render callback, pass `false`. - double getBeat([bool estimateFramesSinceLastRender = true]) { + double getBeat({bool estimateFramesSinceLastRender = true}) { return framesToBeat(_getFrame(estimateFramesSinceLastRender)); } @@ -277,7 +261,7 @@ class Sequence { /// Converts a beat to sample frames. int beatToFrames(double beat) { // (min / b) * (ms) * (ms / min) - final us = ((1 / tempo) * beat * (60000000)).round(); + final us = ((1 / tempo) * beat * 60000000).round(); return Sequence.globalState.usToFrames(us); } @@ -307,7 +291,7 @@ class Sequence { int _getFramesRendered() { if (!globalState.isEngineReady) return 0; - return NativeBridge.getPosition() - engineStartFrame - LEAD_FRAMES; + return NativeBridge.getPosition() - engineStartFrame - kLeadFrames; } /// Gets the current frame position of the sequencer. @@ -315,10 +299,9 @@ class Sequence { if (!globalState.isEngineReady) return 0; if (isPlaying) { - final frame = _getFramesRendered() + - (estimateFramesSinceLastRender ? _getFramesSinceLastRender() : 0); - final loopedFrame = - loopState == LoopState.Off ? frame : getLoopedFrame(frame); + final frame = + _getFramesRendered() + (estimateFramesSinceLastRender ? _getFramesSinceLastRender() : 0); + final loopedFrame = loopState == LoopState.off ? frame : getLoopedFrame(frame); return max(min(loopedFrame, beatToFrames(endBeat)), 0); } else { @@ -330,9 +313,9 @@ class Sequence { /// was called. int _getFramesSinceLastRender() { final microsecondsSinceLastRender = max( - 0, - DateTime.now().microsecondsSinceEpoch - - NativeBridge.getLastRenderTimeUs()); + 0, + DateTime.now().microsecondsSinceEpoch - NativeBridge.getLastRenderTimeUs(), + ); return globalState.usToFrames(microsecondsSinceLastRender); } @@ -348,8 +331,7 @@ class Sequence { } Future> _createTracks(List instruments) async { - final tracks = await Future.wait( - instruments.map((instrument) => _createTrack(instrument))); + final tracks = await Future.wait(instruments.map(_createTrack)); final nonNullTracks = tracks.whereType().toList(); return nonNullTracks; diff --git a/lib/track.dart b/lib/track.dart index 8808dedc..41259661 100644 --- a/lib/track.dart +++ b/lib/track.dart @@ -4,60 +4,58 @@ import 'dart:math'; import 'package:path/path.dart' as p; import 'constants.dart'; -import 'models/instrument.dart'; import 'models/events.dart'; +import 'models/instrument.dart'; import 'native_bridge.dart'; import 'sequence.dart'; /// Represents a track. A track belongs to a sequence and has a collection of /// events. class Track { + Track._withId({required this.sequence, required this.id, required this.instrument}); + final Sequence sequence; final int id; final Instrument instrument; final events = []; int lastFrameSynced = 0; - Track._withId( - {required this.sequence, required this.id, required this.instrument}); - /// Creates a track in the underlying sequencer engine. - static Future build( - {required Sequence sequence, required Instrument instrument}) async { + static Future build({required Sequence sequence, required Instrument instrument}) async { int? id; if (instrument is Sf2Instrument) { id = await NativeBridge.addTrackSf2( - instrument.idOrPath, instrument.isAsset, instrument.presetIndex); + instrument.idOrPath, + instrument.presetIndex, + isAsset: instrument.isAsset, + ); } else if (instrument is SfzInstrument) { final sfzFile = File(instrument.idOrPath); String? normalizedSfzPath; if (instrument.isAsset) { - final normalizedSfzDir = - await NativeBridge.normalizeAssetDir(sfzFile.parent.path); + final normalizedSfzDir = await NativeBridge.normalizeAssetDir(sfzFile.parent.path); - if (normalizedSfzDir == null) - throw Exception( - 'Could not normalize asset dir for ${sfzFile.parent.path}'); + if (normalizedSfzDir == null) { + throw Exception('Could not normalize asset dir for ${sfzFile.parent.path}'); + } normalizedSfzPath = '$normalizedSfzDir/${p.basename(sfzFile.path)}'; } else { normalizedSfzPath = sfzFile.path; } - id = await NativeBridge.addTrackSfz( - normalizedSfzPath, instrument.tuningPath); + id = await NativeBridge.addTrackSfz(normalizedSfzPath, instrument.tuningPath); } else if (instrument is RuntimeSfzInstrument) { final sfzContent = instrument.sfz.buildString(); String? normalizedSampleRoot; if (instrument.isAsset) { - normalizedSampleRoot = - await NativeBridge.normalizeAssetDir(instrument.sampleRoot); + normalizedSampleRoot = await NativeBridge.normalizeAssetDir(instrument.sampleRoot); - if (normalizedSampleRoot == null) - throw Exception( - 'Could not normalize asset dir for ${instrument.sampleRoot}'); + if (normalizedSampleRoot == null) { + throw Exception('Could not normalize asset dir for ${instrument.sampleRoot}'); + } } else { normalizedSampleRoot = instrument.sampleRoot; } @@ -65,8 +63,7 @@ class Track { // Sfizz uses the parent path of this (line 73 of Parser.cpp) final fakeSfzDir = '$normalizedSampleRoot/does_not_exist.sfz'; - id = await NativeBridge.addTrackSfzString( - fakeSfzDir, sfzContent, instrument.tuningString); + id = await NativeBridge.addTrackSfzString(fakeSfzDir, sfzContent, instrument.tuningString); } else if (instrument is AudioUnitInstrument) { id = await NativeBridge.addTrackAudioUnit(instrument.idOrPath); } else { @@ -75,11 +72,7 @@ class Track { if (id == -1) return null; - return Track._withId( - sequence: sequence, - id: id!, - instrument: instrument, - ); + return Track._withId(sequence: sequence, id: id!, instrument: instrument); } /// Handles a Note On event on this track immediately. @@ -87,12 +80,12 @@ class Track { void startNoteNow({required int noteNumber, required double velocity}) { final nextBeat = sequence.getBeat(); final event = MidiEvent.ofNoteOn( - beat: nextBeat, - noteNumber: noteNumber, - velocity: _velocityToMidi(velocity)); + beat: nextBeat, + noteNumber: noteNumber, + velocity: _velocityToMidi(velocity), + ); - NativeBridge.handleEventsNow( - id, [event], Sequence.globalState.sampleRate!, sequence.tempo); + NativeBridge.handleEventsNow(id, [event], Sequence.globalState.sampleRate!, sequence.tempo); } /// Handles a Note Off event on this track immediately. @@ -101,19 +94,16 @@ class Track { final nextBeat = sequence.getBeat(); final event = MidiEvent.ofNoteOff(beat: nextBeat, noteNumber: noteNumber); - NativeBridge.handleEventsNow( - id, [event], Sequence.globalState.sampleRate!, sequence.tempo); + NativeBridge.handleEventsNow(id, [event], Sequence.globalState.sampleRate!, sequence.tempo); } /// Handles a MIDI CC event on this track immediately. /// The event will not be added to this track's events. void midiCCNow({required int ccNumber, required int ccValue}) { final nextBeat = sequence.getBeat(); - final event = - MidiEvent.cc(beat: nextBeat, ccNumber: ccNumber, ccValue: ccValue); + final event = MidiEvent.cc(beat: nextBeat, ccNumber: ccNumber, ccValue: ccValue); - NativeBridge.handleEventsNow( - id, [event], Sequence.globalState.sampleRate!, sequence.tempo); + NativeBridge.handleEventsNow(id, [event], Sequence.globalState.sampleRate!, sequence.tempo); } /// Handles a MIDI pitch bend event on this track immediately. @@ -122,8 +112,7 @@ class Track { final nextBeat = sequence.getBeat(); final event = MidiEvent.pitchBend(beat: nextBeat, value: value); - NativeBridge.handleEventsNow( - id, [event], Sequence.globalState.sampleRate!, sequence.tempo); + NativeBridge.handleEventsNow(id, [event], Sequence.globalState.sampleRate!, sequence.tempo); } /// Handles a Volume Change event on this track immediately. @@ -132,36 +121,26 @@ class Track { final nextBeat = sequence.getBeat(); final event = VolumeEvent(beat: nextBeat, volume: volume); - NativeBridge.handleEventsNow( - id, [event], Sequence.globalState.sampleRate!, sequence.tempo); + NativeBridge.handleEventsNow(id, [event], Sequence.globalState.sampleRate!, sequence.tempo); } /// Adds a Note On and Note Off event to this track. /// This does not sync the events to the backend. - void addNote( - {required int noteNumber, - required double velocity, - required double startBeat, - required double durationBeats}) { - addNoteOn( - noteNumber: noteNumber, - velocity: velocity, - beat: startBeat, - ); - - addNoteOff( - noteNumber: noteNumber, - beat: startBeat + durationBeats, - ); + void addNote({ + required int noteNumber, + required double velocity, + required double startBeat, + required double durationBeats, + }) { + addNoteOn(noteNumber: noteNumber, velocity: velocity, beat: startBeat); + + addNoteOff(noteNumber: noteNumber, beat: startBeat + durationBeats); } /// Adds a Note On event to this track. /// This does not sync the events to the backend. - void addNoteOn( - {required int noteNumber, - required double velocity, - required double beat}) { - assert(velocity > 0 && velocity <= 1); + void addNoteOn({required int noteNumber, required double velocity, required double beat}) { + assert(velocity > 0 && velocity <= 1, 'Velocity out-of-range: $velocity must be in (0, 1]]'); final noteOnEvent = MidiEvent.ofNoteOn( beat: beat, @@ -175,20 +154,15 @@ class Track { /// Adds a Note Off event to this track. /// This does not sync the events to the backend. void addNoteOff({required int noteNumber, required double beat}) { - final noteOffEvent = MidiEvent.ofNoteOff( - beat: beat, - noteNumber: noteNumber, - ); + final noteOffEvent = MidiEvent.ofNoteOff(beat: beat, noteNumber: noteNumber); _addEvent(noteOffEvent); } /// Adds a MIDI CC event to this track. /// This does not sync the events to the backend. - void addMidiCC( - {required int ccNumber, required int ccValue, required double beat}) { - final ccEvent = - MidiEvent.cc(beat: beat, ccNumber: ccNumber, ccValue: ccValue); + void addMidiCC({required int ccNumber, required int ccValue, required double beat}) { + final ccEvent = MidiEvent.cc(beat: beat, ccNumber: ccNumber, ccValue: ccValue); _addEvent(ccEvent); } @@ -223,20 +197,20 @@ class Track { /// Syncs events to the backend. This should be called after making changes to /// track events to ensure that the changes are synced immediately. - void syncBuffer( - [int? absoluteStartFrame, int maxEventsToSync = BUFFER_SIZE]) { + void syncBuffer([int? absoluteStartFrame, int maxEventsToSync = kBufferSize]) { final position = NativeBridge.getPosition(); - if (absoluteStartFrame == null) { - absoluteStartFrame = position; + var absoluteStartFrameVar = absoluteStartFrame; + if (absoluteStartFrameVar == null) { + absoluteStartFrameVar = position; } else { - absoluteStartFrame = max(absoluteStartFrame, position); + absoluteStartFrameVar = max(absoluteStartFrameVar, position); } - NativeBridge.clearEvents(id, absoluteStartFrame); + NativeBridge.clearEvents(id, absoluteStartFrameVar); if (sequence.isPlaying) { - final relativeStartFrame = absoluteStartFrame - sequence.engineStartFrame; + final relativeStartFrame = absoluteStartFrameVar - sequence.engineStartFrame; _scheduleEvents(relativeStartFrame, maxEventsToSync); } else { lastFrameSynced = 0; @@ -268,8 +242,7 @@ class Track { if (events.isEmpty) { index = 0; } else { - final indexWhereResult = - events.indexWhere((e) => _compareEvents(e, eventToAdd) == 1); + final indexWhereResult = events.indexWhere((e) => _compareEvents(e, eventToAdd) == 1); if (indexWhereResult == -1) { index = events.length; @@ -283,19 +256,19 @@ class Track { /// Builds events that can be scheduled in the sequencer engine's event buffer /// and adds them to eventsList. - void _scheduleEvents(int startFrame, [int maxEventsToSync = BUFFER_SIZE]) { - final isBeforeLoopEnd = sequence.loopState == LoopState.BeforeLoopEnd; + void _scheduleEvents(int startFrame, [int maxEventsToSync = kBufferSize]) { + final isBeforeLoopEnd = sequence.loopState == LoopState.beforeLoopEnd; final loopLength = sequence.getLoopLengthFrames(); - final loopsElapsed = sequence.loopState == LoopState.Off + final loopsElapsed = sequence.loopState == LoopState.off ? 0 : sequence.getLoopsElapsed(startFrame); var eventsSyncedCount = _scheduleEventsInRange( - maxEventsToSync, - isBeforeLoopEnd ? sequence.getLoopedFrame(startFrame) : startFrame, - sequence.beatToFrames( - isBeforeLoopEnd ? sequence.loopEndBeat : sequence.endBeat), - loopLength * loopsElapsed); + maxEventsToSync, + isBeforeLoopEnd ? sequence.getLoopedFrame(startFrame) : startFrame, + sequence.beatToFrames(isBeforeLoopEnd ? sequence.loopEndBeat : sequence.endBeat), + loopLength * loopsElapsed, + ); if (isBeforeLoopEnd) { var loopIndex = loopsElapsed + 1; @@ -306,10 +279,11 @@ class Track { while (eventsSyncedCount < maxEventsToSync) { // Schedule all events in one loop range lastBatchCount = _scheduleEventsInRange( - maxEventsToSync - eventsSyncedCount, - loopStartFrame, - loopEndFrame, - loopLength * loopIndex); + maxEventsToSync - eventsSyncedCount, + loopStartFrame, + loopEndFrame, + loopLength * loopIndex, + ); eventsSyncedCount += lastBatchCount; if (lastBatchCount == 0) break; @@ -320,8 +294,7 @@ class Track { /// Schedules this track's events that start on or after startBeat and end /// on or before endBeat. Adds frameOffset to every scheduled event. - int _scheduleEventsInRange( - int maxEventsToSync, int startFrame, int? endFrame, int frameOffset) { + int _scheduleEventsInRange(int maxEventsToSync, int startFrame, int? endFrame, int frameOffset) { final eventsToSync = []; for (var eventIndex = 0; eventIndex < events.length; eventIndex++) { @@ -337,14 +310,16 @@ class Track { } final eventsSyncedCount = NativeBridge.scheduleEvents( - id, - eventsToSync, - Sequence.globalState.sampleRate!, - sequence.tempo, - sequence.engineStartFrame + frameOffset); + id, + eventsToSync, + Sequence.globalState.sampleRate!, + sequence.tempo, + sequence.engineStartFrame + frameOffset, + ); if (eventsSyncedCount > 0) { - lastFrameSynced = sequence.engineStartFrame + + lastFrameSynced = + sequence.engineStartFrame + sequence.beatToFrames(eventsToSync[eventsSyncedCount - 1].beat) + frameOffset; } @@ -361,20 +336,20 @@ class Track { } else { // Beats are the same - if (eventA is VolumeEvent && !(eventB is VolumeEvent)) { + if (eventA is VolumeEvent && eventB is! VolumeEvent) { // Volume should come before anything else return -1; - } else if (eventB is VolumeEvent && !(eventA is VolumeEvent)) { + } else if (eventB is VolumeEvent && eventA is! VolumeEvent) { return 1; } else if (eventA is MidiEvent && eventB is MidiEvent) { // Note off should come before note on if the note is the same if (eventA.midiData1 == eventB.midiData1 && - eventA.midiStatus == MIDI_STATUS_NOTE_OFF && - eventB.midiStatus == MIDI_STATUS_NOTE_ON) { + eventA.midiStatus == kMidiStatusNoteOff && + eventB.midiStatus == kMidiStatusNoteOn) { return -1; } else if (eventA.midiData1 == eventB.midiData1 && - eventA.midiStatus == MIDI_STATUS_NOTE_ON && - eventB.midiStatus == MIDI_STATUS_NOTE_OFF) { + eventA.midiStatus == kMidiStatusNoteOn && + eventB.midiStatus == kMidiStatusNoteOff) { return 1; } else { return 0; diff --git a/lib/utils/isolate.dart b/lib/utils/isolate.dart index b4e5f401..9fd106be 100644 --- a/lib/utils/isolate.dart +++ b/lib/utils/isolate.dart @@ -42,12 +42,16 @@ void _castComplete(Completer completer, Object value) { } } -Future singleResponseFuture(void Function(SendPort responsePort) action, - {Duration? timeout, R? timeoutValue}) { - var completer = Completer.sync(); - var responsePort = RawReceivePort(); +Future singleResponseFuture( + void Function(SendPort responsePort) action, { + Duration? timeout, + R? timeoutValue, +}) { + final completer = Completer.sync(); + final responsePort = RawReceivePort(); Timer? timer; - var zone = Zone.current; + final zone = Zone.current; + // ignore: avoid_types_on_closure_parameters responsePort.handler = (Object response) { responsePort.close(); timer?.cancel(); diff --git a/pubspec.lock b/pubspec.lock index 4c5757bd..c2efeed4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,157 +5,217 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" + version: "1.4.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.19.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.2.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" source: hosted - version: "0.12.10" + version: "0.13.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.17.0" path: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" source: hosted - version: "1.8.0" - pedantic: - dependency: "direct dev" + version: "1.9.1" + plugin_platform_interface: + dependency: "direct main" description: - name: pedantic - url: "https://pub.dartlang.org" + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "2.1.8" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" source: hosted - version: "0.3.0" - typed_data: + version: "0.7.10" + vector_math: dependency: transitive description: - name: typed_data - url: "https://pub.dartlang.org" + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" source: hosted - version: "1.3.0" - vector_math: + version: "2.2.0" + vm_service: dependency: transitive description: - name: vector_math - url: "https://pub.dartlang.org" + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "15.2.0" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.10.0" + dart: ">=3.11.5 <4.0.0" + flutter: ">=3.41.9" diff --git a/pubspec.yaml b/pubspec.yaml index 350e90b9..7477992e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,34 +1,27 @@ name: flutter_sequencer -version: 0.4.4 +version: 0.5.0 description: >- A Flutter plugin for sequencing audio. Use it to build sequences of notes and play them back using SFZ or SF2 sound fonts. repository: https://github.com/mikeperri/flutter_sequencer environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.10.0" + sdk: '>=3.11.5 <4.0.0' + flutter: ">=3.41.9" dependencies: + ffi: flutter: sdk: flutter - ffi: ^1.0.0 - path: ^1.8.0 + path: + plugin_platform_interface: dev_dependencies: + flutter_lints: flutter_test: sdk: flutter - pedantic: ^1.11.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' and Android 'package' identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: android: @@ -36,34 +29,3 @@ flutter: pluginClass: FlutterSequencerPlugin ios: pluginClass: FlutterSequencerPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/reset.sh b/reset.sh new file mode 100644 index 00000000..336dd936 --- /dev/null +++ b/reset.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +flutter clean +find . -name ".dart_tool" -exec rm -fR {} \; +find . -name "pubspec.lock" -exec rm {} \; +find . -name "pubspec_overrides.yaml" -exec rm {} \; +find . -name "build" -exec rm -fR {} \; +flutter pub get diff --git a/test/flutter_sequencer_test.dart b/test/flutter_sequencer_test.dart index 3e824801..d30207dd 100644 --- a/test/flutter_sequencer_test.dart +++ b/test/flutter_sequencer_test.dart @@ -1,18 +1,3 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -void main() { - const channel = MethodChannel('flutter_sequencer'); - TestWidgetsFlutterBinding.ensureInitialized(); - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; - }); - }); - - tearDown(() { - channel.setMockMethodCallHandler(null); - }); -} +void main() {}