1 | /**************************************************************************/ |
2 | /* audio_stream_ogg_vorbis.cpp */ |
3 | /**************************************************************************/ |
4 | /* This file is part of: */ |
5 | /* GODOT ENGINE */ |
6 | /* https://godotengine.org */ |
7 | /**************************************************************************/ |
8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
10 | /* */ |
11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
12 | /* a copy of this software and associated documentation files (the */ |
13 | /* "Software"), to deal in the Software without restriction, including */ |
14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
17 | /* the following conditions: */ |
18 | /* */ |
19 | /* The above copyright notice and this permission notice shall be */ |
20 | /* included in all copies or substantial portions of the Software. */ |
21 | /* */ |
22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
29 | /**************************************************************************/ |
30 | |
31 | #include "audio_stream_ogg_vorbis.h" |
32 | |
33 | #include "core/io/file_access.h" |
34 | #include "core/variant/typed_array.h" |
35 | |
36 | #include "modules/vorbis/resource_importer_ogg_vorbis.h" |
37 | #include <ogg/ogg.h> |
38 | |
39 | int AudioStreamPlaybackOggVorbis::_mix_internal(AudioFrame *p_buffer, int p_frames) { |
40 | ERR_FAIL_COND_V(!ready, 0); |
41 | |
42 | if (!active) { |
43 | return 0; |
44 | } |
45 | |
46 | int todo = p_frames; |
47 | |
48 | int beat_length_frames = -1; |
49 | bool beat_loop = vorbis_stream->has_loop(); |
50 | if (beat_loop && vorbis_stream->get_bpm() > 0 && vorbis_stream->get_beat_count() > 0) { |
51 | beat_length_frames = vorbis_stream->get_beat_count() * vorbis_data->get_sampling_rate() * 60 / vorbis_stream->get_bpm(); |
52 | } |
53 | |
54 | while (todo > 0 && active) { |
55 | AudioFrame *buffer = p_buffer; |
56 | buffer += p_frames - todo; |
57 | |
58 | int to_mix = todo; |
59 | if (beat_length_frames >= 0 && (beat_length_frames - (int)frames_mixed) < to_mix) { |
60 | to_mix = MAX(0, beat_length_frames - (int)frames_mixed); |
61 | } |
62 | |
63 | int mixed = _mix_frames_vorbis(buffer, to_mix); |
64 | ERR_FAIL_COND_V(mixed < 0, 0); |
65 | todo -= mixed; |
66 | frames_mixed += mixed; |
67 | |
68 | if (loop_fade_remaining < FADE_SIZE) { |
69 | int to_fade = loop_fade_remaining + MIN(FADE_SIZE - loop_fade_remaining, mixed); |
70 | for (int i = loop_fade_remaining; i < to_fade; i++) { |
71 | buffer[i - loop_fade_remaining] += loop_fade[i] * (float(FADE_SIZE - i) / float(FADE_SIZE)); |
72 | } |
73 | loop_fade_remaining = to_fade; |
74 | } |
75 | |
76 | if (beat_length_frames >= 0) { |
77 | /** |
78 | * Length determined by beat length |
79 | * This code is commented out because, in practice, it is preferred that the fade |
80 | * is done by the transitioner and this stream just goes on until it ends while fading out. |
81 | * |
82 | * End fade implementation is left here for reference in case at some point this feature |
83 | * is desired. |
84 | |
85 | if (!beat_loop && (int)frames_mixed > beat_length_frames - FADE_SIZE) { |
86 | print_line("beat length fade/after mix?"); |
87 | //No loop, just fade and finish |
88 | for (int i = 0; i < mixed; i++) { |
89 | int idx = frames_mixed + i - mixed; |
90 | buffer[i] *= 1.0 - float(MAX(0, (idx - (beat_length_frames - FADE_SIZE)))) / float(FADE_SIZE); |
91 | } |
92 | if ((int)frames_mixed == beat_length_frames) { |
93 | for (int i = p_frames - todo; i < p_frames; i++) { |
94 | p_buffer[i] = AudioFrame(0, 0); |
95 | } |
96 | active = false; |
97 | break; |
98 | } |
99 | } else |
100 | **/ |
101 | |
102 | if (beat_loop && beat_length_frames <= (int)frames_mixed) { |
103 | // End of file when doing beat-based looping. <= used instead of == because importer editing |
104 | if (!have_packets_left && !have_samples_left) { |
105 | //Nothing remaining, so do nothing. |
106 | loop_fade_remaining = FADE_SIZE; |
107 | } else { |
108 | // Add some loop fade; |
109 | int faded_mix = _mix_frames_vorbis(loop_fade, FADE_SIZE); |
110 | |
111 | for (int i = faded_mix; i < FADE_SIZE; i++) { |
112 | // In case lesss was mixed, pad with zeros |
113 | loop_fade[i] = AudioFrame(0, 0); |
114 | } |
115 | loop_fade_remaining = 0; |
116 | } |
117 | |
118 | seek(vorbis_stream->loop_offset); |
119 | loops++; |
120 | // We still have buffer to fill, start from this element in the next iteration. |
121 | continue; |
122 | } |
123 | } |
124 | |
125 | if (!have_packets_left && !have_samples_left) { |
126 | // Actual end of file! |
127 | bool is_not_empty = mixed > 0 || vorbis_stream->get_length() > 0; |
128 | if (vorbis_stream->loop && is_not_empty) { |
129 | //loop |
130 | |
131 | seek(vorbis_stream->loop_offset); |
132 | loops++; |
133 | // We still have buffer to fill, start from this element in the next iteration. |
134 | |
135 | } else { |
136 | for (int i = p_frames - todo; i < p_frames; i++) { |
137 | p_buffer[i] = AudioFrame(0, 0); |
138 | } |
139 | active = false; |
140 | } |
141 | } |
142 | } |
143 | return p_frames - todo; |
144 | } |
145 | |
146 | int AudioStreamPlaybackOggVorbis::_mix_frames_vorbis(AudioFrame *p_buffer, int p_frames) { |
147 | ERR_FAIL_COND_V(!ready, 0); |
148 | if (!have_samples_left) { |
149 | ogg_packet *packet = nullptr; |
150 | int err; |
151 | |
152 | if (!vorbis_data_playback->next_ogg_packet(&packet)) { |
153 | have_packets_left = false; |
154 | WARN_PRINT("ran out of packets in stream" ); |
155 | return -1; |
156 | } |
157 | |
158 | err = vorbis_synthesis(&block, packet); |
159 | ERR_FAIL_COND_V_MSG(err != 0, 0, "Error during vorbis synthesis " + itos(err)); |
160 | |
161 | err = vorbis_synthesis_blockin(&dsp_state, &block); |
162 | ERR_FAIL_COND_V_MSG(err != 0, 0, "Error during vorbis block processing " + itos(err)); |
163 | |
164 | have_packets_left = !packet->e_o_s; |
165 | } |
166 | |
167 | float **pcm; // Accessed with pcm[channel_idx][sample_idx]. |
168 | |
169 | int frames = vorbis_synthesis_pcmout(&dsp_state, &pcm); |
170 | if (frames > p_frames) { |
171 | frames = p_frames; |
172 | have_samples_left = true; |
173 | } else { |
174 | have_samples_left = false; |
175 | } |
176 | |
177 | if (info.channels > 1) { |
178 | for (int frame = 0; frame < frames; frame++) { |
179 | p_buffer[frame].l = pcm[0][frame]; |
180 | p_buffer[frame].r = pcm[1][frame]; |
181 | } |
182 | } else { |
183 | for (int frame = 0; frame < frames; frame++) { |
184 | p_buffer[frame].l = pcm[0][frame]; |
185 | p_buffer[frame].r = pcm[0][frame]; |
186 | } |
187 | } |
188 | vorbis_synthesis_read(&dsp_state, frames); |
189 | return frames; |
190 | } |
191 | |
192 | float AudioStreamPlaybackOggVorbis::get_stream_sampling_rate() { |
193 | return vorbis_data->get_sampling_rate(); |
194 | } |
195 | |
196 | bool AudioStreamPlaybackOggVorbis::_alloc_vorbis() { |
197 | vorbis_info_init(&info); |
198 | info_is_allocated = true; |
199 | vorbis_comment_init(&comment); |
200 | comment_is_allocated = true; |
201 | |
202 | ERR_FAIL_COND_V(vorbis_data.is_null(), false); |
203 | vorbis_data_playback = vorbis_data->instantiate_playback(); |
204 | |
205 | ogg_packet *packet; |
206 | int err; |
207 | |
208 | for (int i = 0; i < 3; i++) { |
209 | if (!vorbis_data_playback->next_ogg_packet(&packet)) { |
210 | WARN_PRINT("Not enough packets to parse header" ); |
211 | return false; |
212 | } |
213 | |
214 | err = vorbis_synthesis_headerin(&info, &comment, packet); |
215 | ERR_FAIL_COND_V_MSG(err != 0, false, "Error parsing header" ); |
216 | } |
217 | |
218 | err = vorbis_synthesis_init(&dsp_state, &info); |
219 | ERR_FAIL_COND_V_MSG(err != 0, false, "Error initializing dsp state" ); |
220 | dsp_state_is_allocated = true; |
221 | |
222 | err = vorbis_block_init(&dsp_state, &block); |
223 | ERR_FAIL_COND_V_MSG(err != 0, false, "Error initializing block" ); |
224 | block_is_allocated = true; |
225 | |
226 | ready = true; |
227 | |
228 | return true; |
229 | } |
230 | |
231 | void AudioStreamPlaybackOggVorbis::start(double p_from_pos) { |
232 | ERR_FAIL_COND(!ready); |
233 | loop_fade_remaining = FADE_SIZE; |
234 | active = true; |
235 | seek(p_from_pos); |
236 | loops = 0; |
237 | begin_resample(); |
238 | } |
239 | |
240 | void AudioStreamPlaybackOggVorbis::stop() { |
241 | active = false; |
242 | } |
243 | |
244 | bool AudioStreamPlaybackOggVorbis::is_playing() const { |
245 | return active; |
246 | } |
247 | |
248 | int AudioStreamPlaybackOggVorbis::get_loop_count() const { |
249 | return loops; |
250 | } |
251 | |
252 | double AudioStreamPlaybackOggVorbis::get_playback_position() const { |
253 | return double(frames_mixed) / (double)vorbis_data->get_sampling_rate(); |
254 | } |
255 | |
256 | void AudioStreamPlaybackOggVorbis::tag_used_streams() { |
257 | vorbis_stream->tag_used(get_playback_position()); |
258 | } |
259 | |
260 | void AudioStreamPlaybackOggVorbis::seek(double p_time) { |
261 | ERR_FAIL_COND(!ready); |
262 | ERR_FAIL_COND(vorbis_stream.is_null()); |
263 | if (!active) { |
264 | return; |
265 | } |
266 | |
267 | vorbis_synthesis_restart(&dsp_state); |
268 | |
269 | if (p_time >= vorbis_stream->get_length()) { |
270 | p_time = 0; |
271 | } |
272 | frames_mixed = uint32_t(vorbis_data->get_sampling_rate() * p_time); |
273 | |
274 | const int64_t desired_sample = p_time * get_stream_sampling_rate(); |
275 | |
276 | if (!vorbis_data_playback->seek_page(desired_sample)) { |
277 | WARN_PRINT("seek failed" ); |
278 | return; |
279 | } |
280 | |
281 | ogg_packet *packet; |
282 | if (!vorbis_data_playback->next_ogg_packet(&packet)) { |
283 | WARN_PRINT_ONCE("seeking beyond limits" ); |
284 | return; |
285 | } |
286 | |
287 | // The granule position of the page we're seeking through. |
288 | int64_t granule_pos = 0; |
289 | |
290 | int headers_remaining = 0; |
291 | int samples_in_page = 0; |
292 | int err; |
293 | while (true) { |
294 | if (vorbis_synthesis_idheader(packet)) { |
295 | headers_remaining = 3; |
296 | } |
297 | if (!headers_remaining) { |
298 | err = vorbis_synthesis(&block, packet); |
299 | ERR_FAIL_COND_MSG(err != 0, "Error during vorbis synthesis " + itos(err)); |
300 | |
301 | err = vorbis_synthesis_blockin(&dsp_state, &block); |
302 | ERR_FAIL_COND_MSG(err != 0, "Error during vorbis block processing " + itos(err)); |
303 | |
304 | int samples_out = vorbis_synthesis_pcmout(&dsp_state, nullptr); |
305 | err = vorbis_synthesis_read(&dsp_state, samples_out); |
306 | ERR_FAIL_COND_MSG(err != 0, "Error during vorbis read updating " + itos(err)); |
307 | |
308 | samples_in_page += samples_out; |
309 | |
310 | } else { |
311 | headers_remaining--; |
312 | } |
313 | if (packet->granulepos != -1 && headers_remaining == 0) { |
314 | // This indicates the end of the page. |
315 | granule_pos = packet->granulepos; |
316 | break; |
317 | } |
318 | if (packet->e_o_s) { |
319 | break; |
320 | } |
321 | if (!vorbis_data_playback->next_ogg_packet(&packet)) { |
322 | // We should get an e_o_s flag before this happens. |
323 | WARN_PRINT("Vorbis file ended without warning." ); |
324 | break; |
325 | } |
326 | } |
327 | |
328 | int64_t samples_to_burn = samples_in_page - (granule_pos - desired_sample); |
329 | |
330 | if (samples_to_burn > samples_in_page) { |
331 | WARN_PRINT_ONCE("Burning more samples than we have in this page. Check seek algorithm." ); |
332 | } else if (samples_to_burn < 0) { |
333 | WARN_PRINT_ONCE("Burning negative samples doesn't make sense. Check seek algorithm." ); |
334 | } |
335 | |
336 | // Seek again, this time we'll burn a specific number of samples instead of all of them. |
337 | if (!vorbis_data_playback->seek_page(desired_sample)) { |
338 | WARN_PRINT("seek failed" ); |
339 | return; |
340 | } |
341 | |
342 | if (!vorbis_data_playback->next_ogg_packet(&packet)) { |
343 | WARN_PRINT_ONCE("seeking beyond limits" ); |
344 | return; |
345 | } |
346 | vorbis_synthesis_restart(&dsp_state); |
347 | |
348 | while (true) { |
349 | if (vorbis_synthesis_idheader(packet)) { |
350 | headers_remaining = 3; |
351 | } |
352 | if (!headers_remaining) { |
353 | err = vorbis_synthesis(&block, packet); |
354 | ERR_FAIL_COND_MSG(err != 0, "Error during vorbis synthesis " + itos(err)); |
355 | |
356 | err = vorbis_synthesis_blockin(&dsp_state, &block); |
357 | ERR_FAIL_COND_MSG(err != 0, "Error during vorbis block processing " + itos(err)); |
358 | |
359 | int samples_out = vorbis_synthesis_pcmout(&dsp_state, nullptr); |
360 | int read_samples = samples_to_burn > samples_out ? samples_out : samples_to_burn; |
361 | err = vorbis_synthesis_read(&dsp_state, samples_out); |
362 | ERR_FAIL_COND_MSG(err != 0, "Error during vorbis read updating " + itos(err)); |
363 | samples_to_burn -= read_samples; |
364 | |
365 | if (samples_to_burn <= 0) { |
366 | break; |
367 | } |
368 | } else { |
369 | headers_remaining--; |
370 | } |
371 | if (packet->granulepos != -1 && headers_remaining == 0) { |
372 | // This indicates the end of the page. |
373 | break; |
374 | } |
375 | if (packet->e_o_s) { |
376 | break; |
377 | } |
378 | if (!vorbis_data_playback->next_ogg_packet(&packet)) { |
379 | // We should get an e_o_s flag before this happens. |
380 | WARN_PRINT("Vorbis file ended without warning." ); |
381 | break; |
382 | } |
383 | } |
384 | } |
385 | |
386 | AudioStreamPlaybackOggVorbis::~AudioStreamPlaybackOggVorbis() { |
387 | if (block_is_allocated) { |
388 | vorbis_block_clear(&block); |
389 | } |
390 | if (dsp_state_is_allocated) { |
391 | vorbis_dsp_clear(&dsp_state); |
392 | } |
393 | if (comment_is_allocated) { |
394 | vorbis_comment_clear(&comment); |
395 | } |
396 | if (info_is_allocated) { |
397 | vorbis_info_clear(&info); |
398 | } |
399 | } |
400 | |
401 | Ref<AudioStreamPlayback> AudioStreamOggVorbis::instantiate_playback() { |
402 | Ref<AudioStreamPlaybackOggVorbis> ovs; |
403 | |
404 | ERR_FAIL_COND_V(packet_sequence.is_null(), nullptr); |
405 | |
406 | ovs.instantiate(); |
407 | ovs->vorbis_stream = Ref<AudioStreamOggVorbis>(this); |
408 | ovs->vorbis_data = packet_sequence; |
409 | ovs->frames_mixed = 0; |
410 | ovs->active = false; |
411 | ovs->loops = 0; |
412 | if (ovs->_alloc_vorbis()) { |
413 | return ovs; |
414 | } |
415 | // Failed to allocate data structures. |
416 | return nullptr; |
417 | } |
418 | |
419 | String AudioStreamOggVorbis::get_stream_name() const { |
420 | return "" ; //return stream_name; |
421 | } |
422 | |
423 | void AudioStreamOggVorbis::maybe_update_info() { |
424 | ERR_FAIL_COND(packet_sequence.is_null()); |
425 | |
426 | vorbis_info info; |
427 | vorbis_comment ; |
428 | int err; |
429 | |
430 | vorbis_info_init(&info); |
431 | vorbis_comment_init(&comment); |
432 | |
433 | Ref<OggPacketSequencePlayback> packet_sequence_playback = packet_sequence->instantiate_playback(); |
434 | |
435 | for (int i = 0; i < 3; i++) { |
436 | ogg_packet *packet; |
437 | if (!packet_sequence_playback->next_ogg_packet(&packet)) { |
438 | WARN_PRINT("Failed to get header packet" ); |
439 | break; |
440 | } |
441 | if (i == 0) { |
442 | packet->b_o_s = 1; |
443 | |
444 | ERR_FAIL_COND(!vorbis_synthesis_idheader(packet)); |
445 | } |
446 | |
447 | err = vorbis_synthesis_headerin(&info, &comment, packet); |
448 | ERR_FAIL_COND_MSG(err != 0, "Error parsing header packet " + itos(i) + ": " + itos(err)); |
449 | } |
450 | |
451 | packet_sequence->set_sampling_rate(info.rate); |
452 | |
453 | vorbis_comment_clear(&comment); |
454 | vorbis_info_clear(&info); |
455 | } |
456 | |
457 | void AudioStreamOggVorbis::set_packet_sequence(Ref<OggPacketSequence> p_packet_sequence) { |
458 | packet_sequence = p_packet_sequence; |
459 | if (packet_sequence.is_valid()) { |
460 | maybe_update_info(); |
461 | } |
462 | } |
463 | |
464 | Ref<OggPacketSequence> AudioStreamOggVorbis::get_packet_sequence() const { |
465 | return packet_sequence; |
466 | } |
467 | |
468 | void AudioStreamOggVorbis::set_loop(bool p_enable) { |
469 | loop = p_enable; |
470 | } |
471 | |
472 | bool AudioStreamOggVorbis::has_loop() const { |
473 | return loop; |
474 | } |
475 | |
476 | void AudioStreamOggVorbis::set_loop_offset(double p_seconds) { |
477 | loop_offset = p_seconds; |
478 | } |
479 | |
480 | double AudioStreamOggVorbis::get_loop_offset() const { |
481 | return loop_offset; |
482 | } |
483 | |
484 | double AudioStreamOggVorbis::get_length() const { |
485 | ERR_FAIL_COND_V(packet_sequence.is_null(), 0); |
486 | return packet_sequence->get_length(); |
487 | } |
488 | |
489 | void AudioStreamOggVorbis::set_bpm(double p_bpm) { |
490 | ERR_FAIL_COND(p_bpm < 0); |
491 | bpm = p_bpm; |
492 | emit_changed(); |
493 | } |
494 | |
495 | double AudioStreamOggVorbis::get_bpm() const { |
496 | return bpm; |
497 | } |
498 | |
499 | void AudioStreamOggVorbis::set_beat_count(int p_beat_count) { |
500 | ERR_FAIL_COND(p_beat_count < 0); |
501 | beat_count = p_beat_count; |
502 | emit_changed(); |
503 | } |
504 | |
505 | int AudioStreamOggVorbis::get_beat_count() const { |
506 | return beat_count; |
507 | } |
508 | |
509 | void AudioStreamOggVorbis::set_bar_beats(int p_bar_beats) { |
510 | ERR_FAIL_COND(p_bar_beats < 2); |
511 | bar_beats = p_bar_beats; |
512 | emit_changed(); |
513 | } |
514 | |
515 | int AudioStreamOggVorbis::get_bar_beats() const { |
516 | return bar_beats; |
517 | } |
518 | |
519 | bool AudioStreamOggVorbis::is_monophonic() const { |
520 | return false; |
521 | } |
522 | |
523 | void AudioStreamOggVorbis::_bind_methods() { |
524 | ClassDB::bind_static_method("AudioStreamOggVorbis" , D_METHOD("load_from_buffer" , "buffer" ), &AudioStreamOggVorbis::load_from_buffer); |
525 | ClassDB::bind_static_method("AudioStreamOggVorbis" , D_METHOD("load_from_file" , "path" ), &AudioStreamOggVorbis::load_from_file); |
526 | |
527 | ClassDB::bind_method(D_METHOD("set_packet_sequence" , "packet_sequence" ), &AudioStreamOggVorbis::set_packet_sequence); |
528 | ClassDB::bind_method(D_METHOD("get_packet_sequence" ), &AudioStreamOggVorbis::get_packet_sequence); |
529 | |
530 | ClassDB::bind_method(D_METHOD("set_loop" , "enable" ), &AudioStreamOggVorbis::set_loop); |
531 | ClassDB::bind_method(D_METHOD("has_loop" ), &AudioStreamOggVorbis::has_loop); |
532 | |
533 | ClassDB::bind_method(D_METHOD("set_loop_offset" , "seconds" ), &AudioStreamOggVorbis::set_loop_offset); |
534 | ClassDB::bind_method(D_METHOD("get_loop_offset" ), &AudioStreamOggVorbis::get_loop_offset); |
535 | |
536 | ClassDB::bind_method(D_METHOD("set_bpm" , "bpm" ), &AudioStreamOggVorbis::set_bpm); |
537 | ClassDB::bind_method(D_METHOD("get_bpm" ), &AudioStreamOggVorbis::get_bpm); |
538 | |
539 | ClassDB::bind_method(D_METHOD("set_beat_count" , "count" ), &AudioStreamOggVorbis::set_beat_count); |
540 | ClassDB::bind_method(D_METHOD("get_beat_count" ), &AudioStreamOggVorbis::get_beat_count); |
541 | |
542 | ClassDB::bind_method(D_METHOD("set_bar_beats" , "count" ), &AudioStreamOggVorbis::set_bar_beats); |
543 | ClassDB::bind_method(D_METHOD("get_bar_beats" ), &AudioStreamOggVorbis::get_bar_beats); |
544 | |
545 | ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "packet_sequence" , PROPERTY_HINT_NONE, "" , PROPERTY_USAGE_NO_EDITOR), "set_packet_sequence" , "get_packet_sequence" ); |
546 | ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bpm" , PROPERTY_HINT_RANGE, "0,400,0.01,or_greater" ), "set_bpm" , "get_bpm" ); |
547 | ADD_PROPERTY(PropertyInfo(Variant::INT, "beat_count" , PROPERTY_HINT_RANGE, "0,512,1,or_greater" ), "set_beat_count" , "get_beat_count" ); |
548 | ADD_PROPERTY(PropertyInfo(Variant::INT, "bar_beats" , PROPERTY_HINT_RANGE, "2,32,1,or_greater" ), "set_bar_beats" , "get_bar_beats" ); |
549 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop" ), "set_loop" , "has_loop" ); |
550 | ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "loop_offset" ), "set_loop_offset" , "get_loop_offset" ); |
551 | } |
552 | |
553 | AudioStreamOggVorbis::AudioStreamOggVorbis() {} |
554 | |
555 | AudioStreamOggVorbis::~AudioStreamOggVorbis() {} |
556 | |
557 | Ref<AudioStreamOggVorbis> AudioStreamOggVorbis::load_from_buffer(const Vector<uint8_t> &file_data) { |
558 | return ResourceImporterOggVorbis::load_from_buffer(file_data); |
559 | } |
560 | |
561 | Ref<AudioStreamOggVorbis> AudioStreamOggVorbis::load_from_file(const String &p_path) { |
562 | return ResourceImporterOggVorbis::load_from_file(p_path); |
563 | } |
564 | |