Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions ft8cn/app/src/main/cpp/usb_audio_capture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,228 @@ Java_com_bg7yoz_ft8cn_wave_UsbAudioNative_nativeStop(
delete s;
LOGI("stopped capture");
}

// ----------------------------------------------------------------------------
// Synchronous OUTPUT write
// ----------------------------------------------------------------------------
//
// FT8TransmitSignal hands us a fully-formatted PCM byte buffer (interleaved
// little-endian int16, already at the device's native sample rate and
// channel count) plus the iso OUT endpoint info. We push it out through
// libusb iso transfers and block until the kernel has drained the entire
// buffer — same shape as the existing UsbRequest-based writeAudio() in
// Java, but via libusb so the car-dash kernel doesn't reject us.
//
// Implementation: keep kNumOutputTransfers transfers in flight, each
// carrying kPacketsPerOutputTransfer packets. Each completion callback
// refills its transfer with the next chunk and resubmits, until the input
// buffer is exhausted; the JNI thread spins libusb_handle_events_timeout_*
// until the in-flight count drops to zero.

namespace {

constexpr int kPacketsPerOutputTransfer = 8;
constexpr int kNumOutputTransfers = 4;

struct OutputSession {
libusb_context* ctx = nullptr;
libusb_device_handle* handle = nullptr;
int endpoint = 0;
int maxPktSize = 0;
const uint8_t* pcm = nullptr;
int pcmLen = 0;
int pcmOffset = 0;
std::atomic<int> inFlight{0};
std::atomic<int> firstError{0}; // non-zero = first libusb error code seen
};

void LIBUSB_CALL onOutputComplete(libusb_transfer* xfer) {
auto* s = static_cast<OutputSession*>(xfer->user_data);

if (xfer->status != LIBUSB_TRANSFER_COMPLETED) {
int expect = 0;
s->firstError.compare_exchange_strong(expect, xfer->status);
// For terminal errors we let the in-flight counter drop so the
// caller exits. Iso has best-effort delivery semantics, but a
// NO_DEVICE / CANCELLED is unrecoverable.
if (xfer->status == LIBUSB_TRANSFER_NO_DEVICE
|| xfer->status == LIBUSB_TRANSFER_CANCELLED) {
s->inFlight.fetch_sub(1, std::memory_order_acq_rel);
return;
}
// Other statuses (STALL, TIMED_OUT, OVERFLOW): try to keep going,
// but only if PCM remains.
}

if (s->pcmOffset >= s->pcmLen) {
// Done; let this transfer retire.
s->inFlight.fetch_sub(1, std::memory_order_acq_rel);
return;
}

int chunkBytes = kPacketsPerOutputTransfer * s->maxPktSize;
int remaining = s->pcmLen - s->pcmOffset;
int thisChunk = std::min(chunkBytes, remaining);

std::memcpy(xfer->buffer, s->pcm + s->pcmOffset, thisChunk);
if (thisChunk < chunkBytes) {
std::memset(xfer->buffer + thisChunk, 0, chunkBytes - thisChunk);
}
s->pcmOffset += thisChunk;

int rc = libusb_submit_transfer(xfer);
if (rc != 0) {
int expect = 0;
s->firstError.compare_exchange_strong(expect, rc);
s->inFlight.fetch_sub(1, std::memory_order_acq_rel);
}
}

} // namespace

