/*************************************************************************/ /* editor_sample_import_plugin.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* http://www.godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2017 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2017 Godot Engine contributors (cf. AUTHORS.md) */ /* */ /* 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 "editor_sample_import_plugin.h" #include "editor/editor_dir_dialog.h" #include "editor/editor_file_dialog.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" #include "editor/property_editor.h" #include "io/marshalls.h" #include "io/resource_saver.h" #include "os/file_access.h" #include "scene/resources/sample.h" class _EditorSampleImportOptions : public Object { OBJ_TYPE(_EditorSampleImportOptions, Object); public: enum CompressMode { COMPRESS_MODE_DISABLED, COMPRESS_MODE_RAM, COMPRESS_MODE_DISK }; enum CompressBitrate { COMPRESS_64, COMPRESS_96, COMPRESS_128, COMPRESS_192 }; bool force_8_bit; bool force_mono; bool force_rate; float force_rate_hz; bool edit_trim; bool edit_normalize; bool edit_loop; CompressMode compress_mode; CompressBitrate compress_bitrate; bool _set(const StringName &p_name, const Variant &p_value) { String n = p_name; if (n == "force/8_bit") force_8_bit = p_value; else if (n == "force/mono") force_mono = p_value; else if (n == "force/max_rate") force_rate = p_value; else if (n == "force/max_rate_hz") force_rate_hz = p_value; else if (n == "edit/trim") edit_trim = p_value; else if (n == "edit/normalize") edit_normalize = p_value; else if (n == "edit/loop") edit_loop = p_value; else if (n == "compress/mode") compress_mode = CompressMode(int(p_value)); else if (n == "compress/bitrate") compress_bitrate = CompressBitrate(int(p_value)); else return false; return true; } bool _get(const StringName &p_name, Variant &r_ret) const { String n = p_name; if (n == "force/8_bit") r_ret = force_8_bit; else if (n == "force/mono") r_ret = force_mono; else if (n == "force/max_rate") r_ret = force_rate; else if (n == "force/max_rate_hz") r_ret = force_rate_hz; else if (n == "edit/trim") r_ret = edit_trim; else if (n == "edit/normalize") r_ret = edit_normalize; else if (n == "edit/loop") r_ret = edit_loop; else if (n == "compress/mode") r_ret = compress_mode; else if (n == "compress/bitrate") r_ret = compress_bitrate; else return false; return true; } void _get_property_list(List *p_list) const { p_list->push_back(PropertyInfo(Variant::BOOL, "force/8_bit")); p_list->push_back(PropertyInfo(Variant::BOOL, "force/mono")); p_list->push_back(PropertyInfo(Variant::BOOL, "force/max_rate")); p_list->push_back(PropertyInfo(Variant::REAL, "force/max_rate_hz", PROPERTY_HINT_EXP_RANGE, "11025,192000,1")); p_list->push_back(PropertyInfo(Variant::BOOL, "edit/trim")); p_list->push_back(PropertyInfo(Variant::BOOL, "edit/normalize")); p_list->push_back(PropertyInfo(Variant::BOOL, "edit/loop")); p_list->push_back(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM)")); //p_list->push_back(PropertyInfo(Variant::INT,"compress/bitrate",PROPERTY_HINT_ENUM,"64,96,128,192")); } static void _bind_methods() { ADD_SIGNAL(MethodInfo("changed")); } _EditorSampleImportOptions() { force_8_bit = false; force_mono = false; force_rate = true; force_rate_hz = 44100; edit_trim = true; edit_normalize = true; edit_loop = false; compress_mode = COMPRESS_MODE_RAM; compress_bitrate = COMPRESS_128; } }; class EditorSampleImportDialog : public ConfirmationDialog { OBJ_TYPE(EditorSampleImportDialog, ConfirmationDialog); EditorSampleImportPlugin *plugin; LineEdit *import_path; LineEdit *save_path; EditorFileDialog *file_select; EditorDirDialog *save_select; ConfirmationDialog *error_dialog; PropertyEditor *option_editor; _EditorSampleImportOptions *options; public: void _choose_files(const Vector &p_path) { String files; for (int i = 0; i < p_path.size(); i++) { if (i > 0) files += ","; files += p_path[i]; } /* if (p_path.size()) { String srctex=p_path[0]; String ipath = EditorImportDB::get_singleton()->find_source_path(srctex); if (ipath!="") save_path->set_text(ipath.get_base_dir()); }*/ import_path->set_text(files); } void _choose_save_dir(const String &p_path) { save_path->set_text(p_path); } void _browse() { file_select->popup_centered_ratio(); } void _browse_target() { save_select->popup_centered_ratio(); } void popup_import(const String &p_path) { popup_centered(Size2(400, 400) * EDSCALE); if (p_path != "") { Ref rimd = ResourceLoader::load_import_metadata(p_path); ERR_FAIL_COND(!rimd.is_valid()); save_path->set_text(p_path.get_base_dir()); List opts; rimd->get_options(&opts); for (List::Element *E = opts.front(); E; E = E->next()) { options->_set(E->get(), rimd->get_option(E->get())); } String src = ""; for (int i = 0; i < rimd->get_source_count(); i++) { if (i > 0) src += ","; src += EditorImportPlugin::expand_source_path(rimd->get_source_path(i)); } import_path->set_text(src); } } void _import() { Vector samples = import_path->get_text().split(","); if (samples.size() == 0) { error_dialog->set_text(TTR("No samples to import!")); error_dialog->popup_centered(Size2(200, 100) * EDSCALE); } if (save_path->get_text().strip_edges() == "") { error_dialog->set_text(TTR("Target path is empty.")); error_dialog->popup_centered_minsize(); return; } if (!save_path->get_text().begins_with("res://")) { error_dialog->set_text(TTR("Target path must be a complete resource path.")); error_dialog->popup_centered_minsize(); return; } if (!DirAccess::exists(save_path->get_text())) { error_dialog->set_text(TTR("Target path must exist.")); error_dialog->popup_centered_minsize(); return; } for (int i = 0; i < samples.size(); i++) { Ref imd = memnew(ResourceImportMetadata); List pl; options->_get_property_list(&pl); for (List::Element *E = pl.front(); E; E = E->next()) { Variant v; String opt = E->get().name; options->_get(opt, v); imd->set_option(opt, v); } imd->add_source(EditorImportPlugin::validate_source_path(samples[i])); String dst = save_path->get_text(); if (dst == "") { error_dialog->set_text(TTR("Save path is empty!")); error_dialog->popup_centered(Size2(200, 100) * EDSCALE); } dst = dst.plus_file(samples[i].get_file().basename() + ".smp"); Error err = plugin->import(dst, imd); } hide(); } void _notification(int p_what) { if (p_what == NOTIFICATION_ENTER_TREE) { option_editor->edit(options); } } static void _bind_methods() { ObjectTypeDB::bind_method("_choose_files", &EditorSampleImportDialog::_choose_files); ObjectTypeDB::bind_method("_choose_save_dir", &EditorSampleImportDialog::_choose_save_dir); ObjectTypeDB::bind_method("_import", &EditorSampleImportDialog::_import); ObjectTypeDB::bind_method("_browse", &EditorSampleImportDialog::_browse); ObjectTypeDB::bind_method("_browse_target", &EditorSampleImportDialog::_browse_target); // ADD_SIGNAL( MethodInfo("imported",PropertyInfo(Variant::OBJECT,"scene")) ); } EditorSampleImportDialog(EditorSampleImportPlugin *p_plugin) { plugin = p_plugin; set_title(TTR("Import Audio Samples")); VBoxContainer *vbc = memnew(VBoxContainer); add_child(vbc); set_child_rect(vbc); HBoxContainer *hbc = memnew(HBoxContainer); vbc->add_margin_child(TTR("Source Sample(s):"), hbc); import_path = memnew(LineEdit); import_path->set_h_size_flags(SIZE_EXPAND_FILL); hbc->add_child(import_path); Button *import_choose = memnew(Button); import_choose->set_text(" .. "); hbc->add_child(import_choose); import_choose->connect("pressed", this, "_browse"); hbc = memnew(HBoxContainer); vbc->add_margin_child(TTR("Target Path:"), hbc); save_path = memnew(LineEdit); save_path->set_h_size_flags(SIZE_EXPAND_FILL); hbc->add_child(save_path); Button *save_choose = memnew(Button); save_choose->set_text(" .. "); hbc->add_child(save_choose); save_choose->connect("pressed", this, "_browse_target"); file_select = memnew(EditorFileDialog); file_select->set_access(EditorFileDialog::ACCESS_FILESYSTEM); add_child(file_select); file_select->set_mode(EditorFileDialog::MODE_OPEN_FILES); file_select->connect("files_selected", this, "_choose_files"); file_select->add_filter("*.wav ; MS Waveform"); save_select = memnew(EditorDirDialog); add_child(save_select); // save_select->set_mode(EditorFileDialog::MODE_OPEN_DIR); save_select->connect("dir_selected", this, "_choose_save_dir"); get_ok()->connect("pressed", this, "_import"); get_ok()->set_text(TTR("Import")); error_dialog = memnew(ConfirmationDialog); add_child(error_dialog); error_dialog->get_ok()->set_text(TTR("Accept")); // error_dialog->get_cancel()->hide(); set_hide_on_ok(false); options = memnew(_EditorSampleImportOptions); option_editor = memnew(PropertyEditor); option_editor->hide_top_label(); vbc->add_margin_child(TTR("Options:"), option_editor, true); } ~EditorSampleImportDialog() { memdelete(options); } }; String EditorSampleImportPlugin::get_name() const { return "sample"; } String EditorSampleImportPlugin::get_visible_name() const { return TTR("Audio Sample"); } void EditorSampleImportPlugin::import_dialog(const String &p_from) { dialog->popup_import(p_from); } Error EditorSampleImportPlugin::import(const String &p_path, const Ref &p_from) { ERR_FAIL_COND_V(p_from->get_source_count() != 1, ERR_INVALID_PARAMETER); Ref from = p_from; String src_path = EditorImportPlugin::expand_source_path(from->get_source_path(0)); Ref smp = ResourceLoader::load(src_path); ERR_FAIL_COND_V(smp.is_null(), ERR_CANT_OPEN); float rate = smp->get_mix_rate(); bool is16 = smp->get_format() == Sample::FORMAT_PCM16; int chans = smp->is_stereo() ? 2 : 1; int len = smp->get_length(); Sample::LoopFormat loop = smp->get_loop_format(); int loop_beg = smp->get_loop_begin(); int loop_end = smp->get_loop_end(); print_line("Input Sample: "); print_line("\tlen: " + itos(len)); print_line("\tchans: " + itos(chans)); print_line("\t16bits: " + itos(is16)); print_line("\trate: " + itos(rate)); print_line("\tloop: " + itos(loop)); print_line("\tloop begin: " + itos(loop_beg)); print_line("\tloop end: " + itos(loop_end)); Vector data; data.resize(len * chans); { DVector src_data = smp->get_data(); DVector::Read sr = src_data.read(); for (int i = 0; i < len * chans; i++) { float s = 0; if (is16) { int16_t i16 = decode_uint16(&sr[i * 2]); s = i16 / 32767.0; } else { int8_t i8 = sr[i]; s = i8 / 127.0; } data[i] = s; } } //apply frequency limit bool limit_rate = from->get_option("force/max_rate"); int limit_rate_hz = from->get_option("force/max_rate_hz"); if (limit_rate && rate > limit_rate_hz) { //resampleeee!!! int new_data_len = len * limit_rate_hz / rate; Vector new_data; new_data.resize(new_data_len * chans); for (int c = 0; c < chans; c++) { for (int i = 0; i < new_data_len; i++) { //simple cubic interpolation should be enough. float pos = float(i) * len / new_data_len; float mu = pos - Math::floor(pos); int ipos = int(Math::floor(pos)); float y0 = data[MAX(0, ipos - 1) * chans + c]; float y1 = data[ipos * chans + c]; float y2 = data[MIN(len - 1, ipos + 1) * chans + c]; float y3 = data[MIN(len - 1, ipos + 2) * chans + c]; float mu2 = mu * mu; float a0 = y3 - y2 - y0 + y1; float a1 = y0 - y1 - a0; float a2 = y2 - y0; float a3 = y1; float res = (a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3); new_data[i * chans + c] = res; } } if (loop) { loop_beg = loop_beg * new_data_len / len; loop_end = loop_end * new_data_len / len; } data = new_data; rate = limit_rate_hz; len = new_data_len; } bool normalize = from->get_option("edit/normalize"); if (normalize) { float max = 0; for (int i = 0; i < data.size(); i++) { float amp = Math::abs(data[i]); if (amp > max) max = amp; } if (max > 0) { float mult = 1.0 / max; for (int i = 0; i < data.size(); i++) { data[i] *= mult; } } } bool trim = from->get_option("edit/trim"); if (trim && !loop) { int first = 0; int last = (len * chans) - 1; bool found = false; float limit = Math::db2linear(-30); for (int i = 0; i < data.size(); i++) { float amp = Math::abs(data[i]); if (!found && amp > limit) { first = i; found = true; } if (found && amp > limit) { last = i; } } first /= chans; last /= chans; if (first < last) { Vector new_data; new_data.resize((last - first + 1) * chans); for (int i = first * chans; i <= last * chans; i++) { new_data[i - first * chans] = data[i]; } data = new_data; len = data.size() / chans; } } bool make_loop = from->get_option("edit/loop"); if (make_loop && !loop) { loop = Sample::LOOP_FORWARD; loop_beg = 0; loop_end = len; } int compression = from->get_option("compress/mode"); bool force_mono = from->get_option("force/mono"); if (force_mono && chans == 2) { Vector new_data; new_data.resize(data.size() / 2); for (int i = 0; i < len; i++) { new_data[i] = (data[i * 2 + 0] + data[i * 2 + 1]) / 2.0; } data = new_data; chans = 1; } bool force_8_bit = from->get_option("force/8_bit"); if (force_8_bit) { is16 = false; } DVector dst_data; Sample::Format dst_format; if (compression == _EditorSampleImportOptions::COMPRESS_MODE_RAM) { dst_format = Sample::FORMAT_IMA_ADPCM; if (chans == 1) { _compress_ima_adpcm(data, dst_data); } else { print_line("INTERLEAAVE!"); //byte interleave Vector left; Vector right; int tlen = data.size() / 2; left.resize(tlen); right.resize(tlen); for (int i = 0; i < tlen; i++) { left[i] = data[i * 2 + 0]; right[i] = data[i * 2 + 1]; } DVector bleft; DVector bright; _compress_ima_adpcm(left, bleft); _compress_ima_adpcm(right, bright); int dl = bleft.size(); dst_data.resize(dl * 2); DVector::Write w = dst_data.write(); DVector::Read rl = bleft.read(); DVector::Read rr = bright.read(); for (int i = 0; i < dl; i++) { w[i * 2 + 0] = rl[i]; w[i * 2 + 1] = rr[i]; } } // print_line("compressing ima-adpcm, resulting buffersize is "+itos(dst_data.size())+" from "+itos(data.size())); } else { dst_format = is16 ? Sample::FORMAT_PCM16 : Sample::FORMAT_PCM8; dst_data.resize(data.size() * (is16 ? 2 : 1)); { DVector::Write w = dst_data.write(); int ds = data.size(); for (int i = 0; i < ds; i++) { if (is16) { int16_t v = CLAMP(data[i] * 32767, -32768, 32767); encode_uint16(v, &w[i * 2]); } else { int8_t v = CLAMP(data[i] * 127, -128, 127); w[i] = v; } } } } Ref target; if (ResourceCache::has(p_path)) { target = Ref(ResourceCache::get(p_path)->cast_to()); } else { target = smp; } target->create(dst_format, chans == 2 ? true : false, len); target->set_data(dst_data); target->set_mix_rate(rate); target->set_loop_format(loop); target->set_loop_begin(loop_beg); target->set_loop_end(loop_end); from->set_source_md5(0, FileAccess::get_md5(src_path)); from->set_editor(get_name()); target->set_import_metadata(from); Error err = ResourceSaver::save(p_path, smp); return err; } void EditorSampleImportPlugin::_compress_ima_adpcm(const Vector &p_data, DVector &dst_data) { /*p_sample_data->data = (void*)malloc(len); xm_s8 *dataptr=(xm_s8*)p_sample_data->data;*/ static const int16_t _ima_adpcm_step_table[89] = { 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 }; static const int8_t _ima_adpcm_index_table[16] = { -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 }; int datalen = p_data.size(); int datamax = datalen; if (datalen & 1) datalen++; dst_data.resize(datalen / 2 + 4); DVector::Write w = dst_data.write(); int i, step_idx = 0, prev = 0; uint8_t *out = w.ptr(); //int16_t xm_prev=0; const float *in = p_data.ptr(); /* initial value is zero */ *(out++) = 0; *(out++) = 0; /* Table index initial value */ *(out++) = 0; /* unused */ *(out++) = 0; for (i = 0; i < datalen; i++) { int step, diff, vpdiff, mask; uint8_t nibble; int16_t xm_sample; if (i >= datamax) xm_sample = 0; else { xm_sample = CLAMP(in[i] * 32767.0, -32768, 32767); //if (xm_sample==32767 || xm_sample==-32768) // printf("clippy!\n",xm_sample); } // xm_sample=xm_sample+xm_prev; // xm_prev=xm_sample; diff = (int)xm_sample - prev; nibble = 0; step = _ima_adpcm_step_table[step_idx]; vpdiff = step >> 3; if (diff < 0) { nibble = 8; diff = -diff; } mask = 4; while (mask) { if (diff >= step) { nibble |= mask; diff -= step; vpdiff += step; } step >>= 1; mask >>= 1; }; if (nibble & 8) prev -= vpdiff; else prev += vpdiff; if (prev > 32767) { //printf("%i,xms %i, prev %i,diff %i, vpdiff %i, clip up %i\n",i,xm_sample,prev,diff,vpdiff,prev); prev = 32767; } else if (prev < -32768) { //printf("%i,xms %i, prev %i,diff %i, vpdiff %i, clip down %i\n",i,xm_sample,prev,diff,vpdiff,prev); prev = -32768; } step_idx += _ima_adpcm_index_table[nibble]; if (step_idx < 0) step_idx = 0; else if (step_idx > 88) step_idx = 88; if (i & 1) { *out |= nibble << 4; out++; } else { *out = nibble; } /*dataptr[i]=prev>>8;*/ } } EditorSampleImportPlugin *EditorSampleImportPlugin::singleton = NULL; void EditorSampleImportPlugin::import_from_drop(const Vector &p_drop, const String &p_dest_path) { Vector files; for (int i = 0; i < p_drop.size(); i++) { String ext = p_drop[i].extension().to_lower(); if (ext == "wav") { files.push_back(p_drop[i]); } } if (files.size()) { import_dialog(); dialog->_choose_files(files); dialog->_choose_save_dir(p_dest_path); } } void EditorSampleImportPlugin::reimport_multiple_files(const Vector &p_list) { if (p_list.size() == 0) return; Vector sources; for (int i = 0; i < p_list.size(); i++) { int idx; EditorFileSystemDirectory *efsd = EditorFileSystem::get_singleton()->find_file(p_list[i], &idx); if (efsd) { for (int j = 0; j < efsd->get_source_count(idx); j++) { String file = expand_source_path(efsd->get_source_file(idx, j)); if (sources.find(file) == -1) { sources.push_back(file); } } } } if (sources.size()) { dialog->popup_import(p_list[0]); dialog->_choose_files(sources); dialog->_choose_save_dir(p_list[0].get_base_dir()); } } bool EditorSampleImportPlugin::can_reimport_multiple_files() const { return true; } EditorSampleImportPlugin::EditorSampleImportPlugin(EditorNode *p_editor) { singleton = this; dialog = memnew(EditorSampleImportDialog(this)); p_editor->get_gui_base()->add_child(dialog); } Vector EditorSampleExportPlugin::custom_export(String &p_path, const Ref &p_platform) { if (EditorImportExport::get_singleton()->sample_get_action() == EditorImportExport::SAMPLE_ACTION_NONE || p_path.extension().to_lower() != "wav") { return Vector(); } Ref imd = memnew(ResourceImportMetadata); imd->add_source(EditorImportPlugin::validate_source_path(p_path)); imd->set_option("force/8_bit", false); imd->set_option("force/mono", false); imd->set_option("force/max_rate", true); imd->set_option("force/max_rate_hz", EditorImportExport::get_singleton()->sample_get_max_hz()); imd->set_option("edit/trim", EditorImportExport::get_singleton()->sample_get_trim()); imd->set_option("edit/normalize", false); imd->set_option("edit/loop", false); imd->set_option("compress/mode", 1); String savepath = EditorSettings::get_singleton()->get_settings_path().plus_file("tmp/smpconv.smp"); Error err = EditorSampleImportPlugin::singleton->import(savepath, imd); ERR_FAIL_COND_V(err != OK, Vector()); p_path = p_path.basename() + ".converted.smp"; return FileAccess::get_file_as_array(savepath); } EditorSampleExportPlugin::EditorSampleExportPlugin() { }