/**************************************************************************/ /* audio_stream_ogg_vorbis.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #include "audio_stream_ogg_vorbis.h" #include "core/io/file_access.h" #include "core/variant/typed_array.h" #include "modules/vorbis/resource_importer_ogg_vorbis.h" #include int AudioStreamPlaybackOggVorbis::_mix_internal(AudioFrame *p_buffer, int p_frames) { ERR_FAIL_COND_V(!ready, 0); if (!active) { return 0; } int todo = p_frames; int beat_length_frames = -1; bool beat_loop = vorbis_stream->has_loop(); if (beat_loop && vorbis_stream->get_bpm() > 0 && vorbis_stream->get_beat_count() > 0) { beat_length_frames = vorbis_stream->get_beat_count() * vorbis_data->get_sampling_rate() * 60 / vorbis_stream->get_bpm(); } while (todo > 0 && active) { AudioFrame *buffer = p_buffer; buffer += p_frames - todo; int to_mix = todo; if (beat_length_frames >= 0 && (beat_length_frames - (int)frames_mixed) < to_mix) { to_mix = MAX(0, beat_length_frames - (int)frames_mixed); } int mixed = _mix_frames_vorbis(buffer, to_mix); ERR_FAIL_COND_V(mixed < 0, 0); todo -= mixed; frames_mixed += mixed; if (loop_fade_remaining < FADE_SIZE) { int to_fade = loop_fade_remaining + MIN(FADE_SIZE - loop_fade_remaining, mixed); for (int i = loop_fade_remaining; i < to_fade; i++) { buffer[i - loop_fade_remaining] += loop_fade[i] * (float(FADE_SIZE - i) / float(FADE_SIZE)); } loop_fade_remaining = to_fade; } if (beat_length_frames >= 0) { /** * Length determined by beat length * This code is commented out because, in practice, it is preferred that the fade * is done by the transitioner and this stream just goes on until it ends while fading out. * * End fade implementation is left here for reference in case at some point this feature * is desired. if (!beat_loop && (int)frames_mixed > beat_length_frames - FADE_SIZE) { print_line("beat length fade/after mix?"); //No loop, just fade and finish for (int i = 0; i < mixed; i++) { int idx = frames_mixed + i - mixed; buffer[i] *= 1.0 - float(MAX(0, (idx - (beat_length_frames - FADE_SIZE)))) / float(FADE_SIZE); } if ((int)frames_mixed == beat_length_frames) { for (int i = p_frames - todo; i < p_frames; i++) { p_buffer[i] = AudioFrame(0, 0); } active = false; break; } } else **/ if (beat_loop && beat_length_frames <= (int)frames_mixed) { // End of file when doing beat-based looping. <= used instead of == because importer editing if (!have_packets_left && !have_samples_left) { //Nothing remaining, so do nothing. loop_fade_remaining = FADE_SIZE; } else { // Add some loop fade; int faded_mix = _mix_frames_vorbis(loop_fade, FADE_SIZE); for (int i = faded_mix; i < FADE_SIZE; i++) { // In case lesss was mixed, pad with zeros loop_fade[i] = AudioFrame(0, 0); } loop_fade_remaining = 0; } seek(vorbis_stream->loop_offset); loops++; // We still have buffer to fill, start from this element in the next iteration. continue; } } if (!have_packets_left && !have_samples_left) { // Actual end of file! bool is_not_empty = mixed > 0 || vorbis_stream->get_length() > 0; if (vorbis_stream->loop && is_not_empty) { //loop seek(vorbis_stream->loop_offset); loops++; // We still have buffer to fill, start from this element in the next iteration. } else { for (int i = p_frames - todo; i < p_frames; i++) { p_buffer[i] = AudioFrame(0, 0); } active = false; } } } return p_frames - todo; } int AudioStreamPlaybackOggVorbis::_mix_frames_vorbis(AudioFrame *p_buffer, int p_frames) { ERR_FAIL_COND_V(!ready, 0); if (!have_samples_left) { ogg_packet *packet = nullptr; int err; if (!vorbis_data_playback->next_ogg_packet(&packet)) { have_packets_left = false; WARN_PRINT("ran out of packets in stream"); return -1; } err = vorbis_synthesis(&block, packet); ERR_FAIL_COND_V_MSG(err != 0, 0, "Error during vorbis synthesis " + itos(err)); err = vorbis_synthesis_blockin(&dsp_state, &block); ERR_FAIL_COND_V_MSG(err != 0, 0, "Error during vorbis block processing " + itos(err)); have_packets_left = !packet->e_o_s; } float **pcm; // Accessed with pcm[channel_idx][sample_idx]. int frames = vorbis_synthesis_pcmout(&dsp_state, &pcm); if (frames > p_frames) { frames = p_frames; have_samples_left = true; } else { have_samples_left = false; } if (info.channels > 1) { for (int frame = 0; frame < frames; frame++) { p_buffer[frame].l = pcm[0][frame]; p_buffer[frame].r = pcm[1][frame]; } } else { for (int frame = 0; frame < frames; frame++) { p_buffer[frame].l = pcm[0][frame]; p_buffer[frame].r = pcm[0][frame]; } } vorbis_synthesis_read(&dsp_state, frames); return frames; } float AudioStreamPlaybackOggVorbis::get_stream_sampling_rate() { return vorbis_data->get_sampling_rate(); } bool AudioStreamPlaybackOggVorbis::_alloc_vorbis() { vorbis_info_init(&info); info_is_allocated = true; vorbis_comment_init(&comment); comment_is_allocated = true; ERR_FAIL_COND_V(vorbis_data.is_null(), false); vorbis_data_playback = vorbis_data->instantiate_playback(); ogg_packet *packet; int err; for (int i = 0; i < 3; i++) { if (!vorbis_data_playback->next_ogg_packet(&packet)) { WARN_PRINT("Not enough packets to parse header"); return false; } err = vorbis_synthesis_headerin(&info, &comment, packet); ERR_FAIL_COND_V_MSG(err != 0, false, "Error parsing header"); } err = vorbis_synthesis_init(&dsp_state, &info); ERR_FAIL_COND_V_MSG(err != 0, false, "Error initializing dsp state"); dsp_state_is_allocated = true; err = vorbis_block_init(&dsp_state, &block); ERR_FAIL_COND_V_MSG(err != 0, false, "Error initializing block"); block_is_allocated = true; ready = true; return true; } void AudioStreamPlaybackOggVorbis::start(double p_from_pos) { ERR_FAIL_COND(!ready); loop_fade_remaining = FADE_SIZE; active = true; seek(p_from_pos); loops = 0; begin_resample(); } void AudioStreamPlaybackOggVorbis::stop() { active = false; } bool AudioStreamPlaybackOggVorbis::is_playing() const { return active; } int AudioStreamPlaybackOggVorbis::get_loop_count() const { return loops; } double AudioStreamPlaybackOggVorbis::get_playback_position() const { return double(frames_mixed) / (double)vorbis_data->get_sampling_rate(); } void AudioStreamPlaybackOggVorbis::tag_used_streams() { vorbis_stream->tag_used(get_playback_position()); } void AudioStreamPlaybackOggVorbis::seek(double p_time) { ERR_FAIL_COND(!ready); ERR_FAIL_COND(vorbis_stream.is_null()); if (!active) { return; } vorbis_synthesis_restart(&dsp_state); if (p_time >= vorbis_stream->get_length()) { p_time = 0; } frames_mixed = uint32_t(vorbis_data->get_sampling_rate() * p_time); const int64_t desired_sample = p_time * get_stream_sampling_rate(); if (!vorbis_data_playback->seek_page(desired_sample)) { WARN_PRINT("seek failed"); return; } ogg_packet *packet; if (!vorbis_data_playback->next_ogg_packet(&packet)) { WARN_PRINT_ONCE("seeking beyond limits"); return; } // The granule position of the page we're seeking through. int64_t granule_pos = 0; int headers_remaining = 0; int samples_in_page = 0; int err; while (true) { if (vorbis_synthesis_idheader(packet)) { headers_remaining = 3; } if (!headers_remaining) { err = vorbis_synthesis(&block, packet); ERR_FAIL_COND_MSG(err != 0, "Error during vorbis synthesis " + itos(err)); err = vorbis_synthesis_blockin(&dsp_state, &block); ERR_FAIL_COND_MSG(err != 0, "Error during vorbis block processing " + itos(err)); int samples_out = vorbis_synthesis_pcmout(&dsp_state, nullptr); err = vorbis_synthesis_read(&dsp_state, samples_out); ERR_FAIL_COND_MSG(err != 0, "Error during vorbis read updating " + itos(err)); samples_in_page += samples_out; } else { headers_remaining--; } if (packet->granulepos != -1 && headers_remaining == 0) { // This indicates the end of the page. granule_pos = packet->granulepos; break; } if (packet->e_o_s) { break; } if (!vorbis_data_playback->next_ogg_packet(&packet)) { // We should get an e_o_s flag before this happens. WARN_PRINT("Vorbis file ended without warning."); break; } } int64_t samples_to_burn = samples_in_page - (granule_pos - desired_sample); if (samples_to_burn > samples_in_page) { WARN_PRINT_ONCE("Burning more samples than we have in this page. Check seek algorithm."); } else if (samples_to_burn < 0) { WARN_PRINT_ONCE("Burning negative samples doesn't make sense. Check seek algorithm."); } // Seek again, this time we'll burn a specific number of samples instead of all of them. if (!vorbis_data_playback->seek_page(desired_sample)) { WARN_PRINT("seek failed"); return; } if (!vorbis_data_playback->next_ogg_packet(&packet)) { WARN_PRINT_ONCE("seeking beyond limits"); return; } vorbis_synthesis_restart(&dsp_state); while (true) { if (vorbis_synthesis_idheader(packet)) { headers_remaining = 3; } if (!headers_remaining) { err = vorbis_synthesis(&block, packet); ERR_FAIL_COND_MSG(err != 0, "Error during vorbis synthesis " + itos(err)); err = vorbis_synthesis_blockin(&dsp_state, &block); ERR_FAIL_COND_MSG(err != 0, "Error during vorbis block processing " + itos(err)); int samples_out = vorbis_synthesis_pcmout(&dsp_state, nullptr); int read_samples = samples_to_burn > samples_out ? samples_out : samples_to_burn; err = vorbis_synthesis_read(&dsp_state, samples_out); ERR_FAIL_COND_MSG(err != 0, "Error during vorbis read updating " + itos(err)); samples_to_burn -= read_samples; if (samples_to_burn <= 0) { break; } } else { headers_remaining--; } if (packet->granulepos != -1 && headers_remaining == 0) { // This indicates the end of the page. break; } if (packet->e_o_s) { break; } if (!vorbis_data_playback->next_ogg_packet(&packet)) { // We should get an e_o_s flag before this happens. WARN_PRINT("Vorbis file ended without warning."); break; } } } AudioStreamPlaybackOggVorbis::~AudioStreamPlaybackOggVorbis() { if (block_is_allocated) { vorbis_block_clear(&block); } if (dsp_state_is_allocated) { vorbis_dsp_clear(&dsp_state); } if (comment_is_allocated) { vorbis_comment_clear(&comment); } if (info_is_allocated) { vorbis_info_clear(&info); } } Ref AudioStreamOggVorbis::instantiate_playback() { Ref ovs; ERR_FAIL_COND_V(packet_sequence.is_null(), nullptr); ovs.instantiate(); ovs->vorbis_stream = Ref(this); ovs->vorbis_data = packet_sequence; ovs->frames_mixed = 0; ovs->active = false; ovs->loops = 0; if (ovs->_alloc_vorbis()) { return ovs; } // Failed to allocate data structures. return nullptr; } String AudioStreamOggVorbis::get_stream_name() const { return ""; //return stream_name; } void AudioStreamOggVorbis::maybe_update_info() { ERR_FAIL_COND(packet_sequence.is_null()); vorbis_info info; vorbis_comment comment; int err; vorbis_info_init(&info); vorbis_comment_init(&comment); Ref packet_sequence_playback = packet_sequence->instantiate_playback(); for (int i = 0; i < 3; i++) { ogg_packet *packet; if (!packet_sequence_playback->next_ogg_packet(&packet)) { WARN_PRINT("Failed to get header packet"); break; } if (i == 0) { packet->b_o_s = 1; ERR_FAIL_COND(!vorbis_synthesis_idheader(packet)); } err = vorbis_synthesis_headerin(&info, &comment, packet); ERR_FAIL_COND_MSG(err != 0, "Error parsing header packet " + itos(i) + ": " + itos(err)); } packet_sequence->set_sampling_rate(info.rate); vorbis_comment_clear(&comment); vorbis_info_clear(&info); } void AudioStreamOggVorbis::set_packet_sequence(Ref p_packet_sequence) { packet_sequence = p_packet_sequence; if (packet_sequence.is_valid()) { maybe_update_info(); } } Ref AudioStreamOggVorbis::get_packet_sequence() const { return packet_sequence; } void AudioStreamOggVorbis::set_loop(bool p_enable) { loop = p_enable; } bool AudioStreamOggVorbis::has_loop() const { return loop; } void AudioStreamOggVorbis::set_loop_offset(double p_seconds) { loop_offset = p_seconds; } double AudioStreamOggVorbis::get_loop_offset() const { return loop_offset; } double AudioStreamOggVorbis::get_length() const { ERR_FAIL_COND_V(packet_sequence.is_null(), 0); return packet_sequence->get_length(); } void AudioStreamOggVorbis::set_bpm(double p_bpm) { ERR_FAIL_COND(p_bpm < 0); bpm = p_bpm; emit_changed(); } double AudioStreamOggVorbis::get_bpm() const { return bpm; } void AudioStreamOggVorbis::set_beat_count(int p_beat_count) { ERR_FAIL_COND(p_beat_count < 0); beat_count = p_beat_count; emit_changed(); } int AudioStreamOggVorbis::get_beat_count() const { return beat_count; } void AudioStreamOggVorbis::set_bar_beats(int p_bar_beats) { ERR_FAIL_COND(p_bar_beats < 2); bar_beats = p_bar_beats; emit_changed(); } int AudioStreamOggVorbis::get_bar_beats() const { return bar_beats; } bool AudioStreamOggVorbis::is_monophonic() const { return false; } void AudioStreamOggVorbis::_bind_methods() { ClassDB::bind_static_method("AudioStreamOggVorbis", D_METHOD("load_from_buffer", "buffer"), &AudioStreamOggVorbis::load_from_buffer); ClassDB::bind_static_method("AudioStreamOggVorbis", D_METHOD("load_from_file", "path"), &AudioStreamOggVorbis::load_from_file); ClassDB::bind_method(D_METHOD("set_packet_sequence", "packet_sequence"), &AudioStreamOggVorbis::set_packet_sequence); ClassDB::bind_method(D_METHOD("get_packet_sequence"), &AudioStreamOggVorbis::get_packet_sequence); ClassDB::bind_method(D_METHOD("set_loop", "enable"), &AudioStreamOggVorbis::set_loop); ClassDB::bind_method(D_METHOD("has_loop"), &AudioStreamOggVorbis::has_loop); ClassDB::bind_method(D_METHOD("set_loop_offset", "seconds"), &AudioStreamOggVorbis::set_loop_offset); ClassDB::bind_method(D_METHOD("get_loop_offset"), &AudioStreamOggVorbis::get_loop_offset); ClassDB::bind_method(D_METHOD("set_bpm", "bpm"), &AudioStreamOggVorbis::set_bpm); ClassDB::bind_method(D_METHOD("get_bpm"), &AudioStreamOggVorbis::get_bpm); ClassDB::bind_method(D_METHOD("set_beat_count", "count"), &AudioStreamOggVorbis::set_beat_count); ClassDB::bind_method(D_METHOD("get_beat_count"), &AudioStreamOggVorbis::get_beat_count); ClassDB::bind_method(D_METHOD("set_bar_beats", "count"), &AudioStreamOggVorbis::set_bar_beats); ClassDB::bind_method(D_METHOD("get_bar_beats"), &AudioStreamOggVorbis::get_bar_beats); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "packet_sequence", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_packet_sequence", "get_packet_sequence"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bpm", PROPERTY_HINT_RANGE, "0,400,0.01,or_greater"), "set_bpm", "get_bpm"); ADD_PROPERTY(PropertyInfo(Variant::INT, "beat_count", PROPERTY_HINT_RANGE, "0,512,1,or_greater"), "set_beat_count", "get_beat_count"); ADD_PROPERTY(PropertyInfo(Variant::INT, "bar_beats", PROPERTY_HINT_RANGE, "2,32,1,or_greater"), "set_bar_beats", "get_bar_beats"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "loop_offset"), "set_loop_offset", "get_loop_offset"); } AudioStreamOggVorbis::AudioStreamOggVorbis() {} AudioStreamOggVorbis::~AudioStreamOggVorbis() {} Ref AudioStreamOggVorbis::load_from_buffer(const Vector &file_data) { return ResourceImporterOggVorbis::load_from_buffer(file_data); } Ref AudioStreamOggVorbis::load_from_file(const String &p_path) { return ResourceImporterOggVorbis::load_from_file(p_path); }