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
14 changes: 13 additions & 1 deletion freedata_gui/src/components/dynamic_components.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import grid_tune from "./grid/grid_tune.vue";
import grid_CQ_btn from "./grid/grid_CQ.vue";
import grid_ping from "./grid/grid_ping.vue";
import grid_freq from "./grid/grid_frequency.vue";
import grid_audio from "./grid/grid_audio.vue";
import grid_beacon from "./grid/grid_beacon.vue";
import grid_mycall_small from "./grid/grid_mycall small.vue";
import grid_scatter from "./grid/grid_scatter.vue";
Expand Down Expand Up @@ -325,8 +326,19 @@ const gridWidgets = [
18,
false,
{ x: 16, y: 8, w: 2, h: 8 }
),
new gridWidget(
grid_audio,
{ x: 16, y: 8, w: 4, h: 24 },
"Audio Stream",
false,
true,
"Audio",
24,
false,
{ x: 16, y: 8, w: 4, h: 24 }
)
//Next new widget ID should be 23
//Next new widget ID should be 24
];

function updateFrequencyAndApply(frequency) {
Expand Down
92 changes: 92 additions & 0 deletions freedata_gui/src/components/grid/grid_audio.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script setup>
import { setActivePinia } from 'pinia';
import { ref } from 'vue';

import pinia from '../../store/index';
setActivePinia(pinia);

import { useAudioStore } from '@/store/audioStore';
const audio = useAudioStore(pinia);




var audioCtx = null;
var isPlaying = ref(false);

function playRxStream() {
if (isPlaying.value) return;


const SAMPLE_RATE = 8000;

audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE });
//let scheduledTime = audioCtx.currentTime;
isPlaying.value = true;

console.log("audio playback");

function loop() {
if (!isPlaying.value) return;

const block = audio.getNextBlock();
if (block) {
const float32 = Float32Array.from(block, s => s / 32768);
const buffer = audioCtx.createBuffer(1, float32.length, SAMPLE_RATE);
buffer.copyToChannel(float32, 0);

const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
source.start(0);
} else {
//console.warn("⛔ Audio buffer underrun");
}

setTimeout(loop, 4);
}

if (audioCtx.state === 'suspended') {
audioCtx.resume().then(loop);
} else {
loop();
}
}

function stopRxStream() {
if (audioCtx) {
isPlaying.value = false;
audioCtx.close();
console.log("Playback stopped");
}
}


function toggleRxStream() {
if (isPlaying.value) {
stopRxStream()
} else {
playRxStream()
}
}

</script>

<template>

<div class="card h-100">
<div class="card-header">

<strong>{{ $t('grid.components.audiostream') }}</strong>
</div>
<div class="card-body overflow-auto m-0" style="align-items: start">
<button :class="isPlaying ? 'btn btn-sm btn-danger' : 'btn btn-sm btn-success'" @click="toggleRxStream">
<i :class="isPlaying ? 'bi bi-stop-fill' : 'bi bi-play-fill'"/>&nbsp;
{{isPlaying ? 'Stop' : 'Start'}}
</button>

</div>
</div>


</template>
25 changes: 25 additions & 0 deletions freedata_gui/src/js/audioStreamHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);

import { useAudioStore } from "../store/audioStore.js";
const audio = useAudioStore(pinia);

const MAX_BLOCKS = 10;

export function addDataToAudio(data) {
const int16 = new Int16Array(data);
const copy = new Int16Array(int16); // Kopie für Sicherheit
/*
const stream = audio.rxStream;

if (stream.length >= MAX_BLOCKS) {
stream.shift();
}

stream.push(copy);
*/
audio.addBlock(copy);


}
6 changes: 6 additions & 0 deletions freedata_gui/src/js/event_sock.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
loadAllData,
} from "../js/eventHandler.js";
import { addDataToWaterfall } from "../js/waterfallHandler.js";
import { addDataToAudio } from "../js/audioStreamHandler.js";

// ----------------- init pinia stores -------------
import { setActivePinia } from "pinia";
Expand All @@ -22,6 +23,10 @@ function connect(endpoint, dispatcher) {
`${wsProtocol}//${hostname}:${adjustedPort}/${endpoint}`,
);

if (endpoint.includes("audio")){
socket.binaryType = "arraybuffer";
}

// handle opening
socket.addEventListener("open", function () {
console.log(`Connected to the WebSocket server: ${endpoint}`);
Expand Down Expand Up @@ -56,4 +61,5 @@ export function initConnections() {
connect("states", stateDispatcher);
connect("events", eventDispatcher);
connect("fft", addDataToWaterfall);
connect("audio_rx", addDataToAudio);
}
1 change: 1 addition & 0 deletions freedata_gui/src/js/waterfallHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function addDataToWaterfall(data) {
});
//window.dispatchEvent(new CustomEvent("wf-data-avail", {bubbles:true, detail: data }));
}

