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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "beatnik",
"version": "0.5.2",
"version": "0.5.3",
"author": "byrds & bytes gmbh",
"homepage": "https://beatnik.audio",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ const routes: Routes = [
path: 'setup-device-group-name/:ip',
loadChildren: () => import('./pages/setup/setup-device-group-name/setup-device-group-name.module').then( m => m.SetupDeviceGroupNamePageModule)
},
{
path: 'volume-presets',
loadChildren: () => import('./pages/volume-presets/volume-presets.module').then( m => m.VolumePresetsPageModule)
},
{
path: 'volume-preset-edit',
loadChildren: () => import('./pages/volume-preset-edit/volume-preset-edit.module').then( m => m.VolumePresetEditPageModule)
},


];
Expand Down
30 changes: 28 additions & 2 deletions src/app/components/camilla-dsp/camilla-dsp.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,33 @@ <h3>Playback</h3>
</ng-container>
</div>
</ion-card>


</div>
<!-- Add processors -->
<div *ngIf="parsedConfig">
<ion-list-header color="primary">
<ion-label>
Processors
</ion-label>
</ion-list-header>
<ion-card class="ion-no-margin ion-padding" color="primary"
*ngFor="let processor of parsedConfig.processors | keyvalue">

<ion-card-header>
<ion-card-subtitle>
{{ processor.key }}
</ion-card-subtitle>
</ion-card-header>
test
<ion-item color="primary" *ngFor="let param of processor.value.parameters | keyvalue">
<ion-label slot="start">{{ param.key }}:</ion-label>
<ion-input type="number" slot="end" [(ngModel)]="param.value"
(ionChange)="updateProcessorParameter(processor.key, param.key, param.value)"></ion-input>
</ion-item>
<!-- <ion-item color="primary"> Type: {{ processor.value.type }} </ion-item> -->
<!-- Add more processor parameters here as needed -->
</ion-card>
</div>
</div>

Expand All @@ -255,12 +281,12 @@ <h3>Playback</h3>
<ion-button (click)="getCaptureSignalLevels()">Get Capture Signal Levels</ion-button>
<ion-button (click)="setUpdateInterval(100)">Set update Interval</ion-button>
<ion-button (click)="getVolume()">Get Volume</ion-button> -->
<!-- <pre class="bg-gray-100 p-4 rounded mt-4 overflow-auto">
<pre class="bg-gray-100 p-4 rounded mt-4 overflow-auto">
{{ lastMessage | json }}
</pre>
<pre class="bg-gray-100 p-4 rounded mt-4 overflow-auto">
{{ parsedConfig | json }}
</pre> -->
</pre>
</div>
</ion-accordion>
</ion-accordion-group>
Expand Down
23 changes: 23 additions & 0 deletions src/app/components/camilla-dsp/camilla-dsp.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,5 +275,28 @@ export class CamillaDspComponent implements OnInit, OnDestroy {
this.ngOnDestroy();
}

updateProcessorParameter(processorKey: string, paramKey: string, newValue: any) {
if (!this.parsedConfig) {
console.error('No configuration loaded.');
return;
}

const processor = this.parsedConfig.processors?.[processorKey];
if (!processor) {
console.error(`Processor with key ${processorKey} not found.`);
return;
}

// Update the parameter locally
(processor.parameters as any)[paramKey] = newValue;

// format the conffigJson to send to CamillaDSP
console.log('Updated processor parameter:', processorKey, paramKey, newValue);
console.log('Updated configuration to send:', this.parsedConfig);

// send the full configJson back to CamillaDSP
this.camillaService.sendCommand('SetConfigJson', JSON.stringify(this.parsedConfig));
}


}
20 changes: 20 additions & 0 deletions src/app/components/client-info/client-info.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,26 @@ <h3>Status: Inactive</h3>
Choose Speakers
</ion-button>

<!-- Test Buttons for new System API -->
<ion-item-divider>
<ion-label>Hardware API Test</ion-label>
</ion-item-divider>

<ion-button expand="full" color="medium" (click)="testGetSystemInfo()">
Get System Info (Console)
</ion-button>
<ion-button expand="full" color="danger" (click)="testLedSetColor()">
LED: Solid Red
</ion-button>
<ion-button expand="full" color="success" (click)="testLedPulse()">
LED: Pulse Green/Blue
</ion-button>
<ion-button expand="full" color="warning" (click)="testLedBlink()">
LED: Blink Yellow
</ion-button>
<ion-button expand="full" color="dark" (click)="testLedOff()">
LED: Turn Off
</ion-button>