extern "C" JNIEXPORT jint JNICALL
Java_com_bg7yoz_ft8cn_wave_UsbAudioNative_nativeWrite(
JNIEnv* env,
jclass /*clazz*/,
jint fd,
jint /*interfaceNumber*/,
jint /*altSetting*/,
jint endpointAddress,
jint maxPacketSize,
jint /*outputSampleRate*/,
jint /*outputChannels*/,
jint /*outputBytesPerSample*/,
jbyteArray pcmArray) {

if (!pcmArray) return LIBUSB_ERROR_INVALID_PARAM;
if (maxPacketSize <= 0) return LIBUSB_ERROR_INVALID_PARAM;

libusb_set_option(nullptr, LIBUSB_OPTION_NO_DEVICE_DISCOVERY);

libusb_context* ctx = nullptr;
int rc = libusb_init(&ctx);
if (rc != 0) {
LOGE("nativeWrite: libusb_init: %s", libusb_error_name(rc));
return rc;
}

libusb_device_handle* handle = nullptr;
rc = libusb_wrap_sys_device(ctx, (intptr_t)fd, &handle);
if (rc != 0 || !handle) {
LOGE("nativeWrite: libusb_wrap_sys_device(fd=%d): %s",
fd, libusb_error_name(rc));
libusb_exit(ctx);
return rc != 0 ? rc : LIBUSB_ERROR_NO_DEVICE;
}

jsize pcmLen = env->GetArrayLength(pcmArray);
// Critical lock: PCM bytes are read directly into transfer buffers
// during this call; we don't need to keep them after we return.
jbyte* pcmBytes = env->GetByteArrayElements(pcmArray, nullptr);
if (!pcmBytes) {
libusb_close(handle);
libusb_exit(ctx);
return LIBUSB_ERROR_NO_MEM;
}

OutputSession s;
s.ctx = ctx;
s.handle = handle;
s.endpoint = endpointAddress;
s.maxPktSize = maxPacketSize;
s.pcm = reinterpret_cast<const uint8_t*>(pcmBytes);
s.pcmLen = (int)pcmLen;
s.pcmOffset = 0;

libusb_transfer* xfers[kNumOutputTransfers] = {nullptr};
int submitted = 0;

for (int i = 0; i < kNumOutputTransfers && s.pcmOffset < s.pcmLen; ++i) {
xfers[i] = libusb_alloc_transfer(kPacketsPerOutputTransfer);
if (!xfers[i]) {
int expect = 0;
s.firstError.compare_exchange_strong(expect, LIBUSB_ERROR_NO_MEM);
continue;
}
int chunkBytes = kPacketsPerOutputTransfer * s.maxPktSize;
auto* buf = static_cast<uint8_t*>(std::malloc(chunkBytes));
if (!buf) {
libusb_free_transfer(xfers[i]);
xfers[i] = nullptr;
int expect = 0;
s.firstError.compare_exchange_strong(expect, LIBUSB_ERROR_NO_MEM);
continue;
}

int remaining = s.pcmLen - s.pcmOffset;
int thisChunk = std::min(chunkBytes, remaining);
std::memcpy(buf, s.pcm + s.pcmOffset, thisChunk);
if (thisChunk < chunkBytes) {
std::memset(buf + thisChunk, 0, chunkBytes - thisChunk);
}
s.pcmOffset += thisChunk;

libusb_fill_iso_transfer(
xfers[i], handle, (unsigned char)s.endpoint,
buf, chunkBytes, kPacketsPerOutputTransfer,
onOutputComplete, &s, /*timeout=*/0);
libusb_set_iso_packet_lengths(xfers[i], s.maxPktSize);
xfers[i]->flags = LIBUSB_TRANSFER_FREE_BUFFER;

int rcSub = libusb_submit_transfer(xfers[i]);
if (rcSub != 0) {
LOGE("nativeWrite: submit i=%d: %s", i, libusb_error_name(rcSub));
int expect = 0;
s.firstError.compare_exchange_strong(expect, rcSub);
libusb_free_transfer(xfers[i]);
xfers[i] = nullptr;
continue;
}
s.inFlight.fetch_add(1, std::memory_order_acq_rel);
submitted++;
}

LOGI("nativeWrite: %d transfers submitted, pcmLen=%d ep=0x%02x maxPkt=%d",
submitted, (int)pcmLen, endpointAddress, maxPacketSize);

if (submitted == 0) {
env->ReleaseByteArrayElements(pcmArray, pcmBytes, JNI_ABORT);
libusb_close(handle);
libusb_exit(ctx);
return s.firstError.load();
}

// Drain. Iso transfers are paced by the device's bandwidth allocation,
// so this naturally takes ~ pcmLen / (sampleRate * channels * 2) seconds.
while (s.inFlight.load(std::memory_order_acquire) > 0) {
timeval tv{0, 50000}; // 50ms
rc = libusb_handle_events_timeout_completed(ctx, &tv, nullptr);
if (rc != 0 && rc != LIBUSB_ERROR_INTERRUPTED) {
LOGE("nativeWrite: handle_events: %s", libusb_error_name(rc));
int expect = 0;
s.firstError.compare_exchange_strong(expect, rc);
break;
}
}

// Cancel any still-pending transfers (in error paths).
for (int i = 0; i < kNumOutputTransfers; ++i) {
if (xfers[i]) libusb_cancel_transfer(xfers[i]);
}
while (s.inFlight.load(std::memory_order_acquire) > 0) {
timeval tv{0, 50000};
libusb_handle_events_timeout_completed(ctx, &tv, nullptr);
}
for (int i = 0; i < kNumOutputTransfers; ++i) {
if (xfers[i]) libusb_free_transfer(xfers[i]);
}

env->ReleaseByteArrayElements(pcmArray, pcmBytes, JNI_ABORT);
libusb_close(handle);
libusb_exit(ctx);

int err = s.firstError.load();
LOGI("nativeWrite: done, firstError=%d (%s)",
err, err == 0 ? "OK" : libusb_error_name(err));
return err;
}
35 changes: 35 additions & 0 deletions ft8cn/app/src/main/java/com/bg7yoz/ft8cn/wave/UsbAudioDevice.java
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,41 @@ public boolean writeAudio(float[] audioData, int sourceSampleRate) {
}
}

