diff --git a/gradle.properties b/gradle.properties index 01b80d70..ac1c36ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,3 +17,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 91f26f53..3a542b5b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1 +1 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip \ No newline at end of file diff --git a/mwengine/CMakeLists.txt b/mwengine/CMakeLists.txt index ade95c64..703fd23b 100644 --- a/mwengine/CMakeLists.txt +++ b/mwengine/CMakeLists.txt @@ -34,7 +34,7 @@ set(target mwengine) # architecture-specific compiler flags if (${ANDROID_ABI} MATCHES "x86_64") - SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64 -msse4.2 -mpopcnt -m64") endif() # source folders diff --git a/mwengine/build.gradle b/mwengine/build.gradle index 8cbd894d..d112fba6 100644 --- a/mwengine/build.gradle +++ b/mwengine/build.gradle @@ -7,12 +7,13 @@ buildscript { maven { url 'https://jitpack.io' } } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:8.2.1' } } android { - compileSdkVersion 32 + namespace 'nl.igorski.mwengine' + compileSdk 33 defaultConfig { // NOTE: the value of minSdkVersion is used by the native build as the target APP_PLATFORM @@ -22,7 +23,7 @@ android { // thus set the value of minSdkVersion to whatever fits your use case. minSdkVersion 16 - targetSdkVersion 32 + targetSdkVersion 33 ndk { abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" diff --git a/mwengine/src/main/AndroidManifest.xml b/mwengine/src/main/AndroidManifest.xml index e7490cf4..d6fa7c38 100644 --- a/mwengine/src/main/AndroidManifest.xml +++ b/mwengine/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/mwengine/src/main/cpp/audioengine.cpp b/mwengine/src/main/cpp/audioengine.cpp index c2412dcf..d35b4294 100755 --- a/mwengine/src/main/cpp/audioengine.cpp +++ b/mwengine/src/main/cpp/audioengine.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2022 Igor Zinken - https://www.igorski.nl + * Copyright (c) 2013-2024 Igor Zinken - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -77,6 +77,7 @@ namespace MWEngine { int AudioEngine::time_sig_beat_unit = 4; int AudioEngine::queuedTime_sig_beat_amount = time_sig_beat_amount; int AudioEngine::queuedTime_sig_beat_unit = time_sig_beat_unit; + bool AudioEngine::broadcast_idle = false; /* buffer read/write pointers */ @@ -745,6 +746,12 @@ namespace MWEngine { if ( recordingState.bouncing && AudioEngineProps::isRendering.load() && DriverAdapter::isAAudio() ) { render( amountOfSamples ); } + + if ( broadcast_idle ) { + broadcast_idle = false; + Notifier::broadcast( Notifications::ENGINE_IDLE ); + } + return AudioEngineProps::isRendering.load(); } @@ -762,6 +769,11 @@ namespace MWEngine { #endif } + void AudioEngine::notifyWhenIdle() + { + broadcast_idle = true; + } + void AudioEngine::handleTempoUpdate( float aQueuedTempo, bool broadcastUpdate ) { float ratio = 1; diff --git a/mwengine/src/main/cpp/audioengine.h b/mwengine/src/main/cpp/audioengine.h index e8bc4b94..93315401 100755 --- a/mwengine/src/main/cpp/audioengine.h +++ b/mwengine/src/main/cpp/audioengine.h @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2022 Igor Zinken - https://www.igorski.nl + * Copyright (c) 2013-2024 Igor Zinken - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -49,6 +49,7 @@ class AudioEngine static void start( Drivers::types audioDriver ); static void stop(); static void reset(); + static void notifyWhenIdle(); // request to be notified when engine reaches idle state after render (fires once per request) static AudioChannel* getInputChannel(); @@ -143,6 +144,8 @@ class AudioEngine static int min_step_position; // the lowest step in the current sequence static int max_step_position; // the maximum step in the current sequence (e.g. 15 for single measure using a 16 step sequencer - step starts at 0.) + static bool broadcast_idle; // whether to broadcast a notification when the engine is entering idle phase after render + /* buffer read/write pointers */ static int bufferPosition; // the current sequence position in samples ("playback head" offset) diff --git a/mwengine/src/main/cpp/definitions/notifications.h b/mwengine/src/main/cpp/definitions/notifications.h index 18d8b0b5..b673a11d 100644 --- a/mwengine/src/main/cpp/definitions/notifications.h +++ b/mwengine/src/main/cpp/definitions/notifications.h @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2015-2018 Igor Zinken - http://www.igorski.nl + * Copyright (c) 2015-2024 Igor Zinken - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -52,6 +52,7 @@ class Notifications /* system messages */ STATUS_BRIDGE_CONNECTED, // JNI bridge connected + ENGINE_IDLE, // engine is currently in idle state (between render cycles), this is the time to safely clean up resources /* fatal errors */ diff --git a/mwengine/src/main/cpp/events/baseaudioevent.cpp b/mwengine/src/main/cpp/events/baseaudioevent.cpp index 7ad44506..699bf785 100755 --- a/mwengine/src/main/cpp/events/baseaudioevent.cpp +++ b/mwengine/src/main/cpp/events/baseaudioevent.cpp @@ -54,8 +54,21 @@ BaseAudioEvent::~BaseAudioEvent() void BaseAudioEvent::dispose() { - removeFromSequencer(); - destroyBuffer(); + _disposed = true; // set the disposed flag as removal can be async when sequencer is running + _enabled = false; + + if ( _instrument != nullptr && isSequenced ) { + _instrument->removeEvent( this, false ); + } else { + stop(); + } +} + +void BaseAudioEvent::onRemove() +{ + if ( _disposed ) { + destroyBuffer(); // if event was removed as part of disposal routine, clean the buffer now + } } BaseInstrument* BaseAudioEvent::getInstrument() diff --git a/mwengine/src/main/cpp/events/baseaudioevent.h b/mwengine/src/main/cpp/events/baseaudioevent.h index 61c6ca1c..9f2573aa 100755 --- a/mwengine/src/main/cpp/events/baseaudioevent.h +++ b/mwengine/src/main/cpp/events/baseaudioevent.h @@ -35,7 +35,8 @@ class BaseAudioEvent BaseAudioEvent(); virtual ~BaseAudioEvent(); - void dispose(); // invoke when removing the Event from the engine + void dispose(); // invoke when removing the Event from the engine, should be done when ENGINE_IDLE is broadcast! + void onRemove(); // invoked by the Instrument once the event has safely been removed /** * event volume is in a percentile (0 - 1) range @@ -213,6 +214,7 @@ class BaseAudioEvent bool _removalEnqueued; bool _locked; bool _updateAfterUnlock; // use in update-methods when checking for lock + bool _disposed = false; // _destroyableBuffer indicates we can delete the buffer on destruction (true by default and // implies that this AudioEvent holds the only reference to its buffers diff --git a/mwengine/src/main/cpp/instruments/baseinstrument.cpp b/mwengine/src/main/cpp/instruments/baseinstrument.cpp index 6cbd0d64..3d1de973 100644 --- a/mwengine/src/main/cpp/instruments/baseinstrument.cpp +++ b/mwengine/src/main/cpp/instruments/baseinstrument.cpp @@ -175,6 +175,9 @@ bool BaseInstrument::removeEvent( BaseAudioEvent* audioEvent, bool isLiveEvent ) removeEventFromMeasureCache( audioEvent ); } } + if ( removed ) { + audioEvent->onRemove(); + } return removed; } diff --git a/mwengine/src/main/cpp/sequencer.cpp b/mwengine/src/main/cpp/sequencer.cpp index 97af0918..6e3ba2d5 100755 --- a/mwengine/src/main/cpp/sequencer.cpp +++ b/mwengine/src/main/cpp/sequencer.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2022 Igor Zinken - https://www.igorski.nl + * Copyright (c) 2013-2024 Igor Zinken - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -45,6 +45,7 @@ int Sequencer::registerInstrument( BaseInstrument* instrument ) for ( auto const & i : instruments ) { if ( i == instrument ) { wasPresent = true; + break; } } @@ -112,9 +113,9 @@ bool Sequencer::getAudioEvents( std::vector* channels, int buffer } } - if ( addLiveInstruments && instrument->hasLiveEvents() ) + if ( addLiveInstruments && instrument->hasLiveEvents() ) { collectLiveEvents( instrument ); - + } channels->push_back( instrumentChannel ); } } @@ -133,7 +134,6 @@ void Sequencer::collectSequencedEvents( BaseInstrument* instrument, int bufferPo if ( !instrument->hasEvents() ) { return; } - AudioChannel* channel = instrument->audioChannel; auto audioEvents = instrument->getEventsForMeasure( measure ); diff --git a/mwengine/src/main/cpp/sequencercontroller.cpp b/mwengine/src/main/cpp/sequencercontroller.cpp index 8c08ef13..222b7993 100644 --- a/mwengine/src/main/cpp/sequencercontroller.cpp +++ b/mwengine/src/main/cpp/sequencercontroller.cpp @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2022 Igor Zinken - https://www.igorski.nl + * Copyright (c) 2013-2024 Igor Zinken - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -86,6 +86,11 @@ void SequencerController::setVolume( float aVolume ) AudioEngine::volume = VolumeUtil::toLog( aVolume ); } +bool SequencerController::getPlaying() +{ + return Sequencer::playing; +} + void SequencerController::setPlaying( bool aIsPlaying ) { Sequencer::playing = aIsPlaying; diff --git a/mwengine/src/main/cpp/sequencercontroller.h b/mwengine/src/main/cpp/sequencercontroller.h index e2358adc..f70260d6 100644 --- a/mwengine/src/main/cpp/sequencercontroller.h +++ b/mwengine/src/main/cpp/sequencercontroller.h @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2022 Igor Zinken - http://www.igorski.nl + * Copyright (c) 2013-2024 Igor Zinken - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -44,6 +44,7 @@ class SequencerController void setTempo ( float aTempo, int aTimeSigBeatAmount, int aTimeSigBeatUnit ); void setTempoNow( float aTempo, int aTimeSigBeatAmount, int aTimeSigBeatUnit ); void setVolume ( float aVolume ); + bool getPlaying (); void setPlaying ( bool aPlaying ); /** diff --git a/mwengine/src/main/java/nl/igorski/mwengine/MWEngine.java b/mwengine/src/main/java/nl/igorski/mwengine/MWEngine.java index 43d07e90..b39c3efe 100644 --- a/mwengine/src/main/java/nl/igorski/mwengine/MWEngine.java +++ b/mwengine/src/main/java/nl/igorski/mwengine/MWEngine.java @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Copyright (c) 2013-2022 Igor Zinken - https://www.igorski.nl + * Copyright (c) 2013-2024 Igor Zinken - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -34,6 +34,10 @@ import android.util.Log; import android.view.WindowManager; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import nl.igorski.mwengine.core.*; public final class MWEngine @@ -77,6 +81,10 @@ public interface IObserver void handleNotification( int aNotificationId, int aNotificationValue ); } + public interface VoidInterface { + public void operation(); + } + private static MWEngine INSTANCE; // we only allow a single instance to be constructed for resource optimization private IObserver _observer; @@ -96,6 +104,8 @@ public interface IObserver private boolean _initialCreation = true; private boolean _isRunning = false; + private List _idleCallbacks = new ArrayList( Collections.emptyList()); + private static int ENGINE_IDLE_NOTIFICATION = Notifications.ids.ENGINE_IDLE.ordinal(); /** * The Java-side bridge to manage all native layer components @@ -373,6 +383,20 @@ public void saveRecordedSnippet( int snippetBufferIndex ) { AudioEngine.saveRecordedSnippet( snippetBufferIndex ); } + /** + * Delay execution of provided callback until the engine is idle, e.g. + * between render cycles. This can be used to safely dispose() of sequenced AudioEvents + * and deallocate referenced resources while the Sequencer is running. + */ + public void executeWhenIdle( VoidInterface callback ) { + if ( _sequencerController.getPlaying()) { + _idleCallbacks.add( callback ); + AudioEngine.notifyWhenIdle(); + } else { + callback.operation(); + } + } + public void reset() { AudioEngine.reset(); } @@ -465,7 +489,17 @@ public static void handleBridgeConnected( int aSomething ) { } public static void handleNotification( int aNotificationId ) { - if ( INSTANCE != null && INSTANCE._observer != null ) INSTANCE._observer.handleNotification( aNotificationId ); + if ( INSTANCE != null ) { + if ( ENGINE_IDLE_NOTIFICATION == aNotificationId ) { + for ( VoidInterface callback : INSTANCE._idleCallbacks ) { + callback.operation(); + } + INSTANCE._idleCallbacks.clear(); + } + if ( INSTANCE._observer != null ) { + INSTANCE._observer.handleNotification( aNotificationId ); + } + } } public static void handleNotificationWithData( int aNotificationId, int aNotificationData ) { diff --git a/mwengine_example/build.gradle b/mwengine_example/build.gradle index aa259e5f..3596733a 100644 --- a/mwengine_example/build.gradle +++ b/mwengine_example/build.gradle @@ -19,12 +19,13 @@ buildscript { maven { url 'https://jitpack.io' } } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:8.2.1' } } android { - compileSdkVersion 32 + namespace 'nl.igorski.mwengine.example' + compileSdk 33 defaultConfig { // the sdk version range below matches the one of the library. You can change these as you see fit @@ -34,7 +35,7 @@ android { // thus set the value of minSdkVersion to whatever fits your use case. minSdkVersion 16 - targetSdkVersion 32 + targetSdkVersion 33 versionCode 1 versionName "1.0.0" diff --git a/mwengine_example/src/main/AndroidManifest.xml b/mwengine_example/src/main/AndroidManifest.xml index d4173b42..be9bf2d8 100644 --- a/mwengine_example/src/main/AndroidManifest.xml +++ b/mwengine_example/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/mwengine_example/src/main/java/nl/igorski/mwengine/example/MWEngineActivity.java b/mwengine_example/src/main/java/nl/igorski/mwengine/example/MWEngineActivity.java index 1464f3d9..70fb0798 100644 --- a/mwengine_example/src/main/java/nl/igorski/mwengine/example/MWEngineActivity.java +++ b/mwengine_example/src/main/java/nl/igorski/mwengine/example/MWEngineActivity.java @@ -223,7 +223,13 @@ private void init() { }); findViewById( R.id.PatternSwitchButton ).setOnClickListener(( View v ) -> { _patternIndex = _patternIndex == 0 ? 1 : 0; - createBassPattern(); + // we allow users to dispose events while the Sequencer is running, as such we delay execution + // until the engine has notified that it has entered its idle phase between renders (meaning: + // it is safe to perform operations that potentially deallocate buffer memory) + _engine.executeWhenIdle(() -> { + createDrumPattern(); + createBassPattern(); + }); }); findViewById( R.id.RecordInputButton ).setOnTouchListener(( View v, MotionEvent event ) -> { switch( event.getAction()) { @@ -315,12 +321,7 @@ protected void setupSong() { // STEP 2 : Sample events to play back a drum beat - createDrumEvent( "hat", 2 ); // hi-hat on the second 8th note after the first beat of the bar - createDrumEvent( "clap", 4 ); // clap sound on the second beat of the bar - createDrumEvent( "hat", 6 ); // hi-hat on the second 8th note after the second beat - createDrumEvent( "hat", 10 ); // hi-hat on the second 8th note after the third beat - createDrumEvent( "clap", 12 ); // clap sound on the third beat of the bar - createDrumEvent( "hat", 14 ); // hi-hat on the second 8th note after the fourth beat + createDrumPattern(); // Real-time synthesis events @@ -550,6 +551,22 @@ private void createSynthEvent( SynthInstrument synth, double frequency, int posi _synth2Events.add( event ); } + private void createDrumPattern() { + // clear any existing patterns (when switching) + for ( final SampleEvent event : _drumEvents ) { + event.dispose(); + } + _drumEvents.clear(); // clear the old events so they can be garbage collected + // depending on the pattern hi-hats occur on the off beats or every 16th note + int start = _patternIndex == 0 ? 2 : 0; + int incr = _patternIndex == 0 ? 4 : 1; + for ( int i = start; i < 16; i += incr ) { + createDrumEvent( "hat", i ); + } + createDrumEvent( "clap", 4 ); // clap sound on the second beat of the bar + createDrumEvent( "clap", 12 ); // clap sound on the third beat of the bar + } + private void createBassPattern() { // clear any existing patterns (when switching) for ( final SynthEvent event : _synth1Events ) { diff --git a/mwengine_example/src/main/res/values/strings.xml b/mwengine_example/src/main/res/values/strings.xml index 53bc2ea9..dc8ba167 100755 --- a/mwengine_example/src/main/res/values/strings.xml +++ b/mwengine_example/src/main/res/values/strings.xml @@ -9,7 +9,7 @@ You can add new audio on the fly too: hold down the first button to play a C note or hold down the second button to record audio from your Android device input (tip: use headphones to prevent feedback occurring when using the microphone on your phone at louder volume settings) - You can switch up the bass pattern by clicking the button below. One plays the "official" MWEngine loop and the other E1M1 by Bobby Prince, or whoever inspired him. + You can switch up the bass and drum patterns by clicking the button below. One plays the "official" MWEngine loop and the other E1M1 by Bobby Prince, or whoever inspired him. Audio driver Play Pause