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.0",
"version": "0.5.1",
"author": "byrds & bytes gmbh",
"homepage": "https://beatnik.audio",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/app/client-name/client-name.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>
{{group.name || group.clients[0].host.name}}
</p>
Empty file.
24 changes: 24 additions & 0 deletions src/app/client-name/client-name.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';

import { ClientNameComponent } from './client-name.component';

describe('ClientNameComponent', () => {
let component: ClientNameComponent;
let fixture: ComponentFixture<ClientNameComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ClientNameComponent ],
imports: [IonicModule.forRoot()]
}).compileComponents();

fixture = TestBed.createComponent(ClientNameComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));

it('should create', () => {
expect(component).toBeTruthy();
});
});
18 changes: 18 additions & 0 deletions src/app/client-name/client-name.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Component, Input, OnInit } from '@angular/core';
import { Group } from '../model/snapcast.model';

@Component({
selector: 'app-client-name',
templateUrl: './client-name.component.html',
styleUrls: ['./client-name.component.scss'],
standalone: false
})
export class ClientNameComponent implements OnInit {

@Input() group: Group;

constructor() { }

ngOnInit() {}

}
18 changes: 18 additions & 0 deletions src/app/client-name/client-name.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ClientNameComponent } from './client-name.component';



@NgModule({
declarations: [
ClientNameComponent
],
imports: [
CommonModule
],
exports: [
ClientNameComponent
]
})
export class ClientNameModule { }
100 changes: 79 additions & 21 deletions src/app/components/camilla-dsp/camilla-dsp.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<div class="row g-0">
<div class="col col-12 col-lg-12">



<div *ngIf="levels" class="meters-section">
<ion-list-header color="primary">
<ion-label>
Expand Down Expand Up @@ -163,7 +165,7 @@ <h3>Playback</h3>
*ngFor="let filter of parsedConfig.filters | keyvalue">
<ion-card-header lines="none">
<ion-card-subtitle>
{{ filter.key }}
{{ filter.key }}
</ion-card-subtitle>
</ion-card-header>
<ion-item color="primary"> Type: {{ filter.value.type }} </ion-item>
Expand Down Expand Up @@ -231,27 +233,83 @@ <h3>Playback</h3>
</div>


<div class="p-4">
<h1 class="text-2xl font-bold mb-4">CamillaDSP Control</h1>
<ion-input placeholder="WebSocket URL" [(ngModel)]="url"></ion-input>
<p class="mb-2">
Connection Status: <strong>{{ connectionStatus }}</strong>
</p>
<!-- <ion-button (click)="connect()">Connect</ion-button>
<ion-button (click)="disconnect()">Disconnect</ion-button>
<ion-button (click)="getState()">Get State</ion-button>
<ion-button (click)="getConfigJson()">Get Config JSON</ion-button>
<ion-button (click)="getConfigYaml()">Get Config YAML</ion-button>
<ion-button (click)="getUpdateInterval()">Get Update Interval</ion-button>
<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> -->
<ion-accordion-group class="ion-margin-top">
<ion-accordion value="camilla-dsp-control">
<ion-item slot="header">
<ion-label>CamillaDSP Connection</ion-label>
</ion-item>

<div class="ion-padding" slot="content">
<ion-card-title class="ion-padding-top">WebSocket Connection</ion-card-title>

<ion-input placeholder="WebSocket URL" [(ngModel)]="url"></ion-input>
<p class="mb-2">
Connection Status: <strong>{{ connectionStatus }}</strong>
</p>
<!-- <ion-button (click)="connect()">Connect</ion-button>
<ion-button (click)="disconnect()">Disconnect</ion-button>
<ion-button (click)="getState()">Get State</ion-button>
<ion-button (click)="getConfigJson()">Get Config JSON</ion-button>
<ion-button (click)="getConfigYaml()">Get Config YAML</ion-button>
<ion-button (click)="getUpdateInterval()">Get Update Interval</ion-button>
<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">
{{ lastMessage | json }}
</pre>
<pre class="bg-gray-100 p-4 rounded mt-4 overflow-auto">
{{ parsedConfig | json }}
</pre> -->
</div>
</ion-accordion>
</ion-accordion-group>