</ng-container>
<ng-container *ngIf="segment === 'camilla-dsp'">
Expand Down
47 changes: 47 additions & 0 deletions src/app/components/client-info/client-info.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { UserPreference } from 'src/app/enum/user-preference.enum';
import { SnapCastServerStatusResponse, Client } from 'src/app/model/snapcast.model';
import { BeatnikHardwareService, HardwareStatus } from 'src/app/services/beatnik-hardware.service';
import { BeatnikSnapcastService, SnapcastActionResponse } from 'src/app/services/beatnik-snapcast.service';
import { BeatnikSystemService } from 'src/app/services/beatnik-system.service';
import { CamillaDspService } from 'src/app/services/camilla-dsp.service';
import { SnapcastService } from 'src/app/services/snapcast.service';
import { SoundcardPickerComponent } from '../soundcard-picker/soundcard-picker.component';
Expand Down Expand Up @@ -36,6 +37,7 @@ export class ClientInfoComponent implements OnInit {
private modalController: ModalController,
private beatnikHardwareService: BeatnikHardwareService,
private beatnikSnapcastService: BeatnikSnapcastService,
private beatnikSystemService: BeatnikSystemService,
private camillaService: CamillaDspService,
private alertController: AlertController
) { }
Expand Down Expand Up @@ -287,6 +289,51 @@ export class ClientInfoComponent implements OnInit {
console.log('Client Info Component: Speaker selection cancelled or no selection made');
}
}

async testGetSystemInfo() {
if (!this.client) return;
const localHostName = await this.getUrl();
this.beatnikSystemService.getInfo(localHostName).subscribe({
next: (info) => console.log('System Info:', info),
error: (err) => console.error('Failed to get system info:', err)
});
}

async testLedSetColor() {
if (!this.client) return;
const localHostName = await this.getUrl();
this.beatnikSystemService.setLedState({ command: 'set_color', params: { r: 1, g: 0, b: 0 } }, localHostName).subscribe({
next: (res) => console.log('LED Set Color:', res),
error: (err) => console.error('Failed to set LED color:', err)
});
}

async testLedPulse() {
if (!this.client) return;
const localHostName = await this.getUrl();
this.beatnikSystemService.setLedState({ command: 'pulse', params: { on_color: [0, 1, 0], off_color: [0, 0, 1], fade_in: 1, fade_out: 1 } }, localHostName).subscribe({
next: (res) => console.log('LED Pulse:', res),
error: (err) => console.error('Failed to pulse LED:', err)
});
}

async testLedBlink() {
if (!this.client) return;
const localHostName = await this.getUrl();
this.beatnikSystemService.setLedState({ command: 'blink', params: { color: [1, 1, 0], on_time: 0.5, off_time: 0.5 } }, localHostName).subscribe({
next: (res) => console.log('LED Blink:', res),
error: (err) => console.error('Failed to blink LED:', err)
});
}

async testLedOff() {
if (!this.client) return;
const localHostName = await this.getUrl();
this.beatnikSystemService.setLedState({ command: 'off' }, localHostName).subscribe({
next: (res) => console.log('LED Off:', res),
error: (err) => console.error('Failed to turn off LED:', err)
});
}
}


