From 62a87399080244ca5f4b3711a037142c55030ff1 Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Sun, 29 Jun 2025 15:12:04 +0200 Subject: [PATCH 01/11] Give up com.noisepages.nettoyeur:midi Reimplement Midi adapters using Android Midi API, reusing Peter's (from/to)WireConverters coming from: https://github.com/nettoyeurny/btmidi/blob/master/AndroidMidi/src/com/noisepages/nettoyeur/midi/ --- PdCore/build.gradle | 2 - .../android/midi/MidiToPdAdapter.java | 119 +++++++++++------- .../android/midi/PdToMidiAdapter.java | 82 +++++++----- 3 files changed, 121 insertions(+), 82 deletions(-) diff --git a/PdCore/build.gradle b/PdCore/build.gradle index feb45c59..c1ccca55 100644 --- a/PdCore/build.gradle +++ b/PdCore/build.gradle @@ -9,8 +9,6 @@ archivesBaseName = 'pd-core' version = rootProject.version dependencies { - api 'com.noisepages.nettoyeur:midi:1.0.0-rc1' - implementation 'com.noisepages.nettoyeur:midi:1.0.0-rc1' implementation 'androidx.legacy:legacy-support-v4:' + rootProject.androidxLegacySupportVersion } diff --git a/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java b/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java index 7c9662a2..ad6a0ea6 100644 --- a/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java +++ b/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java @@ -8,61 +8,88 @@ package org.puredata.android.midi; import org.puredata.core.PdBase; - -import com.noisepages.nettoyeur.midi.MidiReceiver; +import android.media.midi.MidiReceiver; /** * Adapter class for connecting output from AndroidMidi to MIDI input for Pd. * * @author Peter Brinkmann (peter.brinkmann@gmail.com) + * @author Antoine Rousseau (antoine@metalu.net) */ -public class MidiToPdAdapter implements MidiReceiver { - - @Override - public void onRawByte(byte value) { - PdBase.sendMidiByte(0, value); - } - - @Override - public void onProgramChange(int channel, int program) { - PdBase.sendProgramChange(channel, program); - } - - @Override - public void onPolyAftertouch(int channel, int key, int velocity) { - PdBase.sendPolyAftertouch(channel, key, velocity); - } - - @Override - public void onPitchBend(int channel, int value) { - PdBase.sendPitchBend(channel, value); +public class MidiToPdAdapter extends MidiReceiver { + private static enum State { + NOTE_OFF, NOTE_ON, POLY_TOUCH, CONTROL_CHANGE, PROGRAM_CHANGE, AFTERTOUCH, PITCH_BEND, NONE } + private State midiState = State.NONE; + private int channel; + private int firstByte; @Override - public void onNoteOn(int channel, int key, int velocity) { - PdBase.sendNoteOn(channel, key, velocity); + public void onSend(byte[] msg, int offset, int count, long timestamp) { + while(count-- != 0) processByte(msg[offset++]); } - @Override - public void onNoteOff(int channel, int key, int velocity) { - PdBase.sendNoteOn(channel, key, 0); - } - - @Override - public void onControlChange(int channel, int controller, int value) { - PdBase.sendControlChange(channel, controller, value); - } - - @Override - public void onAftertouch(int channel, int velocity) { - PdBase.sendAftertouch(channel, velocity); - } - - @Override - public boolean beginBlock() { - return false; + private void processByte(int b) { + if (b < 0) { + midiState = State.values()[(b >> 4) & 0x07]; + if (midiState != State.NONE) { + channel = b & 0x0f; + firstByte = -1; + } else { + PdBase.sendMidiByte(0, b); + } + } else { + switch (midiState) { + case NOTE_OFF: + if (firstByte < 0) { + firstByte = b; + } else { + PdBase.sendNoteOn(channel, firstByte, 0); + firstByte = -1; + } + break; + case NOTE_ON: + if (firstByte < 0) { + firstByte = b; + } else { + PdBase.sendNoteOn(channel, firstByte, b); + firstByte = -1; + } + break; + case POLY_TOUCH: + if (firstByte < 0) { + firstByte = b; + } else { + PdBase.sendPolyAftertouch(channel, firstByte, b); + firstByte = -1; + } + break; + case CONTROL_CHANGE: + if (firstByte < 0) { + firstByte = b; + } else { + PdBase.sendControlChange(channel, firstByte, b); + firstByte = -1; + } + break; + case PROGRAM_CHANGE: + PdBase.sendProgramChange(channel, b); + break; + case AFTERTOUCH: + PdBase.sendAftertouch(channel, b); + break; + case PITCH_BEND: + if (firstByte < 0) { + firstByte = b; + } else { + PdBase.sendPitchBend(channel, ((b << 7) | firstByte) - 8192); + firstByte = -1; + } + break; + default /* State.NONE */: + PdBase.sendMidiByte(0, b); + break; + } + } } - - @Override - public void endBlock() {} -} \ No newline at end of file +} diff --git a/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java b/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java index 01f7f289..120a463c 100644 --- a/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java +++ b/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java @@ -8,70 +8,84 @@ package org.puredata.android.midi; import org.puredata.core.PdMidiReceiver; - -import com.noisepages.nettoyeur.midi.MidiReceiver; +import android.media.midi.MidiInputPort; /** * Adapter class for connecting MIDI output from Pd to input for AndroidMidi. * * @author Peter Brinkmann (peter.brinkmann@gmail.com) + * @author Antoine Rousseau (antoine@metalu.net) */ public class PdToMidiAdapter implements PdMidiReceiver { + private final MidiInputPort inputPort; - private final MidiReceiver receiver; - - /** - * Constructor. Note that instances of this class still need to be installed with - * PdBase.setMidiReceiver. - * - * @param receiver to forward MIDI messages to - */ - public PdToMidiAdapter(MidiReceiver receiver) { - this.receiver = receiver; + public PdToMidiAdapter(MidiInputPort inputPort) { + this.inputPort = inputPort; } - + @Override - public void receiveProgramChange(int channel, int value) { - receiver.onProgramChange(channel, value); + public void receiveNoteOn(int channel, int pitch, int velocity) { + write(0x90, channel, pitch, velocity); } - + @Override public void receivePolyAftertouch(int channel, int pitch, int value) { - receiver.onPolyAftertouch(channel, pitch, value); + write(0xa0, channel, pitch, value); } - + @Override - public void receivePitchBend(int channel, int value) { - receiver.onPitchBend(channel, value); + public void receiveControlChange(int channel, int controller, int value) { + write(0xb0, channel, controller, value); } - + @Override - public void receiveNoteOn(int channel, int pitch, int velocity) { - receiver.onNoteOn(channel, pitch, velocity); + public void receiveProgramChange(int channel, int program) { + write(0xc0, channel, program); } - + @Override - public void receiveMidiByte(int port, int value) { - receiver.onRawByte((byte) value); + public void receiveAftertouch(int channel, int value) { + write(0xd0, channel, value); } - + @Override - public void receiveControlChange(int channel, int controller, int value) { - receiver.onControlChange(channel, controller, value); + public void receivePitchBend(int channel, int value) { + value += 8192; + write(0xe0, channel, (value & 0x7f), (value >> 7)); } - + @Override - public void receiveAftertouch(int channel, int value) { - receiver.onAftertouch(channel, value); + public void receiveMidiByte(int port, int value) { + final byte[] message = {(byte) value}; + writeMessage(message); + } + + private static byte firstByte(int msg, int ch) { + return (byte) (msg | (ch & 0x0f)); + } + + private void write(int msg, int ch, int a) { + final byte[] message = {firstByte(msg, ch), (byte) a}; + writeMessage(message); + } + + private void write(int msg, int ch, int a, int b) { + final byte[] message = {firstByte(msg, ch), (byte) a, (byte) b}; + writeMessage(message); + } + + private void writeMessage(byte[] message) { + try { + inputPort.send(message, 0, message.length); + } catch(Exception e) {} } @Override public boolean beginBlock() { - return receiver.beginBlock(); + return false; } @Override public void endBlock() { - receiver.endBlock(); } } From f824aecd20d3b9ca2339994e132756f25d84f802 Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Tue, 1 Jul 2025 16:58:37 +0200 Subject: [PATCH 02/11] Midi adapters: handle multiple devices, and javadoc-ument code --- .../android/midi/MidiToPdAdapter.java | 34 +++++++- .../android/midi/PdToMidiAdapter.java | 87 +++++++++++++++++-- 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java b/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java index ad6a0ea6..7daa6f3d 100644 --- a/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java +++ b/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java @@ -17,6 +17,7 @@ * @author Antoine Rousseau (antoine@metalu.net) */ public class MidiToPdAdapter extends MidiReceiver { + private final int port; private static enum State { NOTE_OFF, NOTE_ON, POLY_TOUCH, CONTROL_CHANGE, PROGRAM_CHANGE, AFTERTOUCH, PITCH_BEND, NONE } @@ -24,6 +25,37 @@ private static enum State { private int channel; private int firstByte; +/** + * Create an adapter for a specific port, to connect to a MidiOutputPort + * @param port starting at 0; Midi messages sent to Pd will have the channel increased by (16 * port). + *