<ion-accordion-group class="ion-margin-bottom">
<ion-accordion value="camilla-dsp-config">
<ion-item slot="header">
<ion-label>Advanced CamillaDSP Config</ion-label>
</ion-item>

</div>
<div class="ion-padding" slot="content">
<ion-card-title>Default Config</ion-card-title>
<p>Manage the default config file used on this device. Only use this if you know what you are doing.</p>
<ion-item>
<ion-label position="stacked">Current Default Config</ion-label>
<ion-note color="medium">{{ currentCamillaConfigFile || 'No default config selected' }}</ion-note>
</ion-item>

<ion-item>
<ion-label position="stacked">Available Config Files</ion-label>
<ion-select [(ngModel)]="selectedCamillaConfig" placeholder="Select CamillaDSP config"
[disabled]="isLoadingCamillaConfigs || isSavingCamillaConfig || availableCamillaConfigs.length === 0">
<ion-select-option *ngFor="let config of availableCamillaConfigs" [value]="config">
{{ config }}
</ion-select-option>
</ion-select>
</ion-item>

<!-- <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> -->
<ion-text color="medium" *ngIf="isLoadingCamillaConfigs">
<p>Loading CamillaDSP configs...</p>
</ion-text>

<ion-text color="danger" *ngIf="camillaConfigError">
<p>{{ camillaConfigError }}</p>
</ion-text>

<ion-text color="success" *ngIf="camillaConfigMessage">
<p>{{ camillaConfigMessage }}</p>
</ion-text>

<ion-button expand="block" color="medium" fill="outline" (click)="loadCamillaConfigState()"
[disabled]="isLoadingCamillaConfigs || isSavingCamillaConfig">
Refresh Config List
</ion-button>

<ion-button expand="block" color="primary" (click)="saveDefaultCamillaConfig()"
[disabled]="!selectedCamillaConfig || isLoadingCamillaConfigs || isSavingCamillaConfig">
Set Default Config
</ion-button>
</div>
</ion-accordion>
</ion-accordion-group>
109 changes: 108 additions & 1 deletion src/app/components/camilla-dsp/camilla-dsp.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { CamillaDspConfig, MixerSource, SignalLevels } from 'src/app/model/camilla-dsp.model';
import { BeatnikHardwareService } from 'src/app/services/beatnik-hardware.service';
import { CamillaDspService, ConnectionStatus } from 'src/app/services/camilla-dsp.service';