// Prefer the libusb-backed native path. Same reason as input: on hosts
// where Android's UsbRequest can't drive iso (notably car-dash kernels)
// request.initialize() returns false and writeAudio fails immediately,
// which aborts the TX after ~200ms. libusb talks to USBDEVFS directly.
if (UsbAudioNative.isAvailable() && streamingInterfaceOut != null) {
int fd = connection.getFileDescriptor();
int ifaceNum = streamingInterfaceOut.getId();
int altSet = streamingInterfaceOut.getAlternateSetting();
int epAddr = endpointOut.getAddress();
int maxPkt = endpointOut.getMaxPacketSize();

com.bg7yoz.ft8cn.GeneralVariables.fileLog(String.format(
"UsbAudioDevice: trying libusb native write "
+ "fd=%d iface=%d alt=%d ep=0x%02x maxPkt=%d "
+ "bytes=%d outputRate=%d ch=%d",
fd, ifaceNum, altSet, epAddr, maxPkt,
pcmData.length, outputSampleRate, outputChannels));

int rc = UsbAudioNative.nativeWrite(
fd, ifaceNum, altSet, epAddr, maxPkt,
outputSampleRate, outputChannels, /*bytesPerSample=*/2,
pcmData);

if (rc == 0) {
com.bg7yoz.ft8cn.GeneralVariables.fileLog(
"UsbAudioDevice: libusb native write OK");
return true;
}
com.bg7yoz.ft8cn.GeneralVariables.fileLog(
"UsbAudioDevice: libusb native write FAILED rc=" + rc
+ ", falling back to UsbRequest");
}

// Fallback: original UsbRequest-based write. Kept so devices that work
// through Android's iso path don't regress on the new native lib.
// Write in chunks matching max packet size
int packetSize = endpointOut.getMaxPacketSize();
int offset = 0;
Expand Down
35 changes: 35 additions & 0 deletions ft8cn/app/src/main/java/com/bg7yoz/ft8cn/wave/UsbAudioNative.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,39 @@ public static native long nativeStart(
* event thread has drained outstanding URBs and the callback's
* {@code onCaptureStopped} has been invoked. */
public static native void nativeStop(long handle);

/**
* Synchronously push a complete PCM buffer out through an iso OUT
* endpoint. Mirrors {@link #nativeStart} but for transmit — caller
* already has the endpoint open and alt-setting active, and hands us
* the fd plus the byte buffer to drain. Blocks until the kernel has
* accepted every packet (or an error occurs).
*
* @param fd file descriptor from
* {@link android.hardware.usb.UsbDeviceConnection#getFileDescriptor()}.
* @param interfaceNumber bInterfaceNumber of the audio streaming OUT interface.
* @param altSetting alt-setting already activated on that interface.
* @param endpointAddress bEndpointAddress of the iso OUT endpoint.
* @param maxPacketSize wMaxPacketSize from the endpoint descriptor.
* @param outputSampleRate device sample rate (informational).
* @param outputChannels device channel count (informational; buffer is
* expected to already be interleaved correctly).
* @param outputBytesPerSample 2 for 16-bit PCM (informational).
* @param pcmData interleaved little-endian int16 PCM bytes,
* already at outputSampleRate × outputChannels.
* @return 0 on success, or a negative libusb error code
* ({@code LIBUSB_ERROR_*}) on failure. Caller
* should fall back to the {@code UsbRequest} path
* on a non-zero return.
*/
public static native int nativeWrite(
int fd,
int interfaceNumber,
int altSetting,
int endpointAddress,
int maxPacketSize,
int outputSampleRate,
int outputChannels,
int outputBytesPerSample,
byte[] pcmData);
}
Loading