Expand Down
10 changes: 5 additions & 5 deletions src/app/components/player-toolbar/player-toolbar.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<ion-content color="primary">
<ion-toolbar class="play-toolbar" color="primary">
<ng-container *ngIf="(displayState$|async) as state ">
<ng-container *ngFor="let stream of state.server.streams">
<ng-container *ngFor="let stream of state.server.streams; trackBy: trackByStream">
<ion-item *ngIf="stream.properties.metadata" class="ion-padding-top" lines="none" color="primary">
<ion-thumbnail slot="start">
<img [src]="stream.properties.metadata.artData
Expand All @@ -27,18 +27,18 @@ <h2> {{ stream.properties.metadata.title }}</h2>
<app-snapcast-stream-volume-control class="stream-range" (streamVolumeChange)="streamVolumeChanged()"
[stream]="stream" [groups]="state.server.groups "></app-snapcast-stream-volume-control>
</ion-item> -->
<ng-container *ngFor="let group of state.server.groups">
<ng-container *ngFor="let group of state.server.groups; trackBy: trackByGroup">
<ng-container *ngIf="group.clients[0].connected">
<ion-item *ngIf="group.stream_id === stream.id" lines="none" color="primary">
<ion-label>
<p>
<app-client-name [group]="group"></app-client-name>
</p>
<p>Clients: {{ group.clients.length }}</p>
<ng-container *ngFor="let client of group.clients">
<ng-container *ngFor="let client of group.clients; trackBy: trackByClient">
<ion-range *ngIf="client.config.volume" color="light" min="0" max="100" step="1"
[ngModel]="client.config.volume.percent" (ionKnobMoveStart)="knobMoveStartEvent($event)"
(ionKnobMoveEnd)="knobMoveEndEvent($event)" (ionInput)="changeVolumeForClient(client, $event)">
[ngModel]="client.config.volume.percent" (ionKnobMoveStart)="knobMoveStartEvent(client.id, $event)"
(ionKnobMoveEnd)="knobMoveEndEvent(client.id, $event)" (ionInput)="changeVolumeForClient(client, $event)">
<ion-icon slot="start" size="small" name="volume-mute"></ion-icon>
<ion-icon slot="end" size="small" name="volume-high"></ion-icon>
</ion-range>
Expand Down
70 changes: 55 additions & 15 deletions src/app/components/player-toolbar/player-toolbar.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// player-toolbar.component.ts
import { Component, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; // OnChanges und SimpleChanges hinzugefügt
import { Observable, Subscription, tap, firstValueFrom } from 'rxjs'; // firstValueFrom hinzugefügt
import { Observable, Subscription, tap, firstValueFrom, map } from 'rxjs'; // firstValueFrom hinzugefügt
import { Group, Stream, ServerDetail, Client, SnapCastServerStatusResponse } from 'src/app/model/snapcast.model'; // Client importiert für Typisierung
import { SnapcastService } from 'src/app/services/snapcast.service';
import { Haptics, ImpactStyle, NotificationType } from '@capacitor/haptics';
Expand All @@ -21,8 +21,10 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy {
private subscriptions = new Subscription();

// Make sure it's this device that changes the volume. If multiple devices are connected, we want to avoid conflicts.
private knobMoveStart = false;
private knobMoveEnd = false;
private draggingClients = new Map<string, boolean>();
private lastDragEndTimes = new Map<string, number>();
private optimisticVolumes = new Map<string, number>();
private readonly VOLUME_COOLDOWN_MS = 5000;



Expand All @@ -35,7 +37,35 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy {
}

ngOnInit(): void {
this.displayState$ = this.snapcastService.state$
this.displayState$ = this.snapcastService.state$.pipe(
map(state => {
if (!state) return state;

// Deep clone to avoid mutating the service's state
const modifiedState = JSON.parse(JSON.stringify(state));

modifiedState.server.groups.forEach((group: Group) => {
group.clients.forEach((client: Client) => {
const isDragging = this.draggingClients.get(client.id);
const lastDragEnd = this.lastDragEndTimes.get(client.id) || 0;
const inCooldown = (Date.now() - lastDragEnd) < this.VOLUME_COOLDOWN_MS;

if (isDragging || inCooldown) {
// Retain optimistic volume to prevent jumping backward
const optVol = this.optimisticVolumes.get(client.id);
if (optVol !== undefined) {
client.config.volume.percent = optVol;
}
} else {
// Store the definitive server truth if we are out of cooldown
this.optimisticVolumes.set(client.id, client.config.volume.percent);
}
});
});

return modifiedState;
})
);
this.snapcastService.connect();
}

Expand All @@ -49,8 +79,8 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy {
* @param event The event emitted from the range slider (e.g., ionChange).
*/
changeVolumeForClient(client: Client, event: any): void {
if (!this.knobMoveStart) {
console.warn('PlayerToolbarComponent: changeVolumeForClient called without knobMoveStart. Ignoring event:', event);
if (!this.draggingClients.get(client.id)) {
console.warn(`PlayerToolbarComponent: changeVolumeForClient called without knobMoveStart for ${client.id}. Ignoring event.`, event);
return;
}
// Step 1: Robustly extract the numerical value from the event.
Expand All @@ -70,6 +100,9 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy {
return;
}

// Immediately track the optimistic volume so the local view freezes on the current slider value
this.optimisticVolumes.set(client.id, newVolume);

console.log(`PlayerToolbarComponent: Setting desired volume for client ${client.id} to ${newVolume}`);

// Step 2: Call the service method.
Expand Down Expand Up @@ -136,25 +169,32 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy {
return this.coverDateService.convertCoverDataBase64(coverData, extension);
}

knobMoveStartEvent(event: any): void {
console.log('Knob move started:', event);
this.knobMoveStart = true;
this.knobMoveEnd = false;
knobMoveStartEvent(clientId: string, event: any): void {
console.log(`Knob move started for client ${clientId}:`, event);
this.draggingClients.set(clientId, true);
}

knobMoveEndEvent(event: any): void {
console.log('Knob move ended:', event);
this.knobMoveEnd = true;
this.knobMoveStart = false;
knobMoveEndEvent(clientId: string, event: any): void {
console.log(`Knob move ended for client ${clientId}:`, event);
this.draggingClients.set(clientId, false);
this.lastDragEndTimes.set(clientId, Date.now());
// Optionally, you can add haptic feedback here
}

onCoverImageError(event: Event): void {
this.coverDateService.onCoverImageError(event);
}

trackByStream(index: number, stream: Stream): string {
return stream.id;
}

trackByGroup(index: number, group: Group): string {
return group.id;
}


trackByClient(index: number, client: Client): string {
return client.id;
}

}
1 change: 1 addition & 0 deletions src/app/enum/user-preference.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
export enum UserPreference {
USERNAME = 'username',
SERVER_URL = 'serverUrl',
VOLUME_PRESETS = 'volumePresets',
}
Loading
Loading