/**
* Setwaterfall colormap array by index
* @param {number} index colormap index to use
Expand Down
1 change: 1 addition & 0 deletions freedata_gui/src/locales/de_Deutsch.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"downloadpreset": "Einstell. Herunterladen",
"downloadpreset_help": "Lade die GUI-Einstellungen herunter um sie zu speichern und zu teilen",
"components": {
"audiostream": "Audio Stream",
"tune": "Tune",
"stop_help": "Sitzung abbrechen und Aussendung beenden",
"transmissioncharts": "Übertragungs-Diagramme",
Expand Down
1 change: 1 addition & 0 deletions freedata_gui/src/locales/en_English.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"downloadpreset": "Download Preset",
"downloadpreset_help": "Download preset file for sharing or saving",
"components": {
"audiostream": "Audio Stream",
"tune": "Tune",
"stop_help": "Abort session and stop transmissions",
"transmissioncharts": "Transmission charts",
Expand Down
50 changes: 50 additions & 0 deletions freedata_gui/src/store/audioStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,48 @@ const skel = [
export const useAudioStore = defineStore("audioStore", () => {
const audioInputs = ref([]);
const audioOutputs = ref([]);
const rxStream = ref([]);

const BUFFER_SIZE = 1024;
const rxStreamBuffer = new Array(BUFFER_SIZE).fill(null);

let writePtr = 0;
let readPtr = 0;
let readyBlocks = 0;

function addBlock(block) {
rxStreamBuffer[writePtr] = block;
writePtr = (writePtr + 1) % BUFFER_SIZE;

if (readyBlocks < BUFFER_SIZE) {
readyBlocks++;
} else {
readPtr = (readPtr + 1) % BUFFER_SIZE;
}
}

function getNextBlock() {
if (readyBlocks === 0) return null;

const block = rxStreamBuffer[readPtr];
readPtr = (readPtr + 1) % BUFFER_SIZE;
readyBlocks--;
return block;
}

function resetBuffer() {
writePtr = 0;
readPtr = 0;
readyBlocks = 0;
for (let i = 0; i < BUFFER_SIZE; i++) {
rxStreamBuffer[i] = null;
}
}






const loadAudioDevices = async () => {
try {
Expand All @@ -35,5 +77,13 @@ export const useAudioStore = defineStore("audioStore", () => {
audioInputs,
audioOutputs,
loadAudioDevices,
rxStream,
addBlock,
getNextBlock,
resetBuffer,
get bufferedBlockCount() {
return readyBlocks;
},

};
});
17 changes: 17 additions & 0 deletions freedata_server/api/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,20 @@ async def websocket_states(websocket: WebSocket, ctx: AppContext = Depends(get_c
await ctx.websocket_manager.handle_connection(
websocket, ctx.websocket_manager.states_client_list, ctx.state_queue
)

@router.websocket("/audio_rx")
async def websocket_audio_rx(
websocket: WebSocket,
ctx: AppContext = Depends(get_ctx)
):
"""
WebSocket endpoint for state updates.
"""
await websocket.accept()
await ctx.websocket_manager.handle_connection(
websocket,
ctx.websocket_manager.audio_rx_client_list,
ctx.state_queue
)
#while True:
# await websocket.send_bytes(b"\x00" * 1024)
2 changes: 2 additions & 0 deletions freedata_server/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def __init__(self, config_file: str):
self.modem_events = Queue()
self.modem_fft = Queue()
self.modem_service = Queue()
self.audio_rx_queue = Queue(maxsize=10)

self.event_manager = EventManager(self, [self.modem_events])
self.state_manager = StateManager(self.state_queue)
self.schedule_manager = ScheduleManager(self)
Expand Down
26 changes: 25 additions & 1 deletion freedata_server/modem.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(self, ctx) -> None:
self.MODE = 0
self.rms_counter = 0

self.AUDIO_STREAMING_CHUNK_SIZE = 2400
self.audio_out_queue = queue.Queue()

# Make sure our resampler will work
Expand Down Expand Up @@ -349,6 +350,24 @@ def enqueue_audio_out(self, audio_48k) -> None:

return

def enqueue_streaming_audio_chunks(self, audio_block, queue):
#total_samples = len(audio_block)
#for start in range(0, total_samples, self.AUDIO_STREAMING_CHUNK_SIZE):
# end = start + self.AUDIO_STREAMING_CHUNK_SIZE
# chunk = audio_block[start:end]
# queue.put(chunk.tobytes())

block_size = self.AUDIO_STREAMING_CHUNK_SIZE

pad_length = -len(audio_block) % block_size
padded_data = np.pad(audio_block, (0, pad_length), mode='constant')
sliced_audio_data = padded_data.reshape(-1, block_size)
# add each block to audio out queue
for block in sliced_audio_data:
queue.put(block)



def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None:
"""Callback function for the audio output stream.

Expand All @@ -372,6 +391,8 @@ def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, statu
audio.calculate_fft(audio_8k, self.ctx.modem_fft, self.ctx.state_manager)
outdata[:] = chunk.reshape(outdata.shape)



else:
# reset transmitting state only, if we are not actively processing audio
# for avoiding a ptt toggle state bug
Expand Down Expand Up @@ -407,7 +428,10 @@ def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status)
try:
audio_48k = np.frombuffer(indata, dtype=np.int16)
audio_8k = self.resampler.resample48_to_8(audio_48k)
if self.ctx.config_manager.config["AUDIO"].get("rx_auto_audio_level"):

self.enqueue_streaming_audio_chunks(audio_8k, self.ctx.audio_rx_queue)

if self.ctx.config_manager.config['AUDIO'].get('rx_auto_audio_level'):
audio_8k = audio.normalize_audio(audio_8k)

audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
Expand Down
Loading
Loading