Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5940ef1

Browse files
committed
add(yasb/cava): implement Cava widget with customizable options
This commit introduces a new Cava widget for visualizing audio levels. The widget is highly configurable with options such as bar height, spacing, width, sleep timer, sensitivity, cutoff frequencies, frame rate, noise reduction, mono option, reverse mode, foreground color, gradient, gradient colors, and hide empty option. The widget also supports custom container padding.
1 parentc5ba37f commit5940ef1

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
DEFAULTS= {
2+
'bar_height':20,
3+
'bars_number':10,
4+
'output_bit_format':"16bit",
5+
'bar_spacing':1,
6+
'bar_width':3,
7+
'sleep_timer':0,
8+
'sensitivity':100,
9+
'lower_cutoff_freq':50,
10+
'higher_cutoff_freq':10000,
11+
'framerate':60,
12+
'noise_reduction':0.77,
13+
'channels':'stereo',
14+
'mono_option':'average',
15+
'reverse':0,
16+
'foreground':'#ffffff',
17+
'gradient':1,
18+
'gradient_color_1':'#74c7ec',
19+
'gradient_color_2':'#89b4fa',
20+
'gradient_color_3':'#cba6f7',
21+
'hide_empty':False,
22+
'container_padding': {'top':0,'left':0,'bottom':0,'right':0},
23+
}
24+
25+
VALIDATION_SCHEMA= {
26+
'bar_height': {
27+
'type':'integer',
28+
'required':False,
29+
'default':DEFAULTS['bar_height']
30+
},
31+
'bars_number': {
32+
'type':'integer',
33+
'required':False,
34+
'default':DEFAULTS['bars_number']
35+
},
36+
'output_bit_format': {
37+
'type':'string',
38+
'required':False,
39+
'default':DEFAULTS['output_bit_format']
40+
},
41+
'bar_spacing': {
42+
'type':'integer',
43+
'required':False,
44+
'default':DEFAULTS['bar_spacing']
45+
},
46+
'bar_width': {
47+
'type':'integer',
48+
'required':False,
49+
'default':DEFAULTS['bar_width']
50+
},
51+
'sleep_timer': {
52+
'type':'integer',
53+
'required':False,
54+
'default':DEFAULTS['sleep_timer']
55+
},
56+
'sensitivity': {
57+
'type':'integer',
58+
'required':False,
59+
'default':DEFAULTS['sensitivity']
60+
},
61+
'lower_cutoff_freq': {
62+
'type':'integer',
63+
'required':False,
64+
'default':DEFAULTS['lower_cutoff_freq']
65+
},
66+
'higher_cutoff_freq': {
67+
'type':'integer',
68+
'required':False,
69+
'default':DEFAULTS['higher_cutoff_freq']
70+
},
71+
'framerate': {
72+
'type':'integer',
73+
'required':False,
74+
'default':DEFAULTS['framerate']
75+
},
76+
'noise_reduction': {
77+
'type':'float',
78+
'required':False,
79+
'default':DEFAULTS['noise_reduction']
80+
},
81+
'channels': {
82+
'type':'string',
83+
'required':False,
84+
'default':DEFAULTS['channels']
85+
},
86+
'mono_option': {
87+
'type':'string',
88+
'required':False,
89+
'default':DEFAULTS['mono_option']
90+
},
91+
'reverse': {
92+
'type':'integer',
93+
'required':False,
94+
'default':DEFAULTS['reverse']
95+
},
96+
'foreground': {
97+
'type':'string',
98+
'required':False,
99+
'default':DEFAULTS['foreground']
100+
},
101+
'gradient': {
102+
'type':'integer',
103+
'required':False,
104+
'default':DEFAULTS['gradient']
105+
},
106+
'gradient_color_1': {
107+
'type':'string',
108+
'required':False,
109+
'default':DEFAULTS['gradient_color_1']
110+
},
111+
'gradient_color_2': {
112+
'type':'string',
113+
'required':False,
114+
'default':DEFAULTS['gradient_color_2']
115+
},
116+
'gradient_color_3': {
117+
'type':'string',
118+
'required':False,
119+
'default':DEFAULTS['gradient_color_3']
120+
},
121+
'hide_empty': {
122+
'type':'boolean',
123+
'required':False,
124+
'default':DEFAULTS['hide_empty']
125+
},
126+
'container_padding': {
127+
'type':'dict',
128+
'required':False,
129+
'schema': {
130+
'top': {
131+
'type':'integer',
132+
'default':DEFAULTS['container_padding']['top']
133+
},
134+
'left': {
135+
'type':'integer',
136+
'default':DEFAULTS['container_padding']['left']
137+
},
138+
'bottom': {
139+
'type':'integer',
140+
'default':DEFAULTS['container_padding']['bottom']
141+
},
142+
'right': {
143+
'type':'integer',
144+
'default':DEFAULTS['container_padding']['right']
145+
}
146+
},
147+
'default':DEFAULTS['container_padding']
148+
}
149+
}