+ * Example code: + *
{@code 
+MidiManager midiManager = (MidiManager) getSystemService(MIDI_SERVICE);
+final MidiDeviceInfo[] infos = midiManager.getDevices();
+if (infos.length == 0) return;
+final MidiDeviceInfo info = infos[0]; // Select the first available device
+midiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+	@Override
+	public void onDeviceOpened(MidiDevice device) {
+		if (device == null) return;
+		for (MidiDeviceInfo.PortInfo portInfo : device.getInfo().getPorts()) {
+			if (portInfo.getType() == MidiDeviceInfo.PortInfo.TYPE_OUTPUT) {
+				MidiOutputPort outputPort = device.openOutputPort(portInfo.getPortNumber());
+				if (outputPort != null) {
+					outputPort.connect(new MidiToPdAdapter(0)); // Map the device to Pd Midi port 0 (channel 0-15)
+					break; // Only connect to the first available output port
+				}
+			}
+		}
+	}
+}
+ * }
+ */ + public MidiToPdAdapter(int port) { + this.port = port; + } + @Override public void onSend(byte[] msg, int offset, int count, long timestamp) { while(count-- != 0) processByte(msg[offset++]); @@ -33,7 +65,7 @@ private void processByte(int b) { if (b < 0) { midiState = State.values()[(b >> 4) & 0x07]; if (midiState != State.NONE) { - channel = b & 0x0f; + channel = b & 0x0f + 16 * port; firstByte = -1; } else { PdBase.sendMidiByte(0, b); diff --git a/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java b/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java index 120a463c..03743c2d 100644 --- a/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java +++ b/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java @@ -9,6 +9,9 @@ import org.puredata.core.PdMidiReceiver; import android.media.midi.MidiInputPort; +import java.util.Map; +import java.util.HashMap; +import java.util.Map.Entry; /** * Adapter class for connecting MIDI output from Pd to input for AndroidMidi. @@ -17,47 +20,112 @@ * @author Antoine Rousseau (antoine@metalu.net) */ public class PdToMidiAdapter implements PdMidiReceiver { - private final MidiInputPort inputPort; + private Map inputPorts = new HashMap(); - public PdToMidiAdapter(MidiInputPort inputPort) { - this.inputPort = inputPort; +/** + * Send Midi messages from Pd to a Midi device + * @param inputPort input port of the Midi ouput device, as returned by MidiDevice.openInputPort() + * @param pdPort starting at 0; Midi messages received from Pd whose channel is between + * (16 * pdPort) and (16 * pdPort + 15) will be sent to the device with the channel reduced by (16 * pdPort) + *

+ * Example code: + *
{@code 
+MidiManager midiManager = (MidiManager) getSystemService(MIDI_SERVICE);
+final MidiDeviceInfo[] infos = midiManager.getDevices();
+if (infos.length == 0) return;
+final MidiDeviceInfo info = infos[0]; // Select the first available device
+midiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+	@Override
+	public void onDeviceOpened(MidiDevice device) {
+		if (device == null) return;
+		for (MidiDeviceInfo.PortInfo portInfo : device.getInfo().getPorts()) {
+			if (portInfo.getType() == MidiDeviceInfo.PortInfo.TYPE_INPUT) {
+				MidiInputPort inputPort = device.openInputPort(portInfo.getPortNumber());
+				if (inputPort != null) {
+					pdToMidiAdapter.open(inputPort, 0); // Map the device to Pd Midi port 0 (channel 0-15)
+					break; // Only connect to the first available input port
+				}
+			}
+		}
+	}
+}
+ * }
+ */ + public void open(MidiInputPort inputPort, int pdPort) { + close(inputPort); + close(pdPort); + inputPorts.put(pdPort, inputPort); + } + +/** + * Close the connection to a device port + * @param inputPort input port of the Midi ouput device, that needs to be closed + */ + public void close(MidiInputPort inputPort) { + if(! inputPorts.containsValue(inputPort)) return; + for (Entry entry : inputPorts.entrySet()) { + if (entry.getValue().equals(inputPort)) { + close(entry.getKey()); + } + } + } + +/** + * Close the connection from a Pd Midi port + * @param pdPort starting at 0; Midi messages coming from Pd for this port will be ignored, and the associated MidiInputPort will be closed + */ + public void close(int pdPort) { + MidiInputPort inputPort = inputPorts.get(pdPort); + if(inputPort != null) { + try { + inputPort.close(); + } catch(Exception e) {} + inputPorts.remove(pdPort); + } } +/** @hidden to javadoc*/ @Override public void receiveNoteOn(int channel, int pitch, int velocity) { write(0x90, channel, pitch, velocity); } +/** @hidden to javadoc*/ @Override public void receivePolyAftertouch(int channel, int pitch, int value) { write(0xa0, channel, pitch, value); } +/** @hidden to javadoc*/ @Override public void receiveControlChange(int channel, int controller, int value) { write(0xb0, channel, controller, value); } +/** @hidden to javadoc*/ @Override public void receiveProgramChange(int channel, int program) { write(0xc0, channel, program); } +/** @hidden to javadoc*/ @Override public void receiveAftertouch(int channel, int value) { write(0xd0, channel, value); } +/** @hidden to javadoc*/ @Override public void receivePitchBend(int channel, int value) { value += 8192; write(0xe0, channel, (value & 0x7f), (value >> 7)); } +/** @hidden to javadoc*/ @Override public void receiveMidiByte(int port, int value) { final byte[] message = {(byte) value}; - writeMessage(message); + writeMessage(port, message); } private static byte firstByte(int msg, int ch) { @@ -66,25 +134,28 @@ private static byte firstByte(int msg, int ch) { private void write(int msg, int ch, int a) { final byte[] message = {firstByte(msg, ch), (byte) a}; - writeMessage(message); + writeMessage(ch, message); } private void write(int msg, int ch, int a, int b) { final byte[] message = {firstByte(msg, ch), (byte) a, (byte) b}; - writeMessage(message); + writeMessage(ch, message); } - private void writeMessage(byte[] message) { - try { + private void writeMessage(int channel, byte[] message) { + MidiInputPort inputPort = inputPorts.get(channel / 16); + if(inputPort != null) try { inputPort.send(message, 0, message.length); } catch(Exception e) {} } +/** @hidden to javadoc*/ @Override public boolean beginBlock() { return false; } +/** @hidden to javadoc*/ @Override public void endBlock() { } From 660f15750c7cb34dda481eab28a2737887438dbe Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Thu, 3 Jul 2025 17:27:06 +0200 Subject: [PATCH 03/11] PdCore: add a 'Verbose [on/off]' preference --- .../src/main/java/org/puredata/android/service/PdService.java | 2 ++ PdCore/src/main/res/values/strings.xml | 3 +++ PdCore/src/main/res/xml/preferences.xml | 2 ++ 3 files changed, 7 insertions(+) diff --git a/PdCore/src/main/java/org/puredata/android/service/PdService.java b/PdCore/src/main/java/org/puredata/android/service/PdService.java index db52ca02..adf2e3a0 100644 --- a/PdCore/src/main/java/org/puredata/android/service/PdService.java +++ b/PdCore/src/main/java/org/puredata/android/service/PdService.java @@ -143,6 +143,8 @@ public synchronized void initAudio(int srate, int nic, int noc, float millis) th inputChannels = nic; outputChannels = noc; bufferSizeMillis = millis; + boolean verbose = prefs.getBoolean(res.getString(R.string.pref_key_verbose), false); + PdBase.setVerbose(verbose); } /** diff --git a/PdCore/src/main/res/values/strings.xml b/PdCore/src/main/res/values/strings.xml index 2aa223d6..26d9d507 100644 --- a/PdCore/src/main/res/values/strings.xml +++ b/PdCore/src/main/res/values/strings.xml @@ -11,4 +11,7 @@ OUTPUT_CHANNELS Output channels Number of output channels + PD_VERBOSE + Verbose + Increase Pd loglevel diff --git a/PdCore/src/main/res/xml/preferences.xml b/PdCore/src/main/res/xml/preferences.xml index 5a0dd393..c33c0714 100644 --- a/PdCore/src/main/res/xml/preferences.xml +++ b/PdCore/src/main/res/xml/preferences.xml @@ -11,4 +11,6 @@ + From 0eb78b244a933be6a3340ea6314d1562e959de4c Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Sat, 12 Jul 2025 19:22:33 +0200 Subject: [PATCH 04/11] upgrade android tools AGP 8.11.0 gradle 8.13 buildToolsVersion 35.0.0 ndk 27.2.12479018 --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 159f4ba2..dc27539b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.11.0' } } @@ -41,7 +41,7 @@ allprojects { ext { minSdkVersion = 28 compileSdkVersion = 34 - buildToolsVersion = '34.0.0' + buildToolsVersion = '35.0.0' androidxLegacySupportVersion = '1.0.0' - ndkVersion = '25.2.9519653' // https://developer.android.com/ndk/downloads#lts-downloads + ndkVersion = "27.2.12479018" // https://developer.android.com/ndk/downloads#lts-downloads } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 42defcc9..c6f00302 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 301dce875888c8798f42f6d7637ffa2c483d817b Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Tue, 1 Jul 2025 17:41:43 +0200 Subject: [PATCH 05/11] fix javadoc and lint errors --- PdCore/AndroidManifest.xml | 1 + .../puredata/android/io/AudioRecordWrapper.java | 16 ++++++++++------ .../org/puredata/android/io/AudioWrapper.java | 2 +- .../org/puredata/android/service/PdService.java | 3 +-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/PdCore/AndroidManifest.xml b/PdCore/AndroidManifest.xml index a647310b..4390c57c 100644 --- a/PdCore/AndroidManifest.xml +++ b/PdCore/AndroidManifest.xml @@ -1,4 +1,5 @@ + diff --git a/PdCore/src/main/java/org/puredata/android/io/AudioRecordWrapper.java b/PdCore/src/main/java/org/puredata/android/io/AudioRecordWrapper.java index a28bf667..32aa3d94 100644 --- a/PdCore/src/main/java/org/puredata/android/io/AudioRecordWrapper.java +++ b/PdCore/src/main/java/org/puredata/android/io/AudioRecordWrapper.java @@ -28,7 +28,7 @@ public class AudioRecordWrapper { private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; - private final AudioRecord rec; + private AudioRecord rec = null; private final int bufSizeShorts; private final BlockingQueue queue = new SynchronousQueue(); private Thread inputThread = null; @@ -43,14 +43,17 @@ public AudioRecordWrapper(int sampleRate, int inChannels, int bufferSizePerChann throw new IOException("bad AudioRecord parameters; sr: " + sampleRate + ", ch: " + inChannels + ", bufSize: " + bufferSizePerChannel); } while (recSizeBytes < minRecSizeBytes) recSizeBytes += bufSizeBytes; - rec = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, ENCODING, recSizeBytes); - if (rec != null && rec.getState() != AudioRecord.STATE_INITIALIZED) { - rec.release(); - throw new IOException("unable to initialize AudioRecord instance for sr: " + sampleRate + ", ch: " + inChannels + ", bufSize: " + bufferSizePerChannel); - } + try{ + rec = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, ENCODING, recSizeBytes); + if (rec != null && rec.getState() != AudioRecord.STATE_INITIALIZED) { + rec.release(); + throw new IOException("unable to initialize AudioRecord instance for sr: " + sampleRate + ", ch: " + inChannels + ", bufSize: " + bufferSizePerChannel); + } + } catch(SecurityException e) {} } public synchronized void start() { + if (rec == null) return; inputThread = new Thread() { @Override public void run() { @@ -91,6 +94,7 @@ public synchronized void stop() { } public synchronized void release() { + if (rec == null) return; stop(); rec.release(); queue.clear(); diff --git a/PdCore/src/main/java/org/puredata/android/io/AudioWrapper.java b/PdCore/src/main/java/org/puredata/android/io/AudioWrapper.java index 2c259457..f5de50c9 100644 --- a/PdCore/src/main/java/org/puredata/android/io/AudioWrapper.java +++ b/PdCore/src/main/java/org/puredata/android/io/AudioWrapper.java @@ -9,7 +9,7 @@ import java.io.IOException; -import org.puredata.android.service.R; +import org.puredata.android.service.*; import org.puredata.android.utils.Properties; import android.annotation.TargetApi; diff --git a/PdCore/src/main/java/org/puredata/android/service/PdService.java b/PdCore/src/main/java/org/puredata/android/service/PdService.java index adf2e3a0..18dc6c42 100644 --- a/PdCore/src/main/java/org/puredata/android/service/PdService.java +++ b/PdCore/src/main/java/org/puredata/android/service/PdService.java @@ -25,7 +25,6 @@ import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; -import androidx.core.app.NotificationCompat; import android.util.Log; import java.io.File; @@ -244,7 +243,7 @@ private Notification makeNotification(Intent intent, int icon, String title, Str } PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE); - return new NotificationCompat.Builder(PdService.this, TAG) + return new Notification.Builder(PdService.this, TAG) .setSmallIcon(icon) .setContentTitle(title) .setTicker(title) From 2e2d6f76d3e5759e0f933a152e60e743b158eabe Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Wed, 2 Jul 2025 13:39:31 +0200 Subject: [PATCH 06/11] modernize and simplify ndk build task --- PdCore/build.gradle | 49 +++++++++------------------------------------ 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/PdCore/build.gradle b/PdCore/build.gradle index c1ccca55..0aef954a 100644 --- a/PdCore/build.gradle +++ b/PdCore/build.gradle @@ -29,8 +29,6 @@ android { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/jni/libpd/java'] - jniLibs.srcDir 'src/main/libs' //set .so files location to libs - jni.srcDirs = [] //disable automatic ndk-build call res.srcDirs = ['src/main/res'] assets.srcDirs = ['assets'] } @@ -45,34 +43,19 @@ android { release.setRoot('build-types/release') } - tasks.create(name: 'buildNative', type: Exec, description: 'Compile JNI source via NDK') { - commandLine ndkBuildExecutablePath, - '-C', file('src/main/jni').absolutePath, - '-j', Runtime.runtime.availableProcessors(), - 'all', - 'NDK_DEBUG=1' - } - - // After ndk-build, copy libexpr.so to libexpr_tilde.so and libfexpr_tilde.so - buildNative.doLast { - def src = 'libexpr.so' - file('src/main/libs').eachDir() { dir -> - println "Cloning $src in $dir" - copy { from(dir) into(dir) include(src) rename(src, 'libexpr_tilde.so') } - copy { from(dir) into(dir) include(src) rename(src, 'libfexpr_tilde.so') } + defaultConfig { + externalNativeBuild { + ndkBuild { + arguments "NDK_DEBUG=1", "-j" + Runtime.runtime.availableProcessors() + cFlags "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang" + } } } - tasks.create(name: 'cleanNative', type: Exec, description: 'Clean JNI object files') { - commandLine ndkBuildExecutablePath, '-C', file('src/main/jni').absolutePath, 'clean' - } - - clean.configure { - dependsOn tasks.named('cleanNative') - } - - tasks.withType(JavaCompile).configureEach { - dependsOn tasks.named('buildNative') + externalNativeBuild { + ndkBuild { + path "src/main/jni/Android.mk" + } } libraryVariants.all { variant -> @@ -84,18 +67,6 @@ android { import org.apache.tools.ant.taskdefs.condition.Os -// TODO: Move to convention plugin? -def getNdkBuildExecutablePath() { - // android.ndkDirectory should return project.android.ndkVersion ndkDirectory - def ndkDir = android.ndkDirectory.absolutePath - def ndkBuildName = Os.isFamily(Os.FAMILY_WINDOWS) ? 'ndk-build.cmd' : 'ndk-build' - def ndkBuildFullPath = new File(ndkDir, ndkBuildName).getAbsolutePath() - if (!new File(ndkBuildFullPath).canExecute()) { - throw new GradleScriptException("ndk-build executable not found: $ndkBuildFullPath") - } - return ndkBuildFullPath -} - task sourcesJar(type: Jar) { archiveClassifier.set('sources') from android.sourceSets.main.java.srcDirs From cb8eb552a5ca33271203d9b6ffeb232a299160d5 Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Mon, 14 Jul 2025 11:57:13 +0200 Subject: [PATCH 07/11] split PdCore to seperate project (composite build) this allows to make apps independent from PdCore sources, so they can declare pd-core dependency like a maven one. --- .gitmodules | 2 +- PdCore/build.gradle | 189 +++--------------- PdCore/gradle.properties | 1 + PdCore/{ => pd-core}/AndroidManifest.xml | 0 PdCore/{ => pd-core}/LICENSE.txt | 0 PdCore/pd-core/build.gradle | 187 +++++++++++++++++ .../puredata/android/io/AudioFormatUtil.java | 0 .../puredata/android/io/AudioParameters.java | 0 .../android/io/AudioRecordWrapper.java | 0 .../org/puredata/android/io/AudioWrapper.java | 0 .../java/org/puredata/android/io/PdAudio.java | 0 .../android/midi/MidiToPdAdapter.java | 0 .../android/midi/PdToMidiAdapter.java | 0 .../android/service/PdPreferences.java | 0 .../puredata/android/service/PdService.java | 0 .../android/utils/PdUiDispatcher.java | 0 .../puredata/android/utils/Properties.java | 0 PdCore/{ => pd-core}/src/main/jni/Android.mk | 0 .../{ => pd-core}/src/main/jni/Application.mk | 0 PdCore/pd-core/src/main/jni/libpd | 1 + .../src/main/res/drawable/icon.png | Bin .../src/main/res/raw/extra_abs.zip | Bin .../src/main/res/raw/silence.wav | Bin .../src/main/res/values/audio.xml | 0 .../src/main/res/values/strings.xml | 0 .../src/main/res/values/styles.xml | 0 .../src/main/res/xml/preferences.xml | 0 PdCore/settings.gradle | 1 + PdCore/src/main/jni/libpd | 1 - build.gradle | 25 +-- settings.gradle | 5 +- 31 files changed, 227 insertions(+), 185 deletions(-) create mode 100644 PdCore/gradle.properties rename PdCore/{ => pd-core}/AndroidManifest.xml (100%) rename PdCore/{ => pd-core}/LICENSE.txt (100%) create mode 100644 PdCore/pd-core/build.gradle rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/io/AudioFormatUtil.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/io/AudioParameters.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/io/AudioRecordWrapper.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/io/AudioWrapper.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/io/PdAudio.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/service/PdPreferences.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/service/PdService.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/utils/PdUiDispatcher.java (100%) rename PdCore/{ => pd-core}/src/main/java/org/puredata/android/utils/Properties.java (100%) rename PdCore/{ => pd-core}/src/main/jni/Android.mk (100%) rename PdCore/{ => pd-core}/src/main/jni/Application.mk (100%) create mode 160000 PdCore/pd-core/src/main/jni/libpd rename PdCore/{ => pd-core}/src/main/res/drawable/icon.png (100%) rename PdCore/{ => pd-core}/src/main/res/raw/extra_abs.zip (100%) rename PdCore/{ => pd-core}/src/main/res/raw/silence.wav (100%) rename PdCore/{ => pd-core}/src/main/res/values/audio.xml (100%) rename PdCore/{ => pd-core}/src/main/res/values/strings.xml (100%) rename PdCore/{ => pd-core}/src/main/res/values/styles.xml (100%) rename PdCore/{ => pd-core}/src/main/res/xml/preferences.xml (100%) create mode 100644 PdCore/settings.gradle delete mode 160000 PdCore/src/main/jni/libpd diff --git a/.gitmodules b/.gitmodules index 241be1c1..f04915a4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "PdCore/src/main/jni/libpd"] - path = PdCore/src/main/jni/libpd + path = PdCore/pd-core/src/main/jni/libpd url = https://github.com/libpd/libpd.git diff --git a/PdCore/build.gradle b/PdCore/build.gradle index 0aef954a..2efef44d 100644 --- a/PdCore/build.gradle +++ b/PdCore/build.gradle @@ -1,172 +1,45 @@ -plugins { - id 'com.android.library' - id 'signing' - id 'maven-publish' -} - -group = rootProject.group -archivesBaseName = 'pd-core' -version = rootProject.version - -dependencies { - implementation 'androidx.legacy:legacy-support-v4:' + rootProject.androidxLegacySupportVersion -} - -android { - compileSdkVersion rootProject.compileSdkVersion - buildToolsVersion rootProject.buildToolsVersion - ndkVersion rootProject.ndkVersion - namespace = 'org.puredata.android.service' - - defaultConfig { - minSdkVersion rootProject.minSdkVersion - targetSdkVersion 33 - versionCode 1 - versionName version - } - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src/main/java', 'src/main/jni/libpd/java'] - res.srcDirs = ['src/main/res'] - assets.srcDirs = ['assets'] - } - - // Move the build types to build-types/ - // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ... - // This moves them out of them default location under src//... which would - // conflict with src/ being used by the main source set. - // Adding new build types or product flavors should be accompanied - // by a similar customization. - debug.setRoot('build-types/debug') - release.setRoot('build-types/release') - } - - defaultConfig { - externalNativeBuild { - ndkBuild { - arguments "NDK_DEBUG=1", "-j" + Runtime.runtime.availableProcessors() - cFlags "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang" - } - } +buildscript { + repositories { + google() + mavenCentral() } - - externalNativeBuild { - ndkBuild { - path "src/main/jni/Android.mk" - } + dependencies { + classpath 'com.android.tools.build:gradle:8.11.0' } - - libraryVariants.all { variant -> - variant.outputs.all { output -> - outputFileName = "${archivesBaseName}.aar" - } - } -} - -import org.apache.tools.ant.taskdefs.condition.Os - -task sourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - failOnError = false // TODO: fix javadoc issues -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - archiveClassifier.set('javadoc') - from javadoc.destinationDir } -artifacts { - archives javadocJar - archives sourcesJar +plugins { + // must be applied to root project + id 'io.github.gradle-nexus.publish-plugin' version '1.0.0' } -def siteUrl = 'https://github.com/libpd/pd-for-android' - -publishing { - publications { - maven(MavenPublication) { - groupId group - artifactId archivesBaseName - version version - // TODO: include aar artifact from components? - artifact "${buildDir}/outputs/aar/${archivesBaseName}.aar" - // ossrh requires javadoc and sources - artifact sourcesJar - artifact javadocJar - - pom { - name = "${project.group}:${project.archivesBaseName}" - description = 'Pure Data for Android' - url = siteUrl - licenses { - license { - name = 'BSD New' - url = 'https://raw.githubusercontent.com/libpd/pd-for-android/master/PdCore/LICENSE.txt' - } - } - developers { - developer { - id = 'joebowbeer' - name = 'Joe Bowbeer' - } - // TODO: Add all other devs here... - } - scm { - connection = 'scm:git:https://github.com/libpd/pd-for-android' - developerConnection = 'scm:git:ssh://github.com/libpd/pd-for-android.git' - url = siteUrl - } - } - } +allprojects { + repositories { + google() + mavenCentral() } } -// configure publishing to a local directory for testing (not necessary) -// ./gradlew publishMavenPublicationToLocalRepository -// tree ./PdCore/build/repos -publishing { +// These are specific to PdCore, but nexusPublishing needs them here: +// https://github.com/gradle-nexus/publish-plugin/issues/84 +group = 'io.github.libpd.android' +version = '1.3.0' + +// Create a Sonatype user token for these environment variables: +// export ORG_GRADLE_PROJECT_sonatypeUsername="" +// export ORG_GRADLE_PROJECT_sonatypePassword="" +nexusPublishing { repositories { - maven { - name = 'local' - def releasesRepoUrl = "$buildDir/repos/releases" - def snapshotsRepoUrl = "$buildDir/repos/snapshots" - url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl - } + sonatype { + nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/')) + snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/')) + } } } -// ossrh requires signed releases, but not snapshots. -// This configures signing if a key is found. -// The following environment variables provide a signing key and passphrase: -// export ORG_GRADLE_PROJECT_signingKey=`cat private.pgp` -// export ORG_GRADLE_PROJECT_signingPassword="" -// After making the above available, you can try signing using -// ./gradlew signMavenPublication -def hasSigningKey = project.hasProperty('signingKeyId') || project.hasProperty('signingKey') -if (hasSigningKey) { - sign(project) -} -void sign(Project project) { - project.signing { - required { project.gradle.taskGraph.hasTask('required') } - def signingKeyId = project.findProperty('signingKeyId') - def signingKey = project.findProperty('signingKey') - def signingPassword = project.findProperty('signingPassword') - if (signingKeyId) { - // use in-memory ascii-armored OpenPGP subkey - useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) - } else if (signingKey) { - // use in-memory ascii-armored key - useInMemoryPgpKeys(signingKey, signingPassword) - } - sign publishing.publications.maven - } +ext { + minSdkVersion = 28 + compileSdkVersion = 34 + androidxLegacySupportVersion = '1.0.0' + ndkVersion = "27.2.12479018" // https://developer.android.com/ndk/downloads#lts-downloads } diff --git a/PdCore/gradle.properties b/PdCore/gradle.properties new file mode 100644 index 00000000..2d8d1e4d --- /dev/null +++ b/PdCore/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file diff --git a/PdCore/AndroidManifest.xml b/PdCore/pd-core/AndroidManifest.xml similarity index 100% rename from PdCore/AndroidManifest.xml rename to PdCore/pd-core/AndroidManifest.xml diff --git a/PdCore/LICENSE.txt b/PdCore/pd-core/LICENSE.txt similarity index 100% rename from PdCore/LICENSE.txt rename to PdCore/pd-core/LICENSE.txt diff --git a/PdCore/pd-core/build.gradle b/PdCore/pd-core/build.gradle new file mode 100644 index 00000000..c33f6a89 --- /dev/null +++ b/PdCore/pd-core/build.gradle @@ -0,0 +1,187 @@ +plugins { + id 'com.android.library' + id 'signing' + id 'maven-publish' +} + +archivesBaseName = 'pd-core' +group = rootProject.group +version = rootProject.version + +dependencies { + implementation 'androidx.legacy:legacy-support-v4:' + rootProject.androidxLegacySupportVersion +} + +android { + compileSdkVersion rootProject.compileSdkVersion + ndkVersion rootProject.ndkVersion + + namespace = 'org.puredata.android.service' + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.compileSdkVersion + versionCode 1 + versionName version + } + + buildFeatures { + prefabPublishing true + } + + prefab { + pd { + headers "src/main/jni/libpd/pure-data/src" + } + } + + packagingOptions { + //exclude("**/*.so") + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src/main/java', 'src/main/jni/libpd/java'] + res.srcDirs = ['src/main/res'] + assets.srcDirs = ['assets'] + } + + // Move the build types to build-types/ + // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ... + // This moves them out of them default location under src//... which would + // conflict with src/ being used by the main source set. + // Adding new build types or product flavors should be accompanied + // by a similar customization. + debug.setRoot('build-types/debug') + release.setRoot('build-types/release') + } + + defaultConfig { + externalNativeBuild { + ndkBuild { + arguments "NDK_DEBUG=1", "-j" + Runtime.runtime.availableProcessors() + cFlags "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang" + } + } + } + + externalNativeBuild { + ndkBuild { + path "src/main/jni/Android.mk" + } + } + + android.libraryVariants.all { variant -> + variant.outputs.all { output -> + output.outputFileName = "${archivesBaseName}.aar" + } + } +} + +import org.apache.tools.ant.taskdefs.condition.Os + +task sourcesJar(type: Jar) { + archiveClassifier.set('sources') + from android.sourceSets.main.java.srcDirs +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += files("${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar") + failOnError = false // TODO: fix javadoc issues +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier.set('javadoc') + from javadoc.destinationDir +} + +artifacts { + archives javadocJar + archives sourcesJar +} + +def siteUrl = 'https://github.com/libpd/pd-for-android' + +publishing { + publications { + maven(MavenPublication) { + groupId group + artifactId archivesBaseName + version version + // TODO: include aar artifact from components? + artifact "${buildDir}/outputs/aar/${archivesBaseName}.aar" + // ossrh requires javadoc and sources + artifact sourcesJar + artifact javadocJar + + pom { + name = "${project.group}:${project.archivesBaseName}" + description = 'Pure Data for Android' + url = siteUrl + licenses { + license { + name = 'BSD New' + url = 'https://raw.githubusercontent.com/libpd/pd-for-android/master/PdCore/LICENSE.txt' + } + } + developers { + developer { + id = 'joebowbeer' + name = 'Joe Bowbeer' + } + // TODO: Add all other devs here... + } + scm { + connection = 'scm:git:https://github.com/libpd/pd-for-android' + developerConnection = 'scm:git:ssh://github.com/libpd/pd-for-android.git' + url = siteUrl + } + } + } + } +} + +// configure publishing to a local directory for testing (not necessary) +// ./gradlew publishMavenPublicationToLocalRepository +// tree ./PdCore/build/repos +publishing { + repositories { + maven { + name = 'local' + def releasesRepoUrl = "$buildDir/repos/releases" + def snapshotsRepoUrl = "$buildDir/repos/snapshots" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + } + } +} + +// ossrh requires signed releases, but not snapshots. +// This configures signing if a key is found. +// The following environment variables provide a signing key and passphrase: +// export ORG_GRADLE_PROJECT_signingKey=`cat private.pgp` +// export ORG_GRADLE_PROJECT_signingPassword="" +// After making the above available, you can try signing using +// ./gradlew signMavenPublication +def hasSigningKey = project.hasProperty('signingKeyId') || project.hasProperty('signingKey') +if (hasSigningKey) { + sign(project) +} +void sign(Project project) { + project.signing { + required { project.gradle.taskGraph.hasTask('required') } + def signingKeyId = project.findProperty('signingKeyId') + def signingKey = project.findProperty('signingKey') + def signingPassword = project.findProperty('signingPassword') + if (signingKeyId) { + // use in-memory ascii-armored OpenPGP subkey + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + } else if (signingKey) { + // use in-memory ascii-armored key + useInMemoryPgpKeys(signingKey, signingPassword) + } + sign publishing.publications.maven + } +} + diff --git a/PdCore/src/main/java/org/puredata/android/io/AudioFormatUtil.java b/PdCore/pd-core/src/main/java/org/puredata/android/io/AudioFormatUtil.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/io/AudioFormatUtil.java rename to PdCore/pd-core/src/main/java/org/puredata/android/io/AudioFormatUtil.java diff --git a/PdCore/src/main/java/org/puredata/android/io/AudioParameters.java b/PdCore/pd-core/src/main/java/org/puredata/android/io/AudioParameters.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/io/AudioParameters.java rename to PdCore/pd-core/src/main/java/org/puredata/android/io/AudioParameters.java diff --git a/PdCore/src/main/java/org/puredata/android/io/AudioRecordWrapper.java b/PdCore/pd-core/src/main/java/org/puredata/android/io/AudioRecordWrapper.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/io/AudioRecordWrapper.java rename to PdCore/pd-core/src/main/java/org/puredata/android/io/AudioRecordWrapper.java diff --git a/PdCore/src/main/java/org/puredata/android/io/AudioWrapper.java b/PdCore/pd-core/src/main/java/org/puredata/android/io/AudioWrapper.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/io/AudioWrapper.java rename to PdCore/pd-core/src/main/java/org/puredata/android/io/AudioWrapper.java diff --git a/PdCore/src/main/java/org/puredata/android/io/PdAudio.java b/PdCore/pd-core/src/main/java/org/puredata/android/io/PdAudio.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/io/PdAudio.java rename to PdCore/pd-core/src/main/java/org/puredata/android/io/PdAudio.java diff --git a/PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java b/PdCore/pd-core/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java rename to PdCore/pd-core/src/main/java/org/puredata/android/midi/MidiToPdAdapter.java diff --git a/PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java b/PdCore/pd-core/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java rename to PdCore/pd-core/src/main/java/org/puredata/android/midi/PdToMidiAdapter.java diff --git a/PdCore/src/main/java/org/puredata/android/service/PdPreferences.java b/PdCore/pd-core/src/main/java/org/puredata/android/service/PdPreferences.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/service/PdPreferences.java rename to PdCore/pd-core/src/main/java/org/puredata/android/service/PdPreferences.java diff --git a/PdCore/src/main/java/org/puredata/android/service/PdService.java b/PdCore/pd-core/src/main/java/org/puredata/android/service/PdService.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/service/PdService.java rename to PdCore/pd-core/src/main/java/org/puredata/android/service/PdService.java diff --git a/PdCore/src/main/java/org/puredata/android/utils/PdUiDispatcher.java b/PdCore/pd-core/src/main/java/org/puredata/android/utils/PdUiDispatcher.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/utils/PdUiDispatcher.java rename to PdCore/pd-core/src/main/java/org/puredata/android/utils/PdUiDispatcher.java diff --git a/PdCore/src/main/java/org/puredata/android/utils/Properties.java b/PdCore/pd-core/src/main/java/org/puredata/android/utils/Properties.java similarity index 100% rename from PdCore/src/main/java/org/puredata/android/utils/Properties.java rename to PdCore/pd-core/src/main/java/org/puredata/android/utils/Properties.java diff --git a/PdCore/src/main/jni/Android.mk b/PdCore/pd-core/src/main/jni/Android.mk similarity index 100% rename from PdCore/src/main/jni/Android.mk rename to PdCore/pd-core/src/main/jni/Android.mk diff --git a/PdCore/src/main/jni/Application.mk b/PdCore/pd-core/src/main/jni/Application.mk similarity index 100% rename from PdCore/src/main/jni/Application.mk rename to PdCore/pd-core/src/main/jni/Application.mk diff --git a/PdCore/pd-core/src/main/jni/libpd b/PdCore/pd-core/src/main/jni/libpd new file mode 160000 index 00000000..d7d1e1ef --- /dev/null +++ b/PdCore/pd-core/src/main/jni/libpd @@ -0,0 +1 @@ +Subproject commit d7d1e1ef6259583065d7bd0b5b37112fa29d2eb6 diff --git a/PdCore/src/main/res/drawable/icon.png b/PdCore/pd-core/src/main/res/drawable/icon.png similarity index 100% rename from PdCore/src/main/res/drawable/icon.png rename to PdCore/pd-core/src/main/res/drawable/icon.png diff --git a/PdCore/src/main/res/raw/extra_abs.zip b/PdCore/pd-core/src/main/res/raw/extra_abs.zip similarity index 100% rename from PdCore/src/main/res/raw/extra_abs.zip rename to PdCore/pd-core/src/main/res/raw/extra_abs.zip diff --git a/PdCore/src/main/res/raw/silence.wav b/PdCore/pd-core/src/main/res/raw/silence.wav similarity index 100% rename from PdCore/src/main/res/raw/silence.wav rename to PdCore/pd-core/src/main/res/raw/silence.wav diff --git a/PdCore/src/main/res/values/audio.xml b/PdCore/pd-core/src/main/res/values/audio.xml similarity index 100% rename from PdCore/src/main/res/values/audio.xml rename to PdCore/pd-core/src/main/res/values/audio.xml diff --git a/PdCore/src/main/res/values/strings.xml b/PdCore/pd-core/src/main/res/values/strings.xml similarity index 100% rename from PdCore/src/main/res/values/strings.xml rename to PdCore/pd-core/src/main/res/values/strings.xml diff --git a/PdCore/src/main/res/values/styles.xml b/PdCore/pd-core/src/main/res/values/styles.xml similarity index 100% rename from PdCore/src/main/res/values/styles.xml rename to PdCore/pd-core/src/main/res/values/styles.xml diff --git a/PdCore/src/main/res/xml/preferences.xml b/PdCore/pd-core/src/main/res/xml/preferences.xml similarity index 100% rename from PdCore/src/main/res/xml/preferences.xml rename to PdCore/pd-core/src/main/res/xml/preferences.xml diff --git a/PdCore/settings.gradle b/PdCore/settings.gradle new file mode 100644 index 00000000..377573a0 --- /dev/null +++ b/PdCore/settings.gradle @@ -0,0 +1 @@ +include ':pd-core' diff --git a/PdCore/src/main/jni/libpd b/PdCore/src/main/jni/libpd deleted file mode 160000 index fb174959..00000000 --- a/PdCore/src/main/jni/libpd +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fb174959373d18d8bd135f6a3abf86ded82be534 diff --git a/build.gradle b/build.gradle index dc27539b..9a0a5fca 100644 --- a/build.gradle +++ b/build.gradle @@ -8,40 +8,17 @@ buildscript { } } -plugins { - // must be applied to root project - id 'io.github.gradle-nexus.publish-plugin' version '1.0.0' -} - -// These are specific to PdCode, but nexusPublishing needs them here: -// https://github.com/gradle-nexus/publish-plugin/issues/84 -group = 'io.github.libpd.android' -version = '1.2.1-SNAPSHOT' - -// Create a Sonatype user token for these environment variables: -// export ORG_GRADLE_PROJECT_sonatypeUsername="" -// export ORG_GRADLE_PROJECT_sonatypePassword="" -nexusPublishing { - repositories { - sonatype { - nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/')) - snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/')) - } - } -} - allprojects { repositories { google() mavenCentral() - jcenter() // FIXME: com.noisepages.nettoyeur:midi } } ext { minSdkVersion = 28 compileSdkVersion = 34 - buildToolsVersion = '35.0.0' androidxLegacySupportVersion = '1.0.0' ndkVersion = "27.2.12479018" // https://developer.android.com/ndk/downloads#lts-downloads + pdCoreVersion = '1.3.0' // Must match version in PdCore/build.gradle } diff --git a/settings.gradle b/settings.gradle index 7b433b2c..3d576f2b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,8 @@ +rootProject.name = 'pd-for-android' + +includeBuild 'PdCore' + include ':CircleOfFifths' -include ':PdCore' include ':PdTest' include ':ScenePlayer' include ':Voice-O-Rama' From c891749c9d1663ec35d67283bb63552a406fdf01 Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Mon, 14 Jul 2025 12:04:26 +0200 Subject: [PATCH 08/11] update apps build config, to use pd-core prefab, and refer to 'composite' style dependency --- CircleOfFifths/build.gradle | 7 ++-- PdTest/build.gradle | 63 +++++++++++++++------------------- PdTest/jni/Android.mk | 13 ++----- PdTest/jni/Application.mk | 4 +-- ScenePlayer/build.gradle | 54 +++++++++++++---------------- ScenePlayer/jni/Android.mk | 13 ++----- ScenePlayer/jni/Application.mk | 4 +-- Voice-O-Rama/build.gradle | 3 +- 8 files changed, 66 insertions(+), 95 deletions(-) diff --git a/CircleOfFifths/build.gradle b/CircleOfFifths/build.gradle index ddc476dd..b76e37f9 100644 --- a/CircleOfFifths/build.gradle +++ b/CircleOfFifths/build.gradle @@ -1,13 +1,12 @@ apply plugin: 'com.android.application' dependencies { - implementation project(':PdCore') + implementation 'io.github.libpd.android:pd-core:' + rootProject.pdCoreVersion } android { - compileSdkVersion rootProject.compileSdkVersion - buildToolsVersion rootProject.buildToolsVersion - ndkVersion rootProject.ndkVersion + compileSdkVersion = rootProject.compileSdkVersion + ndkVersion = rootProject.ndkVersion namespace = 'org.puredata.android.fifths' defaultConfig { diff --git a/PdTest/build.gradle b/PdTest/build.gradle index eac38f5d..3b3cd637 100644 --- a/PdTest/build.gradle +++ b/PdTest/build.gradle @@ -2,14 +2,8 @@ apply plugin: 'com.android.application' import org.apache.tools.ant.taskdefs.condition.Os -dependencies { - implementation project(':PdCore') - implementation 'androidx.legacy:legacy-support-v4:' + rootProject.androidxLegacySupportVersion -} - android { compileSdkVersion rootProject.compileSdkVersion - buildToolsVersion rootProject.buildToolsVersion ndkVersion rootProject.ndkVersion namespace = 'org.puredata.android.test' @@ -25,6 +19,14 @@ android { // } } + packagingOptions { + jniLibs { + // since externals are loaded at native code level, they need to be copied to the actual + // filesystem, instead of staying in the apk; so we must set useLegacyPackaging: + useLegacyPackaging true + } + } + buildTypes { release { minifyEnabled true @@ -32,12 +34,16 @@ android { } } + buildFeatures { + // Extract native libs from prefab, to be able to access the headers and to link to the libs + prefab true + viewBinding true + } + sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] - jniLibs.srcDir 'libs' //set .so files location to libs - jni.srcDirs = [] //disable automatic ndk-build call resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] @@ -55,36 +61,23 @@ android { release.setRoot('build-types/release') } - tasks.create(name: 'buildNative', type: Exec, description: 'Compile JNI source via NDK') { - commandLine ndkBuildExecutablePath, - 'V=1', - '-C', file('jni').absolutePath, - '-j', Runtime.runtime.availableProcessors(), - 'all', - 'NDK_DEBUG=1' - } - - tasks.create(name: 'cleanNative', type: Exec, description: 'Clean JNI object files') { - commandLine ndkBuildExecutablePath, 'V=1', '-C', file('jni').absolutePath, 'clean' - } - - clean.configure { - dependsOn tasks.named('cleanNative') + defaultConfig { + externalNativeBuild { + ndkBuild { + arguments "NDK_DEBUG=1", "-j" + Runtime.runtime.availableProcessors() + } + } } - tasks.withType(JavaCompile).configureEach { - dependsOn tasks.named('buildNative') + externalNativeBuild { + ndkBuild { + path "jni/Android.mk" + } } } -// TODO: Move to convention plugin? -def getNdkBuildExecutablePath() { - // android.ndkDirectory should return project.android.ndkVersion ndkDirectory - def ndkDir = android.ndkDirectory.absolutePath - def ndkBuildName = Os.isFamily(Os.FAMILY_WINDOWS) ? 'ndk-build.cmd' : 'ndk-build' - def ndkBuildFullPath = new File(ndkDir, ndkBuildName).getAbsolutePath() - if (!new File(ndkBuildFullPath).canExecute()) { - throw new GradleScriptException("ndk-build executable not found: $ndkBuildFullPath") - } - return ndkBuildFullPath +dependencies { + implementation 'androidx.legacy:legacy-support-v4:' + rootProject.androidxLegacySupportVersion + implementation 'io.github.libpd.android:pd-core:' + rootProject.pdCoreVersion } + diff --git a/PdTest/jni/Android.mk b/PdTest/jni/Android.mk index 8edb3d03..c4a1bff7 100644 --- a/PdTest/jni/Android.mk +++ b/PdTest/jni/Android.mk @@ -2,16 +2,6 @@ LOCAL_PATH := $(call my-dir) #--------------------------------------------------------------- -include $(CLEAR_VARS) -LOCAL_MODULE := pd -LOCAL_EXPORT_C_INCLUDES := ../../PdCore/src/main/jni/libpd/pure-data/src -LOCAL_SRC_FILES := ../../PdCore/src/main/libs/$(TARGET_ARCH_ABI)/libpd.so -ifneq ($(MAKECMDGOALS),clean) - include $(PREBUILT_SHARED_LIBRARY) -endif - -#--------------------------------------------------------------- - include $(CLEAR_VARS) LOCAL_MODULE := helloworld LOCAL_CFLAGS := -DPD @@ -20,3 +10,6 @@ LOCAL_SHARED_LIBRARIES = pd include $(BUILD_SHARED_LIBRARY) #--------------------------------------------------------------- + +$(call import-module,prefab/pd-core) + diff --git a/PdTest/jni/Application.mk b/PdTest/jni/Application.mk index 8732a49b..1dfcd802 100644 --- a/PdTest/jni/Application.mk +++ b/PdTest/jni/Application.mk @@ -1,4 +1,4 @@ -APP_PLATFORM := android-17 APP_OPTIM := release APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 -APP_ALLOW_MISSING_DEPS=true \ No newline at end of file +APP_ALLOW_MISSING_DEPS=true + diff --git a/ScenePlayer/build.gradle b/ScenePlayer/build.gradle index e40a7727..05e41f36 100644 --- a/ScenePlayer/build.gradle +++ b/ScenePlayer/build.gradle @@ -3,18 +3,17 @@ apply plugin: 'com.android.application' import org.apache.tools.ant.taskdefs.condition.Os dependencies { - implementation project(':PdCore') + implementation 'io.github.libpd.android:pd-core:' + rootProject.pdCoreVersion } android { compileSdkVersion rootProject.compileSdkVersion - buildToolsVersion rootProject.buildToolsVersion ndkVersion rootProject.ndkVersion namespace = 'org.puredata.android.scenes' defaultConfig { minSdkVersion rootProject.minSdkVersion - targetSdkVersion 28 + targetSdkVersion 33 versionCode 11 versionName "0.9.2" } @@ -23,8 +22,6 @@ android { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] - jniLibs.srcDir 'libs' //set .so files location to libs - jni.srcDirs = [] //disable automatic ndk-build call resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] @@ -42,39 +39,36 @@ android { release.setRoot('build-types/release') } - tasks.create(name: 'buildNative', type: Exec, description: 'Compile JNI source via NDK') { - commandLine ndkBuildExecutablePath, - '-C', file('jni').absolutePath, - '-j', Runtime.runtime.availableProcessors(), - 'all', - 'NDK_DEBUG=1' + lintOptions { + ignore 'ExpiredTargetSdkVersion' } - tasks.create(name: 'cleanNative', type: Exec, description: 'Clean JNI object files') { - commandLine ndkBuildExecutablePath, '-C', file('jni').absolutePath, 'clean' + packagingOptions { + jniLibs { + // since externals are loaded at native code level, they need to be copied to the actual + // filesystem, instead of staying in the apk; so we must set useLegacyPackaging: + useLegacyPackaging true + } } - clean.configure { - dependsOn tasks.named('cleanNative') + buildFeatures { + // Extract native libs from prefab, to be able to access the headers and to link to the libs + prefab = true + viewBinding = true } - tasks.withType(JavaCompile).configureEach { - dependsOn tasks.named('buildNative') + defaultConfig { + externalNativeBuild { + ndkBuild { + arguments "NDK_DEBUG=1", "-j" + Runtime.runtime.availableProcessors() + } + } } - lintOptions { - ignore 'ExpiredTargetSdkVersion' + externalNativeBuild { + ndkBuild { + path "jni/Android.mk" + } } } -// TODO: Move to convention plugin? -def getNdkBuildExecutablePath() { - // android.ndkDirectory should return project.android.ndkVersion ndkDirectory - def ndkDir = android.ndkDirectory.absolutePath - def ndkBuildName = Os.isFamily(Os.FAMILY_WINDOWS) ? 'ndk-build.cmd' : 'ndk-build' - def ndkBuildFullPath = new File(ndkDir, ndkBuildName).getAbsolutePath() - if (!new File(ndkBuildFullPath).canExecute()) { - throw new GradleScriptException("ndk-build executable not found: $ndkBuildFullPath") - } - return ndkBuildFullPath -} diff --git a/ScenePlayer/jni/Android.mk b/ScenePlayer/jni/Android.mk index 4ef3dec3..1833a659 100644 --- a/ScenePlayer/jni/Android.mk +++ b/ScenePlayer/jni/Android.mk @@ -2,16 +2,6 @@ LOCAL_PATH := $(call my-dir) #--------------------------------------------------------------- -include $(CLEAR_VARS) -LOCAL_MODULE := pd -LOCAL_EXPORT_C_INCLUDES := ../../PdCore/src/main/jni/libpd/pure-data/src -LOCAL_SRC_FILES := ../../PdCore/src/main/libs/$(TARGET_ARCH_ABI)/libpd.so -ifneq ($(MAKECMDGOALS),clean) - include $(PREBUILT_SHARED_LIBRARY) -endif - -#--------------------------------------------------------------- - include $(CLEAR_VARS) LOCAL_MODULE := rj_accum LOCAL_CFLAGS := -DPD @@ -56,3 +46,6 @@ LOCAL_SHARED_LIBRARIES = pd include $(BUILD_SHARED_LIBRARY) #--------------------------------------------------------------- + +$(call import-module,prefab/pd-core) + diff --git a/ScenePlayer/jni/Application.mk b/ScenePlayer/jni/Application.mk index 8732a49b..1dfcd802 100644 --- a/ScenePlayer/jni/Application.mk +++ b/ScenePlayer/jni/Application.mk @@ -1,4 +1,4 @@ -APP_PLATFORM := android-17 APP_OPTIM := release APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 -APP_ALLOW_MISSING_DEPS=true \ No newline at end of file +APP_ALLOW_MISSING_DEPS=true + diff --git a/Voice-O-Rama/build.gradle b/Voice-O-Rama/build.gradle index e1b50fe7..ead052eb 100644 --- a/Voice-O-Rama/build.gradle +++ b/Voice-O-Rama/build.gradle @@ -1,12 +1,11 @@ apply plugin: 'com.android.application' dependencies { - implementation project(':PdCore') + implementation 'io.github.libpd.android:pd-core:' + rootProject.pdCoreVersion } android { compileSdkVersion rootProject.compileSdkVersion - buildToolsVersion rootProject.buildToolsVersion namespace = 'at.or.at.voiceorama' defaultConfig { From 93b0a063fe034ca0638e64d63c8b8e7f90e24a3e Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Mon, 14 Jul 2025 12:05:17 +0200 Subject: [PATCH 09/11] update CI config for latest changes --- .github/workflows/android.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 92e2e986..936e8e8b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -26,15 +26,19 @@ jobs: git submodule sync --recursive git submodule update --init --recursive - run: ./gradlew androidDependencies - - run: ./gradlew clean assembleRelease + - run: ./gradlew clean + - run: ./gradlew PdCore:pd-core:build + env: + JVM_OPTS: -Xmx3200m + - run: ./gradlew assembleRelease env: JVM_OPTS: -Xmx3200m - uses: actions/upload-artifact@v4 with: name: pd-core-aar - path: PdCore/build/outputs/aar + path: PdCore/pd-core/build/outputs/aar - if: github.event_name == 'push' - run: ./gradlew publishToSonatype + run: ./gradlew :PdCore:pd-core:publishToSonatype env: ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }} ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }} From 20a9e5dba448fe23a62b2800b3c1cd270184ad0c Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Mon, 14 Jul 2025 14:24:18 +0200 Subject: [PATCH 10/11] clear last lint errors --- .../android/scenes/SceneDataBase.java | 12 +++++++--- .../puredata/android/scenes/ScenePlayer.java | 22 ++++++++++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/ScenePlayer/src/org/puredata/android/scenes/SceneDataBase.java b/ScenePlayer/src/org/puredata/android/scenes/SceneDataBase.java index eeaa967e..84be2d84 100644 --- a/ScenePlayer/src/org/puredata/android/scenes/SceneDataBase.java +++ b/ScenePlayer/src/org/puredata/android/scenes/SceneDataBase.java @@ -181,7 +181,9 @@ public static String getString(Cursor cursor, Column column) { } public static String getString(Cursor cursor, String column) { - return cursor.getString(cursor.getColumnIndex(column)); + int column_index = cursor.getColumnIndex(column); + if(column_index < 0) column_index = 0; + return cursor.getString(column_index); } public static long getLong(Cursor cursor, Column column) { @@ -189,7 +191,9 @@ public static long getLong(Cursor cursor, Column column) { } public static long getLong(Cursor cursor, String column) { - return cursor.getLong(cursor.getColumnIndex(column)); + int column_index = cursor.getColumnIndex(column); + if(column_index < 0) column_index = 0; + return cursor.getLong(column_index); } public static double getDouble(Cursor cursor, Column column) { @@ -197,7 +201,9 @@ public static double getDouble(Cursor cursor, Column column) { } public static double getDouble(Cursor cursor, String column) { - return cursor.getDouble(cursor.getColumnIndex(column)); + int column_index = cursor.getColumnIndex(column); + if(column_index < 0) column_index = 0; + return cursor.getDouble(column_index); } private static class SceneDataBaseHelper extends SQLiteOpenHelper { diff --git a/ScenePlayer/src/org/puredata/android/scenes/ScenePlayer.java b/ScenePlayer/src/org/puredata/android/scenes/ScenePlayer.java index d9751b11..643e58fd 100644 --- a/ScenePlayer/src/org/puredata/android/scenes/ScenePlayer.java +++ b/ScenePlayer/src/org/puredata/android/scenes/ScenePlayer.java @@ -398,16 +398,18 @@ private void stopRecording() { if (recFile == null) return; PdBase.sendMessage(TRANSPORT, "record", 0); long duration = System.currentTimeMillis() - recStart; - double longitude = 0.0; - double latitude = 0.0; - LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); - if (locationManager != null) { // Paranoid? Maybe... - Location location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - if (location != null) { - longitude = location.getLongitude(); - latitude = location.getLatitude(); - } - } + double longitude = 0.0; + double latitude = 0.0; + try { + LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); + if (locationManager != null) { // Paranoid? Maybe... + Location location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + if (location != null) { + longitude = location.getLongitude(); + latitude = location.getLatitude(); + } + } + } catch(SecurityException e) {} // do nothing db.addRecording(recFile, recStart, duration, longitude, latitude, sceneId); recFile = null; post("Finished recording"); From 4c74215f1042a9166afaf527346123064a71ab95 Mon Sep 17 00:00:00 2001 From: Antoine Rousseau <_antoine_@metalu.net> Date: Thu, 25 Sep 2025 22:36:02 +0200 Subject: [PATCH 11/11] upgrade ndkVersion from 27.2.12479018 to 28.2.13676358 in order to align to 16KB pagesize, as required by google --- PdCore/build.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PdCore/build.gradle b/PdCore/build.gradle index 2efef44d..3b7183f4 100644 --- a/PdCore/build.gradle +++ b/PdCore/build.gradle @@ -41,5 +41,5 @@ ext { minSdkVersion = 28 compileSdkVersion = 34 androidxLegacySupportVersion = '1.0.0' - ndkVersion = "27.2.12479018" // https://developer.android.com/ndk/downloads#lts-downloads + ndkVersion = "28.2.13676358" // https://developer.android.com/ndk/downloads#lts-downloads } diff --git a/build.gradle b/build.gradle index 9a0a5fca..d86ed405 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,6 @@ ext { minSdkVersion = 28 compileSdkVersion = 34 androidxLegacySupportVersion = '1.0.0' - ndkVersion = "27.2.12479018" // https://developer.android.com/ndk/downloads#lts-downloads + ndkVersion = "28.2.13676358" // https://developer.android.com/ndk/downloads#lts-downloads pdCoreVersion = '1.3.0' // Must match version in PdCore/build.gradle }