Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f8e6045
new plugin: audio-stream
dnecra Dec 17, 2025
f44a1f0
fix audio-stream
dnecra Dec 19, 2025
bb38318
Merge branch 'pear-devs:master' into master
dnecra Dec 19, 2025
1e8b093
fix audio-stream
dnecra Dec 19, 2025
70673db
fix audio-stream
dnecra Dec 19, 2025
f27372a
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
fab4b6b
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
8b36e8b
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
d19c899
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
ec30930
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
e679311
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
16da921
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
8b4448a
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
87146da
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
bc9d099
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
e4c748e
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
792b440
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
d1d891b
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
7a1fbea
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
024508b
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
0af3e8e
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
6c112be
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
5ca2a7f
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
738872d
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
13f818a
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
d995d9f
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
8fa70f8
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
59490ed
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
d87c843
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
668ac39
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
80e17d9
Update src/plugins/audio-stream/backend.ts
dnecra Dec 19, 2025
a2fea13
Merge branch 'pear-devs:master' into master
dnecra Dec 29, 2025
d443777
Migrate audio processing to AudioWorkletNode
dnecra Dec 29, 2025
01842c8
Implement binary PCM data handling in audio stream
dnecra Dec 29, 2025
02a74de
Update src/plugins/audio-stream/backend.ts
dnecra Dec 29, 2025
5e85f8b
Update src/plugins/audio-stream/backend.ts
dnecra Dec 29, 2025
d6aa17d
Update src/plugins/audio-stream/backend.ts
dnecra Dec 29, 2025
ddefbb6
Update src/plugins/audio-stream/backend.ts
dnecra Dec 29, 2025
278bcaf
Update src/plugins/audio-stream/backend.ts
dnecra Dec 29, 2025
b562ea1
Merge branch 'master' into master
ArjixWasTaken Jan 1, 2026
9078890
init rewrite of plugin
ArjixWasTaken Jan 11, 2026
39275d6
Merge branch 'master' into master
ArjixWasTaken Jan 11, 2026
966142d
Merge branch 'master' into master
ArjixWasTaken Jan 28, 2026
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
34 changes: 34 additions & 0 deletions src/i18n/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,40 @@
"description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)",
"name": "Audio Compressor"
},
"audio-stream": {
"description": "Stream audio as PCM data over HTTP for external applications",
"menu": {
"port": {
"label": "Port"
},
"quality-latency": {
"label": "Quality & Latency",
"submenu": {
"bit-depth": {
"label": "Bit Depth"
},
"buffer-size": {
"label": "Buffer Size"
},
"channels": {
"label": "Channels",
"mono": "Mono",
"stereo": "Stereo"
},
"sample-rate": {
"label": "Sample Rate"
}
}
}
},
"name": "Audio Stream",
"prompt": {
"port": {
"label": "Enter the port for the audio stream server:\nStream URL: {{streamUrl}}",
"title": "Audio Stream Port"
}
}
},
"auth-proxy-adapter": {
"description": "Support for the use of authentication proxy services",
"menu": {
Expand Down
6 changes: 6 additions & 0 deletions src/pear-desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ declare module '*.css?inline' {

export default css;
}

declare module '*.js?raw' {
const javascript: string;

export default javascript;
}
34 changes: 34 additions & 0 deletions src/plugins/audio-stream/BroadcastStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export class BroadcastStream {
private subscribers: Set<ReadableStreamDefaultController<Uint8Array>> =
new Set();

// A way for readers to get a new stream
subscribe() {
let controller!: ReadableStreamDefaultController<Uint8Array>;
const stream = new ReadableStream<Uint8Array>({
start(c) {
controller = c;
},
cancel: () => {
this.subscribers.delete(controller);
},
});

this.subscribers.add(controller);
return stream;
}

// A way for you to write data to all readers
write(chunk: Uint8Array) {
for (const controller of this.subscribers) {
controller.enqueue(chunk);
}
}

close() {
for (const controller of this.subscribers) {
controller.close();
}
this.subscribers.clear();
}
}
35 changes: 35 additions & 0 deletions src/plugins/audio-stream/StreamProcessor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// audio-processor.js (loaded by audioContext.audioWorklet.addModule)
class RecorderProcessor extends AudioWorkletProcessor {
constructor(options) {
super();
const bufferSize = options.bufferSize || 4096;
// Prepare an interleaved stereo buffer [L,R,L,R,...]
this.buffer = new Float32Array(bufferSize * 2);
this.bufferIndex = 0;
}

process(inputs, outputs) {
const input = inputs[0];
if (input && input[0]) {
const left = input[0];
const right = input[1] || left; // if mono input, duplicate for right
for (let i = 0; i < left.length; i++) {
this.buffer[this.bufferIndex++] = left[i];
this.buffer[this.bufferIndex++] = right[i];
if (this.bufferIndex >= this.buffer.length) {
// Buffer full: send a copy to the main thread
this.port.postMessage(new Float32Array(this.buffer));
this.bufferIndex = 0;
}
}
}
// Optionally pass the audio through unchanged
if (outputs[0] && inputs[0]) {
outputs[0][0].set(inputs[0][0]);
if (inputs[0][1]) outputs[0][1].set(inputs[0][1]);
}
return true;
}
}

registerProcessor('recorder-processor', RecorderProcessor);
118 changes: 118 additions & 0 deletions src/plugins/audio-stream/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Hono } from 'hono';
import { streamText } from 'hono/streaming';
import { serve, type ServerType } from '@hono/node-server';

import { createBackend } from '@/utils';
import { type AudioStreamConfig } from './config';
import { BroadcastStream } from './BroadcastStream';

const META_INT = 16_000;

let config: AudioStreamConfig;
const broadcast = new BroadcastStream();

export const backend = createBackend<
{
app: Hono;
server?: ServerType;
},
AudioStreamConfig
>({
app: new Hono().get('/stream', (ctx) => {
const icyMetadata = ctx.req.header('Icy-Metadata');
if (icyMetadata === '1') {
ctx.header('icy-metaint', META_INT.toString(10));
ctx.header('icy-name', 'Pear Desktop');
ctx.header('icy-url', 'https://github.com/pear-devs/pear-desktop');
ctx.header(
'icy-audio-info',
`ice-channels=2;ice-samplerate=${config.sampleRate.toString(
10,
)};ice-bitrate=128`,
);
ctx.header('icy-pub', '1');
ctx.header('icy-sr', config.sampleRate.toString(10));
ctx.header('Content-Type', 'audio/L16');
ctx.header('Server', 'Pear Desktop');
}

return streamText(ctx, async (stream) => {
let readable = broadcast.subscribe();
if (icyMetadata === '1') {
let bytesUntilMetadata = META_INT;

readable = readable.pipeThrough(
new TransformStream({
transform(
chunk: Uint8Array,
controller: TransformStreamDefaultController<Uint8Array>,
) {
console.log({ bytesUntilMetadata });
let offset = 0;

while (offset < chunk.byteLength) {
if (bytesUntilMetadata === 0) {
const encoder = new TextEncoder();

// TODO: add real metadata
const metaBuffer = encoder.encode(
".StreamTitle='My Cool Stream Title';",
);

const padding = (16 - (metaBuffer.byteLength % 16)) % 16;
const metaLength = metaBuffer.byteLength + padding;
const lengthByte = metaLength / 16;

controller.enqueue(Uint8Array.from([lengthByte]));

if (metaLength > 0) {
controller.enqueue(Uint8Array.from(metaBuffer));
}

bytesUntilMetadata = META_INT;
}

const chunkRemaining = chunk.byteLength - offset;
const canSend = Math.min(chunkRemaining, bytesUntilMetadata);
controller.enqueue(chunk.subarray(offset, offset + canSend));

bytesUntilMetadata -= canSend;
offset += canSend;
}
},
}),
);
}

return await stream.pipe(readable);
});
}),

async start({ getConfig, ipc }) {
config = await getConfig();

this.server = serve(
{
fetch: this.app.fetch.bind(this.app),
hostname: config.hostname,
port: config.port,
},
({ address, port }) => console.log('Listening on', { address, port }),
);

ipc.on('audio-stream:pcm-binary', (chunk: Uint8Array) => {
broadcast.write(chunk);
});
},
async stop() {
let resolve;

const promise = new Promise((r) => (resolve = r));
this.server?.close(resolve);

await promise;
},
onConfigChange(newConfig) {
config = newConfig;
},
});
22 changes: 22 additions & 0 deletions src/plugins/audio-stream/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface AudioStreamConfig {
enabled: boolean;
port: number;
hostname: string;
// Audio quality settings for PCM streaming
sampleRate: number; // Audio sample rate (e.g., 44100, 48000, 96000)
bitDepth: number; // Bit depth (16 or 32)
channels: number; // Number of channels (1 = mono, 2 = stereo)
bufferSize: number; // Audio buffer size (1024, 2048, 4096, 8192) - affects latency
}

export const defaultAudioStreamConfig: AudioStreamConfig = {
enabled: false,
port: 8765,
hostname: '0.0.0.0',
// High quality audio settings for local network
// Using 48kHz/16-bit for stability - can increase to 96kHz/32-bit once working
sampleRate: 48000, // 48kHz - high quality and widely supported
bitDepth: 16, // 16-bit - reliable and well-tested
channels: 2, // Stereo
bufferSize: 2048, // Low latency buffer size
};
17 changes: 17 additions & 0 deletions src/plugins/audio-stream/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { t } from '@/i18n';
import { createPlugin } from '@/utils';

import { defaultAudioStreamConfig } from './config';
import { backend } from './backend';
import { onMenu } from './menu';
import { renderer } from './renderer';

export default createPlugin({
name: () => t('plugins.audio-stream.name'),
description: () => t('plugins.audio-stream.description'),
restartNeeded: false,
config: defaultAudioStreamConfig,
backend,
renderer,
menu: onMenu,
});
Loading