@Component({
Expand All @@ -21,12 +22,23 @@ export class CamillaDspComponent implements OnInit, OnDestroy {

levels: SignalLevels | null = null;
currentVolume: number = 0;
availableCamillaConfigs: string[] = [];
selectedCamillaConfig: string | null = null;
currentCamillaConfigFile: string | null = null;
isLoadingCamillaConfigs = false;
isSavingCamillaConfig = false;
camillaConfigMessage = '';
camillaConfigError = '';

private levelSubscription: Subscription | undefined;

constructor(private camillaService: CamillaDspService) { }
constructor(
private camillaService: CamillaDspService,
private beatnikHardwareService: BeatnikHardwareService
) { }

async ngOnInit() {
this.loadCamillaConfigState();
this.connect();
// Subscribe to connection status changes
this.subscriptions.add(
Expand Down Expand Up @@ -77,6 +89,99 @@ export class CamillaDspComponent implements OnInit, OnDestroy {
// }, 800);
}

private getHardwareHost(): string | null {
if (!this.url) {
return null;
}

try {
return new URL(this.url).hostname;
} catch (error) {
console.error('Failed to parse CamillaDSP URL for hardware API access:', error);
return null;
}
}

loadCamillaConfigState() {
const host = this.getHardwareHost();
if (!host) {
this.camillaConfigError = 'Unable to determine device host for CamillaDSP config API.';
return;
}

this.isLoadingCamillaConfigs = true;
this.camillaConfigError = '';
this.camillaConfigMessage = '';

this.subscriptions.add(
this.beatnikHardwareService.getCamillaConfigs(host).subscribe({
next: (response) => {
this.availableCamillaConfigs = response.configs ?? [];
this.syncSelectedCamillaConfig();
this.isLoadingCamillaConfigs = false;
},
error: (error) => {
console.error('Failed to load available CamillaDSP configs:', error);
this.camillaConfigError = 'Failed to load available CamillaDSP configs.';
this.isLoadingCamillaConfigs = false;
}
})
);

this.subscriptions.add(
this.beatnikHardwareService.getDefaultCamillaConfig(host).subscribe({
next: (response) => {
this.currentCamillaConfigFile = response.fileName;
this.syncSelectedCamillaConfig();
},
error: (error) => {
console.error('Failed to load default CamillaDSP config:', error);
this.camillaConfigError = 'Failed to load current default CamillaDSP config.';
}
})
);
}

private syncSelectedCamillaConfig() {
if (this.currentCamillaConfigFile && this.availableCamillaConfigs.includes(this.currentCamillaConfigFile)) {
this.selectedCamillaConfig = this.currentCamillaConfigFile;
return;
}

if (!this.selectedCamillaConfig && this.availableCamillaConfigs.length > 0) {
this.selectedCamillaConfig = this.availableCamillaConfigs[0];
}
}

saveDefaultCamillaConfig() {
const host = this.getHardwareHost();

if (!host || !this.selectedCamillaConfig) {
this.camillaConfigError = 'Please choose a CamillaDSP config before saving.';
return;
}

this.isSavingCamillaConfig = true;
this.camillaConfigError = '';
this.camillaConfigMessage = '';

this.subscriptions.add(
this.beatnikHardwareService.setDefaultCamillaConfig(this.selectedCamillaConfig, host).subscribe({
next: (response) => {
this.currentCamillaConfigFile = response.fileName;
this.selectedCamillaConfig = response.fileName;
this.camillaConfigMessage = `Default config set to ${response.fileName}.`;
this.isSavingCamillaConfig = false;
},
error: (error) => {
console.error('Failed to update default CamillaDSP config:', error);
this.camillaConfigError = error?.error?.error ?? 'Failed to update default CamillaDSP config.';
this.isSavingCamillaConfig = false;
}
})
);
}



connect() {
Expand Down Expand Up @@ -169,4 +274,6 @@ export class CamillaDspComponent implements OnInit, OnDestroy {
console.log('CamillaDspComponent: Leaving page, cleaning up resources if needed');
this.ngOnDestroy();
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ion-icon slot="end" [name]="'checkmark-circle'" *ngIf="selectedId === speaker.id"></ion-icon>
<ion-label> <h2>{{ speaker.manufacturer }} {{ speaker.model }}</h2></ion-label>
</ion-item>

</div>
</div>

Expand Down
14 changes: 3 additions & 11 deletions src/app/components/choose-speakers/choose-speakers.component.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, Input, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IonicModule, ModalController } from '@ionic/angular';
import { ModalController } from '@ionic/angular';
import { first, firstValueFrom } from 'rxjs';
import { Speaker } from 'src/app/model/speaker.model';
import { SnapcastService } from 'src/app/services/snapcast.service';

@Component({
selector: 'app-choose-speakers',
templateUrl: './choose-speakers.component.html',
styleUrls: ['./choose-speakers.component.scss'],
// import ionic module here if needed
imports: [
IonicModule,
FormsModule,
CommonModule
],
standalone: true
styleUrls: ['./choose-speakers.component.scss'],
standalone: false
})
export class ChooseSpeakersComponent implements OnInit {

Expand Down
Loading
Loading