#include "BridgeProcessor.h" #include "BridgeEditor.h" #include #include #include #include namespace { constexpr uint32_t frameMagic = 0x4d535250; constexpr uint16_t protocolVersion = 1; constexpr uint16_t messageHello = 1; constexpr uint16_t messageHeartbeat = 2; constexpr uint16_t messageSourceUpdate = 3; constexpr uint16_t messageSubscribe = 10; constexpr uint16_t messageAudio = 20; constexpr size_t frameHeaderBytes = 12; constexpr size_t audioHeaderBytes = 76; constexpr size_t maxPayloadBytes = 256 * 1024; constexpr int serverPort = 51789; template void writeLittleEndian(uint8_t* destination, Integer value) noexcept { const auto little = juce::ByteOrder::swapIfBigEndian(value); std::memcpy(destination, &little, sizeof(little)); } void writeDoubleLittleEndian(uint8_t* destination, double value) noexcept { uint64_t bits = 0; std::memcpy(&bits, &value, sizeof(bits)); writeLittleEndian(destination, bits); } uint16_t readU16(const uint8_t* source) noexcept { uint16_t value = 0; std::memcpy(&value, source, sizeof(value)); return juce::ByteOrder::swapIfBigEndian(value); } uint32_t readU32(const uint8_t* source) noexcept { uint32_t value = 0; std::memcpy(&value, source, sizeof(value)); return juce::ByteOrder::swapIfBigEndian(value); } } PrismBridgeProcessor::PrismBridgeProcessor() : juce::AudioProcessor(BusesProperties() .withInput("Input", juce::AudioChannelSet::stereo(), true) .withOutput("Output", juce::AudioChannelSet::stereo(), true)), juce::Thread("Prism Bridge network"), sourceId(juce::Uuid().toString()), instanceId(juce::Uuid().toString()), hostName(juce::PluginHostType().getHostDescription()) { } PrismBridgeProcessor::~PrismBridgeProcessor() { signalThreadShouldExit(); stopThread(1500); closeSocket(); } void PrismBridgeProcessor::prepareToPlay(double sampleRate, int) { currentSampleRate.store(sampleRate > 0.0 ? sampleRate : 48000.0); nextSequence.store(0); fifo.reset(); #if ! defined(PRISM_BRIDGE_TESTING) || ! PRISM_BRIDGE_TESTING if (!isThreadRunning()) startThread(juce::Thread::Priority::normal); #endif identityDirty.store(true); } void PrismBridgeProcessor::releaseResources() { signalThreadShouldExit(); stopThread(1000); closeSocket(); connected.store(false); selected.store(false); discardQueuedPackets(); } bool PrismBridgeProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const { const auto input = layouts.getMainInputChannelSet(); const auto output = layouts.getMainOutputChannelSet(); return input == output && (input == juce::AudioChannelSet::mono() || input == juce::AudioChannelSet::stereo()); } PrismBridgeProcessor::TransportData PrismBridgeProcessor::readTransport() const noexcept { TransportData result; if (const auto* playHead = getPlayHead()) { if (const auto position = playHead->getPosition()) { result.isPlaying = position->getIsPlaying(); result.isRecording = position->getIsRecording(); result.isLooping = position->getIsLooping(); if (const auto value = position->getTimeInSamples()) { result.timeInSamples = *value; result.hasTimeInSamples = true; } if (const auto value = position->getTimeInSeconds()) { result.timeInSeconds = *value; result.hasTimeInSeconds = true; } if (const auto value = position->getPpqPosition()) { result.ppqPosition = *value; result.hasPpq = true; } if (const auto value = position->getBpm()) { result.bpm = *value; result.hasBpm = std::isfinite(result.bpm) && result.bpm > 0.0; } if (const auto value = position->getPpqPositionOfLastBarStart()) result.lastBarPpq = *value; else result.lastBarPpq = std::numeric_limits::quiet_NaN(); if (const auto value = position->getTimeSignature()) { result.numerator = static_cast(value->numerator); result.denominator = static_cast(value->denominator); result.hasTimeSignature = value->numerator > 0 && value->denominator > 0; } if (const auto value = position->getLoopPoints()) { result.loopStartPpq = value->ppqStart; result.loopEndPpq = value->ppqEnd; } else { result.loopStartPpq = std::numeric_limits::quiet_NaN(); result.loopEndPpq = std::numeric_limits::quiet_NaN(); } } } return result; } PrismBridgeProcessor::TransportData PrismBridgeProcessor::offsetTransport( const TransportData& base, int sampleOffset) const noexcept { auto result = base; const double sampleRate = std::max(1.0, currentSampleRate.load()); const double seconds = static_cast(sampleOffset) / sampleRate; if (result.hasTimeInSamples) result.timeInSamples += sampleOffset; if (result.hasTimeInSeconds) result.timeInSeconds += seconds; if (result.hasPpq && result.hasBpm) result.ppqPosition += seconds * result.bpm / 60.0; return result; } void PrismBridgeProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer&) { juce::ScopedNoDenormals noDenormals; if (isNonRealtime() || !selected.load(std::memory_order_relaxed)) return; const int frameCount = buffer.getNumSamples(); const int channels = buffer.getNumChannels(); if (frameCount <= 0 || channels <= 0) return; const auto transport = readTransport(); const float* left = buffer.getReadPointer(0); const float* right = channels > 1 ? buffer.getReadPointer(1) : left; for (int offset = 0; offset < frameCount; offset += packetFrames) { const int count = std::min(packetFrames, frameCount - offset); enqueuePacket(left + offset, right + offset, count, channels, offsetTransport(transport, offset)); } } void PrismBridgeProcessor::enqueuePacket( const float* left, const float* right, int frameCount, int channelCount, const TransportData& transport) noexcept { int start1 = 0, size1 = 0, start2 = 0, size2 = 0; fifo.prepareToWrite(1, start1, size1, start2, size2); if (size1 == 0) { droppedFrames.fetch_add(static_cast(frameCount), std::memory_order_relaxed); return; } auto& packet = packets[static_cast(start1)]; std::memcpy(packet.left.data(), left, static_cast(frameCount) * sizeof(float)); std::memcpy(packet.right.data(), right, static_cast(frameCount) * sizeof(float)); packet.transport = transport; packet.sequence = nextSequence.fetch_add(1, std::memory_order_relaxed) + 1; packet.frameCount = static_cast(frameCount); packet.channelCount = static_cast(channelCount > 1 ? 2 : 1); packet.sampleRate = currentSampleRate.load(std::memory_order_relaxed); fifo.finishedWrite(1); } bool PrismBridgeProcessor::popPacket(AudioPacket& destination) noexcept { int start1 = 0, size1 = 0, start2 = 0, size2 = 0; fifo.prepareToRead(1, start1, size1, start2, size2); if (size1 == 0) return false; destination = packets[static_cast(start1)]; fifo.finishedRead(1); return true; } void PrismBridgeProcessor::discardQueuedPackets() noexcept { AudioPacket scratch; while (popPacket(scratch)) {} } void PrismBridgeProcessor::run() { int reconnectDelayMs = 250; int64_t lastHeartbeat = 0; AudioPacket packet; while (!threadShouldExit()) { if (!socket || !socket->isConnected()) { connected.store(false); selected.store(false); discardQueuedPackets(); if (!connectSocket()) { wait(reconnectDelayMs); reconnectDelayMs = std::min(5000, reconnectDelayMs * 2); continue; } reconnectDelayMs = 250; lastHeartbeat = static_cast(juce::Time::getMillisecondCounterHiRes()); } if (!readControlMessages()) { closeSocket(); continue; } const auto currentDroppedFrames = droppedFrames.load(); if ((identityDirty.exchange(false) || currentDroppedFrames != reportedDroppedFrames) && !sendHello(messageSourceUpdate)) { closeSocket(); continue; } reportedDroppedFrames = currentDroppedFrames; const auto now = static_cast(juce::Time::getMillisecondCounterHiRes()); if (now - lastHeartbeat >= 1000) { if (!sendHeartbeat()) { closeSocket(); continue; } lastHeartbeat = now; } if (!selected.load()) { discardQueuedPackets(); wait(5); continue; } const auto maximumLivePackets = std::max( 1, static_cast(std::floor(currentSampleRate.load() * 0.05 / packetFrames))); if (fifo.getNumReady() > maximumLivePackets) { while (fifo.getNumReady() > maximumLivePackets && popPacket(packet)) droppedFrames.fetch_add(packet.frameCount, std::memory_order_relaxed); } if (popPacket(packet)) { if (!sendAudioPacket(packet)) closeSocket(); } else { wait(2); } } closeSocket(); } bool PrismBridgeProcessor::connectSocket() { socket = std::make_unique(); if (!socket->connect("127.0.0.1", serverPort, 250)) { socket.reset(); return false; } connected.store(true); controlBuffer.setSize(0); if (!sendHello(messageHello)) { closeSocket(); return false; } reportedDroppedFrames = droppedFrames.load(); return true; } void PrismBridgeProcessor::closeSocket() { connected.store(false); selected.store(false); if (socket) socket->close(); socket.reset(); } juce::String PrismBridgeProcessor::buildIdentityJson() const { const juce::ScopedLock lock(identityLock); auto object = new juce::DynamicObject(); object->setProperty("sourceId", sourceId); object->setProperty("instanceId", instanceId); object->setProperty("customName", customName); object->setProperty("trackName", trackName); object->setProperty("hostName", hostName); object->setProperty("sampleRate", currentSampleRate.load()); object->setProperty("channelCount", getTotalNumInputChannels()); object->setProperty("droppedFrames", static_cast(droppedFrames.load())); return juce::JSON::toString(juce::var(object), true); } bool PrismBridgeProcessor::sendHello(uint16_t messageType) { const auto json = buildIdentityJson(); return sendFrame(messageType, json.toRawUTF8(), static_cast(json.getNumBytesAsUTF8())); } bool PrismBridgeProcessor::sendHeartbeat() { return sendFrame(messageHeartbeat, nullptr, 0); } bool PrismBridgeProcessor::sendAudioPacket(const AudioPacket& packet) { std::array payload {}; const auto payloadSize = encodeAudioPayload(packet, payload.data(), payload.size()); return payloadSize > 0 && sendFrame(messageAudio, payload.data(), payloadSize); } size_t PrismBridgeProcessor::encodeAudioPayload( const AudioPacket& packet, uint8_t* payload, size_t capacity) const noexcept { const auto requiredSize = audioHeaderBytes + static_cast(packet.frameCount) * packet.channelCount * sizeof(float); if (!payload || packet.frameCount == 0 || packet.frameCount > packetFrames || (packet.channelCount != 1 && packet.channelCount != 2) || capacity < requiredSize) return 0; std::memset(payload, 0, requiredSize); writeLittleEndian(payload + 0, packet.sequence); writeLittleEndian(payload + 4, packet.frameCount); payload[6] = packet.channelCount; uint8_t flags = 0; if (packet.transport.isPlaying) flags |= 1 << 0; if (packet.transport.isRecording) flags |= 1 << 1; if (packet.transport.isLooping) flags |= 1 << 2; if (packet.transport.hasTimeInSamples) flags |= 1 << 3; if (packet.transport.hasTimeInSeconds) flags |= 1 << 4; if (packet.transport.hasPpq) flags |= 1 << 5; if (packet.transport.hasBpm) flags |= 1 << 6; if (packet.transport.hasTimeSignature) flags |= 1 << 7; payload[7] = flags; writeDoubleLittleEndian(payload + 8, packet.sampleRate); writeLittleEndian(payload + 16, packet.transport.timeInSamples); writeDoubleLittleEndian(payload + 24, packet.transport.timeInSeconds); writeDoubleLittleEndian(payload + 32, packet.transport.ppqPosition); writeDoubleLittleEndian(payload + 40, packet.transport.bpm); writeDoubleLittleEndian(payload + 48, packet.transport.lastBarPpq); writeDoubleLittleEndian(payload + 56, packet.transport.loopStartPpq); writeDoubleLittleEndian(payload + 64, packet.transport.loopEndPpq); writeLittleEndian(payload + 72, packet.transport.numerator); writeLittleEndian(payload + 74, packet.transport.denominator); size_t offset = audioHeaderBytes; for (size_t index = 0; index < packet.frameCount; ++index) { uint32_t bits = 0; std::memcpy(&bits, &packet.left[index], sizeof(bits)); writeLittleEndian(payload + offset, bits); offset += sizeof(float); } if (packet.channelCount > 1) { for (size_t index = 0; index < packet.frameCount; ++index) { uint32_t bits = 0; std::memcpy(&bits, &packet.right[index], sizeof(bits)); writeLittleEndian(payload + offset, bits); offset += sizeof(float); } } return offset; } bool PrismBridgeProcessor::sendFrame(uint16_t messageType, const void* payload, size_t payloadSize) { if (payloadSize > maxPayloadBytes) return false; std::array header {}; writeLittleEndian(header.data() + 0, frameMagic); writeLittleEndian(header.data() + 4, protocolVersion); writeLittleEndian(header.data() + 6, messageType); writeLittleEndian(header.data() + 8, static_cast(payloadSize)); return writeAll(header.data(), header.size()) && (payloadSize == 0 || writeAll(payload, payloadSize)); } bool PrismBridgeProcessor::writeAll(const void* source, size_t byteCount) { if (!socket || !socket->isConnected()) return false; const auto* bytes = static_cast(source); size_t written = 0; while (written < byteCount && !threadShouldExit()) { if (socket->waitUntilReady(false, 50) < 1) return false; const int result = socket->write(bytes + written, static_cast(byteCount - written)); if (result <= 0) return false; written += static_cast(result); } return written == byteCount; } bool PrismBridgeProcessor::readControlMessages() { if (!socket || !socket->isConnected()) return false; while (socket->waitUntilReady(true, 0) > 0) { std::array buffer {}; const int bytesRead = socket->read(buffer.data(), static_cast(buffer.size()), false); if (bytesRead <= 0) return false; controlBuffer.append(buffer.data(), static_cast(bytesRead)); if (controlBuffer.getSize() > maxPayloadBytes + frameHeaderBytes) return false; parseControlBuffer(); } return true; } void PrismBridgeProcessor::parseControlBuffer() { while (controlBuffer.getSize() >= frameHeaderBytes) { const auto* data = static_cast(controlBuffer.getData()); const uint32_t magic = readU32(data + 0); const uint16_t version = readU16(data + 4); const uint16_t messageType = readU16(data + 6); const uint32_t payloadSize = readU32(data + 8); if (magic != frameMagic || version != protocolVersion || payloadSize > maxPayloadBytes) { closeSocket(); return; } const size_t frameSize = frameHeaderBytes + payloadSize; if (controlBuffer.getSize() < frameSize) return; if (messageType == messageSubscribe) { const auto json = juce::String::fromUTF8( reinterpret_cast(data + frameHeaderBytes), static_cast(payloadSize)); const auto parsed = juce::JSON::parse(json); if (const auto* object = parsed.getDynamicObject()) selected.store(static_cast(object->getProperty("selected"))); } juce::MemoryBlock remainder; if (controlBuffer.getSize() > frameSize) remainder.append(data + frameSize, controlBuffer.getSize() - frameSize); controlBuffer.swapWith(remainder); } } void PrismBridgeProcessor::updateTrackProperties(const TrackProperties& properties) { { const juce::ScopedLock lock(identityLock); trackName = properties.name; } identityDirty.store(true); } void PrismBridgeProcessor::setCustomName(const juce::String& value) { { const juce::ScopedLock lock(identityLock); customName = value.trim().substring(0, 160); } identityDirty.store(true); updateHostDisplay(ChangeDetails().withNonParameterStateChanged(true)); } juce::String PrismBridgeProcessor::getCustomName() const { const juce::ScopedLock lock(identityLock); return customName; } juce::String PrismBridgeProcessor::getDisplayName() const { const juce::ScopedLock lock(identityLock); if (customName.isNotEmpty()) return customName; if (trackName.isNotEmpty()) return trackName; return "Prism Bridge " + sourceId.substring(0, 8); } juce::String PrismBridgeProcessor::getHostName() const { const juce::ScopedLock lock(identityLock); return hostName; } juce::String PrismBridgeProcessor::getTrackName() const { const juce::ScopedLock lock(identityLock); return trackName; } juce::String PrismBridgeProcessor::getConnectionDescription() const { if (!connected.load()) return "Waiting for Prism"; return selected.load() ? "Connected · selected in Prism" : "Connected · available in Prism"; } void PrismBridgeProcessor::getStateInformation(juce::MemoryBlock& destination) { const juce::ScopedLock lock(identityLock); auto object = new juce::DynamicObject(); object->setProperty("sourceId", sourceId); object->setProperty("customName", customName); const auto json = juce::JSON::toString(juce::var(object)); destination.setSize(0); destination.append(json.toRawUTF8(), static_cast(json.getNumBytesAsUTF8())); } void PrismBridgeProcessor::setStateInformation(const void* data, int sizeInBytes) { if (!data || sizeInBytes <= 0) return; const auto parsed = juce::JSON::parse(juce::String::fromUTF8(static_cast(data), sizeInBytes)); if (const auto* object = parsed.getDynamicObject()) { const juce::ScopedLock lock(identityLock); const auto restoredId = object->getProperty("sourceId").toString(); if (restoredId.isNotEmpty()) sourceId = restoredId; customName = object->getProperty("customName").toString().trim().substring(0, 160); } identityDirty.store(true); } juce::AudioProcessorEditor* PrismBridgeProcessor::createEditor() { return new PrismBridgeEditor(*this); } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new PrismBridgeProcessor(); }