‎src/core/widgets/yasb/cava.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
importos
2+
importstruct
3+
importsubprocess
4+
importtempfile
5+
importtextwrap
6+
importlogging
7+
importshutil
8+
importthreading
9+
fromcore.widgets.baseimportBaseWidget
10+
fromcore.validation.widgets.yasb.cavaimportVALIDATION_SCHEMA
11+
fromPyQt6.QtWidgetsimportQHBoxLayout,QWidget,QLabel,QApplication
12+
fromPyQt6.QtGuiimportQLinearGradient,QPainter,QColor
13+
fromPyQt6.QtCoreimportQTimer,pyqtSignal
14+
15+
classCavaBar(QWidget):
16+
def__init__(self,cava_widget):
17+
super().__init__()
18+
self._cava_widget=cava_widget
19+
self.setFixedHeight(self._cava_widget._height)
20+
self.setFixedWidth(self._cava_widget._bars_number* (self._cava_widget._bar_width+self._cava_widget._bar_spacing))
21+
self.setContentsMargins(0,0,0,0)
22+
23+
defpaintEvent(self,event):
24+
painter=QPainter(self)
25+
left_margin=self._cava_widget._bar_spacing//2
26+
fori,sampleinenumerate(self._cava_widget.samples):
27+
x=left_margin+i* (self._cava_widget._bar_width+self._cava_widget._bar_spacing)
28+
height=int(sample*self._cava_widget._height)
29+
y=self._cava_widget._height-height
30+
ifheight>0:
31+
ifself._cava_widget._gradient==1andself._cava_widget.colors:
32+
gradient=QLinearGradient(0,1,0,0)
33+
gradient.setCoordinateMode(QLinearGradient.CoordinateMode.ObjectBoundingMode)
34+
stop_step=1.0/ (len(self._cava_widget.colors)-1)
35+
foridx,colorinenumerate(self._cava_widget.colors):
36+
gradient.setColorAt(idx*stop_step,color)
37+
painter.fillRect(x,y,self._cava_widget._bar_width,height,gradient)
38+
else:
39+
painter.fillRect(x,y,self._cava_widget._bar_width,height,self._cava_widget.foreground_color)
40+
41+
classCavaWidget(BaseWidget):
42+
validation_schema=VALIDATION_SCHEMA
43+
samplesUpdated=pyqtSignal(list)
44+
45+
def__init__(
46+
self,
47+
bar_height:int,
48+
bars_number:int,
49+
output_bit_format:str,
50+
bar_spacing:int,
51+
bar_width:int,
52+
sleep_timer:int,
53+
sensitivity:int,
54+
lower_cutoff_freq:int,
55+
higher_cutoff_freq:int,
56+
framerate:int,
57+
noise_reduction:float,
58+
mono_option:str,
59+
reverse:int,
60+
channels:str,
61+
foreground:str,
62+
gradient:bool,
63+
gradient_color_1:str,
64+
gradient_color_2:str,
65+
gradient_color_3:str,
66+
hide_empty:bool,
67+
container_padding:dict[str,int],
68+
):
69+
super().__init__(class_name="cava-widget")
70+
# Widget configuration
71+
self._height=bar_height
72+
self._bars_number=bars_number
73+
self._output_bit_format=output_bit_format
74+
self._bar_spacing=bar_spacing
75+
self._bar_width=bar_width
76+
self._sleep_timer=sleep_timer
77+
self._sensitivity=sensitivity
78+
self._lower_cutoff_freq=lower_cutoff_freq
79+
self._higher_cutoff_freq=higher_cutoff_freq
80+
self._framerate=framerate
81+
self._noise_reduction=noise_reduction
82+
self._mono_option=mono_option
83+
self._reverse=reverse
84+
self._channels=channels
85+
self._foreground=foreground
86+
self._gradient=gradient
87+
self._gradient_color_1=gradient_color_1
88+
self._gradient_color_2=gradient_color_2
89+
self._gradient_color_3=gradient_color_3
90+
self._hide_empty=hide_empty
91+
self._padding=container_padding
92+
self._hide_cava_widget=True
93+
self._stop_cava=False
94+
95+
# Set up samples and colors
96+
self.samples= [0]*self._bars_number
97+
self.colors= []
98+
99+
# Construct container layout
100+
self._widget_container_layout:QHBoxLayout=QHBoxLayout()
101+
self._widget_container_layout.setSpacing(0)
102+
self._widget_container_layout.setContentsMargins(
103+
self._padding['left'],
104+
self._padding['top'],
105+
self._padding['right'],
106+
self._padding['bottom']
107+
)
108+
self._widget_container=QWidget()
109+
self._widget_container.setLayout(self._widget_container_layout)
110+
self._widget_container.setProperty("class","widget-container")
111+
self.widget_layout.addWidget(self._widget_container)
112+
113+
# Check if cava is available
114+
ifshutil.which("cava")isNone:
115+
error_label=QLabel("Cava not installed")
116+
self._widget_container_layout.addWidget(error_label)
117+
return
118+
119+
# Add the custom bar frame
120+
self._bar_frame=CavaBar(self)
121+
self._widget_container_layout.addWidget(self._bar_frame)
122+
123+
# Connect signal and start audio processing
124+
self.samplesUpdated.connect(self.on_samples_updated)
125+
self.start_cava()
126+
127+
128+
# Set up auto-hide timer for silence
129+
ifself._hide_emptyandself._sleep_timer>0:
130+
self.hide()
131+
self._hide_timer=QTimer(self)
132+
self._hide_timer.setInterval(self._sleep_timer*1000)
133+
self._hide_timer.timeout.connect(self.hide_bar_frame)
134+
135+
ifQApplication.instance():
136+
QApplication.instance().aboutToQuit.connect(self.stop_cava)
137+
138+
defstop_cava(self)->None:
139+
self._stop_cava=True
140+
ifhasattr(self,"thread_cava")andself.thread_cava.is_alive():
141+
self.thread_cava.join()
142+
143+
definitialize_colors(self)->None:
144+
self.foreground_color=QColor(self._foreground)
145+
ifself._gradient==1:
146+
forcolor_strin [self._gradient_color_1,self._gradient_color_2,self._gradient_color_3]:
147+
try:
148+
self.colors.append(QColor(color_str))
149+
exceptExceptionase:
150+
logging.error(f"Error setting gradient color '{color_str}':{e}")
151+
152+
defon_samples_updated(self,new_samples:list)->None:
153+
try:
154+
self.samples=new_samples
155+
exceptException:
156+
return
157+
ifany(val!=0forvalinnew_samples):
158+
try:
159+
ifself._hide_emptyandself._sleep_timer>0:
160+
ifself._hide_cava_widget:
161+
self.show()
162+
self._hide_cava_widget=False
163+
self._hide_timer.start()
164+
self._bar_frame.update()
165+
exceptExceptionase:
166+
logging.error(f"Error updating cava widget:{e}")
167+
168+
defhide_bar_frame(self)->None:
169+
self.hide()
170+
self._hide_cava_widget=True
171+
172+
defstart_cava(self)->None:
173+
# Build configuration file, temp config file will be created in %temp% directory
174+
config_template=textwrap.dedent(f"""\
175+
# Cava config auto-generated by YASB
176+
[general]
177+
bars ={self._bars_number}
178+
bar_spacing ={self._bar_spacing}
179+
bar_width ={self._bar_width}
180+
sleep_timer ={self._sleep_timer}
181+
sensitivity ={self._sensitivity}
182+
lower_cutoff_freq ={self._lower_cutoff_freq}
183+
higher_cutoff_freq ={self._higher_cutoff_freq}
184+
framerate ={self._framerate}
185+
noise_reduction ={self._noise_reduction}
186+
[output]
187+
method = raw
188+
bit_format ={self._output_bit_format}
189+
channels ={self._channels}
190+
mono_option ={self._mono_option}
191+
reverse ={self._reverse}
192+
[color]
193+
foreground = '{self._foreground}'
194+
gradient ={self._gradient}
195+
gradient_color_1 = '{self._gradient_color_1}'
196+
gradient_color_2 = '{self._gradient_color_2}'
197+
gradient_color_3 = '{self._gradient_color_3}'
198+
""")
199+
200+
self.initialize_colors()
201+
202+
# Determine byte type settings for reading audio data
203+
ifself._output_bit_format=="16bit":
204+
bytetype,bytesize,bytenorm= ("H",2,65535)
205+
else:
206+
bytetype,bytesize,bytenorm= ("B",1,255)
207+
208+
defprocess_audio():
209+
try:
210+
cava_config_path=os.path.join(tempfile.gettempdir(),"yasb_cava_config")
211+
withopen(cava_config_path,"w")asconfig_file:
212+
config_file.write(config_template)
213+
config_file.flush()
214+
process=subprocess.Popen(
215+
["cava","-p",cava_config_path],
216+
stdout=subprocess.PIPE,
217+
creationflags=subprocess.CREATE_NO_WINDOW
218+
)
219+
chunk=bytesize*self._bars_number
220+
fmt=bytetype*self._bars_number
221+
whileTrue:
222+
try:
223+
data=process.stdout.read(chunk)
224+
exceptExceptionase:
225+
return
226+
iflen(data)<chunk:
227+
break
228+
samples= [val/bytenormforvalinstruct.unpack(fmt,data)]
229+
ifself._stop_cava:
230+
break
231+
self.samplesUpdated.emit(samples)
232+
exceptExceptionase:
233+
logging.error(f"Error processing audio in Cava:{e}")
234+
finally:
235+
process.terminate()
236+
237+
self.thread_cava=threading.Thread(target=process_audio,daemon=True)
238+
self.thread_cava.start()

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp