max / audiofiles
2 files changed,
+199 insertions,
-316 deletions
| @@ -7543,6 +7543,18 @@ | |||
| 7543 | 7543 | "winnow 1.0.4", | |
| 7544 | 7544 | ] | |
| 7545 | 7545 | ||
| 7546 | + | [[patch.unused]] | |
| 7547 | + | name = "kberg" | |
| 7548 | + | version = "0.1.0" | |
| 7549 | + | ||
| 7550 | + | [[patch.unused]] | |
| 7551 | + | name = "ops-status" | |
| 7552 | + | version = "0.1.0" | |
| 7553 | + | ||
| 7554 | + | [[patch.unused]] | |
| 7555 | + | name = "painhours" | |
| 7556 | + | version = "0.1.0" | |
| 7557 | + | ||
| 7546 | 7558 | [[patch.unused]] | |
| 7547 | 7559 | name = "quasi-axum" | |
| 7548 | 7560 | version = "0.18.0" | |
| @@ -7566,15 +7578,3 @@ | |||
| 7566 | 7578 | [[patch.unused]] | |
| 7567 | 7579 | name = "quasi-webview" | |
| 7568 | 7580 | version = "0.18.0" | |
| 7569 | - | ||
| 7570 | - | [[patch.unused]] | |
| 7571 | - | name = "kberg" | |
| 7572 | - | version = "0.1.0" | |
| 7573 | - | ||
| 7574 | - | [[patch.unused]] | |
| 7575 | - | name = "ops-status" | |
| 7576 | - | version = "0.1.0" | |
| 7577 | - | ||
| 7578 | - | [[patch.unused]] | |
| 7579 | - | name = "painhours" | |
| 7580 | - | version = "0.1.0" |
| @@ -249,310 +249,7 @@ | |||
| 249 | 249 | } | |
| 250 | 250 | }); | |
| 251 | 251 | ui.add_space(theme::space::group()); | |
| 252 | - | ||
| 253 | - | // --- Device Profile --- | |
| 254 | - | if profile_count > 0 { | |
| 255 | - | ui.label(egui::RichText::new("Device Profile").strong()); | |
| 256 | - | if let ImportMode::ConfigureExport { | |
| 257 | - | ref mut config, | |
| 258 | - | ref available_profiles, | |
| 259 | - | .. | |
| 260 | - | } = state.import_wf.import_mode | |
| 261 | - | { | |
| 262 | - | let current_label = config.device_profile.as_deref().unwrap_or("None (manual)"); | |
| 263 | - | ||
| 264 | - | egui::ComboBox::from_id_salt("device_profile_picker") | |
| 265 | - | .selected_text(current_label) | |
| 266 | - | .width(250.0) | |
| 267 | - | .show_ui(ui, |ui| { | |
| 268 | - | if ui | |
| 269 | - | .selectable_value(&mut config.device_profile, None, "None (manual)") | |
| 270 | - | .clicked() | |
| 271 | - | { | |
| 272 | - | // Reset profile-derived fields when switching to manual | |
| 273 | - | config.naming_rules = None; | |
| 274 | - | config.max_file_size_bytes = None; | |
| 275 | - | config.name_overrides = None; | |
| 276 | - | } | |
| 277 | - | ||
| 278 | - | for profile in available_profiles { | |
| 279 | - | let label = format!("{} ({})", profile.name, profile.manufacturer); | |
| 280 | - | let value = Some(profile.name.clone()); | |
| 281 | - | ui.selectable_value(&mut config.device_profile, value, label); | |
| 282 | - | } | |
| 283 | - | }); | |
| 284 | - | ||
| 285 | - | // Show profile info when one is selected. M-6: surface the | |
| 286 | - | // device's supported formats / rates / depths / channels | |
| 287 | - | // so the user knows what the lock is hiding, not just | |
| 288 | - | // that something is hidden. | |
| 289 | - | if let Some(ref name) = config.device_profile | |
| 290 | - | && let Some(profile) = available_profiles.iter().find(|p| &p.name == name) | |
| 291 | - | { | |
| 292 | - | ui.label( | |
| 293 | - | egui::RichText::new(format!("by {}", profile.manufacturer)) | |
| 294 | - | .small() | |
| 295 | - | .color(theme::content_muted()), | |
| 296 | - | ); | |
| 297 | - | if let Some(ref summary) = profile.format_summary { | |
| 298 | - | ui.label( | |
| 299 | - | egui::RichText::new(summary) | |
| 300 | - | .small() | |
| 301 | - | .color(theme::content_muted()), | |
| 302 | - | ); | |
| 303 | - | } | |
| 304 | - | if let Some(ref category) = profile.category { | |
| 305 | - | ui.label( | |
| 306 | - | egui::RichText::new(category) | |
| 307 | - | .small() | |
| 308 | - | .color(theme::content_muted()), | |
| 309 | - | ); | |
| 310 | - | } | |
| 311 | - | if let Some(ref notes) = profile.notes { | |
| 312 | - | ui.label( | |
| 313 | - | egui::RichText::new(notes) | |
| 314 | - | .small() | |
| 315 | - | .color(theme::content_muted()), | |
| 316 | - | ); | |
| 317 | - | } | |
| 318 | - | } | |
| 319 | - | } | |
| 320 | - | ui.add_space(theme::space::peer()); | |
| 321 | - | } | |
| 322 | - | ||
| 323 | - | // --- Format --- | |
| 324 | - | let has_profile = matches!( | |
| 325 | - | &state.import_wf.import_mode, | |
| 326 | - | ImportMode::ConfigureExport { | |
| 327 | - | config: ExportConfig { | |
| 328 | - | device_profile: Some(_), | |
| 329 | - | .. | |
| 330 | - | }, | |
| 331 | - | .. | |
| 332 | - | } | |
| 333 | - | ); | |
| 334 | - | ||
| 335 | - | if !has_profile { | |
| 336 | - | ui.label(egui::RichText::new("Format").strong()); | |
| 337 | - | if let ImportMode::ConfigureExport { ref mut config, .. } = | |
| 338 | - | state.import_wf.import_mode | |
| 339 | - | { | |
| 340 | - | let is_original = config.format == ExportFormat::Original; | |
| 341 | - | let is_wav = config.format == ExportFormat::Wav; | |
| 342 | - | let is_aiff = config.format == ExportFormat::Aiff; | |
| 343 | - | ||
| 344 | - | if ui.radio(is_original, "Original (copy as-is)").clicked() && !is_original { | |
| 345 | - | config.format = ExportFormat::Original; | |
| 346 | - | } | |
| 347 | - | if ui.radio(is_wav, "WAV (decode and re-encode)").clicked() && !is_wav { | |
| 348 | - | config.format = ExportFormat::Wav; | |
| 349 | - | } | |
| 350 | - | if ui.radio(is_aiff, "AIFF (decode and re-encode)").clicked() && !is_aiff { | |
| 351 | - | config.format = ExportFormat::Aiff; | |
| 352 | - | } | |
| 353 | - | ||
| 354 | - | // Heads-up: WAV/AIFF re-encode writes only fmt+data / | |
| 355 | - | // COMM+SSND. Embedded BWF (bext), iXML, smpl loop points, | |
| 356 | - | // cue markers, and ID3 tags are not preserved. Choose | |
| 357 | - | // Original to keep them intact. | |
| 358 | - | if config.format != ExportFormat::Original { | |
| 359 | - | ui.add_space(theme::space::hair()); | |
| 360 | - | ui.label( | |
| 361 | - | egui::RichText::new( | |
| 362 | - | "Re-encoding strips embedded metadata chunks \ | |
| 363 | - | (BWF, iXML, loop points, cue markers, ID3). \ | |
| 364 | - | Choose Original to preserve them.", | |
| 365 | - | ) | |
| 366 | - | .small() | |
| 367 | - | .color(theme::warning()), | |
| 368 | - | ); | |
| 369 | - | } | |
| 370 | - | } | |
| 371 | - | ui.add_space(theme::space::peer()); | |
| 372 | - | ||
| 373 | - | // --- Audio encoding options (WAV/AIFF) --- | |
| 374 | - | let needs_encoding_options = matches!( | |
| 375 | - | &state.import_wf.import_mode, | |
| 376 | - | ImportMode::ConfigureExport { | |
| 377 | - | config: ExportConfig { | |
| 378 | - | format: ExportFormat::Wav | ExportFormat::Aiff, | |
| 379 | - | .. | |
| 380 | - | }, | |
| 381 | - | .. | |
| 382 | - | } | |
| 383 | - | ); | |
| 384 | - | ||
| 385 | - | if needs_encoding_options | |
| 386 | - | && let ImportMode::ConfigureExport { ref mut config, .. } = | |
| 387 | - | state.import_wf.import_mode | |
| 388 | - | { | |
| 389 | - | // Sample rate | |
| 390 | - | ui.label(egui::RichText::new("Sample Rate").strong()); | |
| 391 | - | let rates: [(Option<u32>, &str); 4] = [ | |
| 392 | - | (None, "Original"), | |
| 393 | - | (Some(44100), "44,100 Hz"), | |
| 394 | - | (Some(48000), "48,000 Hz"), | |
| 395 | - | (Some(96000), "96,000 Hz"), | |
| 396 | - | ]; | |
| 397 | - | for (rate, label) in &rates { | |
| 398 | - | if ui.radio(config.sample_rate == *rate, *label).clicked() { | |
| 399 | - | config.sample_rate = *rate; | |
| 400 | - | } | |
| 401 | - | } | |
| 402 | - | ui.add_space(theme::space::peer()); | |
| 403 | - | ||
| 404 | - | // Bit depth | |
| 405 | - | ui.label(egui::RichText::new("Bit Depth").strong()); | |
| 406 | - | let depths: [(Option<u16>, &str); 3] = [ | |
| 407 | - | (None, "Original"), | |
| 408 | - | (Some(16), "16-bit"), | |
| 409 | - | (Some(24), "24-bit"), | |
| 410 | - | ]; | |
| 411 | - | for (depth, label) in &depths { | |
| 412 | - | if ui.radio(config.bit_depth == *depth, *label).clicked() { | |
| 413 | - | config.bit_depth = *depth; | |
| 414 | - | } | |
| 415 | - | } | |
| 416 | - | ui.add_space(theme::space::peer()); | |
| 417 | - | } | |
| 418 | - | ||
| 419 | - | // --- Channels --- | |
| 420 | - | ui.label(egui::RichText::new("Channels").strong()); | |
| 421 | - | if let ImportMode::ConfigureExport { ref mut config, .. } = | |
| 422 | - | state.import_wf.import_mode | |
| 423 | - | { | |
| 424 | - | let ch_options: [(ExportChannels, &str); 3] = [ | |
| 425 | - | (ExportChannels::Original, "Original"), | |
| 426 | - | (ExportChannels::Mono, "Mono"), | |
| 427 | - | (ExportChannels::Stereo, "Stereo"), | |
| 428 | - | ]; | |
| 429 | - | for (ch, label) in &ch_options { | |
| 430 | - | if ui.radio(config.channels == *ch, *label).clicked() { | |
| 431 | - | config.channels = ch.clone(); | |
| 432 | - | } | |
| 433 | - | } | |
| 434 | - | } | |
| 435 | - | ui.add_space(theme::space::peer()); | |
| 436 | - | } | |
| 437 | - | ||
| 438 | - | // --- Structure --- | |
| 439 | - | ui.label(egui::RichText::new("Structure").strong()); | |
| 440 | - | if let ImportMode::ConfigureExport { ref mut config, .. } = state.import_wf.import_mode | |
| 441 | - | { | |
| 442 | - | if ui.radio(!config.flatten, "Preserve tree").clicked() && config.flatten { | |
| 443 | - | config.flatten = false; | |
| 444 | - | } | |
| 445 | - | if ui | |
| 446 | - | .radio(config.flatten, "Flatten (all files in one folder)") | |
| 447 | - | .clicked() | |
| 448 | - | && !config.flatten | |
| 449 | - | { | |
| 450 | - | config.flatten = true; | |
| 451 | - | } | |
| 452 | - | } | |
| 453 | - | ui.add_space(theme::space::peer()); | |
| 454 | - | ||
| 455 | - | // --- Metadata sidecar --- | |
| 456 | - | if let ImportMode::ConfigureExport { ref mut config, .. } = state.import_wf.import_mode | |
| 457 | - | { | |
| 458 | - | ui.checkbox( | |
| 459 | - | &mut config.metadata_sidecar, | |
| 460 | - | "Include metadata (.audiofiles.json)", | |
| 461 | - | ); | |
| 462 | - | } | |
| 463 | - | ui.add_space(theme::space::peer()); | |
| 464 | - | ||
| 465 | - | // --- Naming pattern (when flattened) --- | |
| 466 | - | if let ImportMode::ConfigureExport { | |
| 467 | - | ref mut config, | |
| 468 | - | ref items, | |
| 469 | - | .. | |
| 470 | - | } = state.import_wf.import_mode | |
| 471 | - | && config.flatten | |
| 472 | - | { | |
| 473 | - | ui.label(egui::RichText::new("Naming Pattern").strong()); | |
| 474 | - | let mut pattern = config.naming_pattern.clone().unwrap_or_default(); | |
| 475 | - | ||
| 476 | - | // Token chips (M-8): clicking appends the token to the | |
| 477 | - | // pattern. egui doesn't surface the cursor position on | |
| 478 | - | // TextEdit so append-to-end is the honest affordance. | |
| 479 | - | ui.horizontal_wrapped(|ui| { | |
| 480 | - | ui.label( | |
| 481 | - | egui::RichText::new("Tokens:") | |
| 482 | - | .small() | |
| 483 | - | .color(theme::content_muted()), | |
| 484 | - | ); | |
| 485 | - | const TOKENS: &[&str] = &[ | |
| 486 | - | "{name}", | |
| 487 | - | "{bpm}", | |
| 488 | - | "{key}", | |
| 489 | - | "{class}", | |
| 490 | - | "{duration}", | |
| 491 | - | "{n}", | |
| 492 | - | "{nn}", | |
| 493 | - | "{nnn}", | |
| 494 | - | "{ext}", | |
| 495 | - | ]; | |
| 496 | - | for tok in TOKENS { | |
| 497 | - | if ui | |
| 498 | - | .small_button(*tok) | |
| 499 | - | .on_hover_text("Append this token to the pattern") | |
| 500 | - | .clicked() | |
| 501 | - | { | |
| 502 | - | pattern.push_str(tok); | |
| 503 | - | } | |
| 504 | - | } | |
| 505 | - | }); | |
| 506 | - | ||
| 507 | - | let changed = ui.text_edit_singleline(&mut pattern).changed(); | |
| 508 | - | if changed || config.naming_pattern.as_deref().unwrap_or("") != pattern { | |
| 509 | - | config.naming_pattern = if pattern.is_empty() { | |
| 510 | - | None | |
| 511 | - | } else { | |
| 512 | - | Some(pattern.clone()) | |
| 513 | - | }; | |
| 514 | - | } | |
| 515 | - | ||
| 516 | - | // Live preview (M-7): parse + resolve against the first | |
| 517 | - | // item's context. Parse errors (unknown token, unclosed | |
| 518 | - | // brace) render in yellow so the user catches typos before | |
| 519 | - | // committing to a 200-file export. | |
| 520 | - | if !pattern.is_empty() { | |
| 521 | - | match audiofiles_core::rename::RenamePattern::parse(&pattern) { | |
| 522 | - | Ok(parsed) => { | |
| 523 | - | if let Some(first) = items.first() { | |
| 524 | - | let ctx = audiofiles_core::rename::RenameContext { | |
| 525 | - | name: first.name.clone(), | |
| 526 | - | extension: first.ext.clone(), | |
| 527 | - | bpm: first.bpm, | |
| 528 | - | musical_key: first.musical_key.clone(), | |
| 529 | - | duration: first.duration, | |
| 530 | - | index: 0, | |
| 531 | - | }; | |
| 532 | - | let stem = parsed.resolve(&ctx); | |
| 533 | - | let preview = if first.ext.is_empty() { | |
| 534 | - | stem | |
| 535 | - | } else { | |
| 536 | - | format!("{stem}.{}", first.ext) | |
| 537 | - | }; | |
| 538 | - | ui.label( | |
| 539 | - | egui::RichText::new(format!("Preview: {preview}")) | |
| 540 | - | .small() | |
| 541 | - | .color(theme::content_muted()), | |
| 542 | - | ); | |
| 543 | - | } | |
| 544 | - | } | |
| 545 | - | Err(e) => { | |
| 546 | - | ui.label( | |
| 547 | - | egui::RichText::new(format!("Pattern: {e}")) | |
| 548 | - | .small() | |
| 549 | - | .color(theme::warning()), | |
| 550 | - | ); | |
| 551 | - | } | |
| 552 | - | } | |
| 553 | - | } | |
| 554 | - | ui.add_space(theme::space::peer()); | |
| 555 | - | } | |
| 252 | + | draw_export_fields(ui, state); | |
| 556 | 253 | ||
| 557 | 254 | // --- Destination --- | |
| 558 | 255 | ui.label(egui::RichText::new("Destination").strong()); | |
| @@ -584,6 +281,374 @@ | |||
| 584 | 281 | }); | |
| 585 | 282 | } | |
| 586 | 283 | ||
| 284 | + | /// The export configuration form, as described fields. | |
| 285 | + | /// | |
| 286 | + | /// Six questions, in two groups split by the metadata-sidecar checkbox that | |
| 287 | + | /// sits between them: a bare toggle is a control rather than a field, which is | |
| 288 | + | /// the rule `settings_panel` set for this port, so it is drawn between the | |
| 289 | + | /// groups instead of being described as one. The destination stays outside for | |
| 290 | + | /// the other standing reason — there is no `FieldKind::Path`, deliberately, and | |
| 291 | + | /// a button plus a native dialog is not a question a form holds an answer to. | |
| 292 | + | /// | |
| 293 | + | /// The whole form used to be six hand-drawn `strong` labels over raw egui | |
| 294 | + | /// controls, and it was invisible to the port's sweep for three passes because | |
| 295 | + | /// that sweep matched `radio_value` and `selectable_value` and this screen asks | |
| 296 | + | /// five of its six questions with `ui.radio`. | |
| 297 | + | fn draw_export_fields(ui: &mut egui::Ui, state: &mut BrowserState) { | |
| 298 | + | let ImportMode::ConfigureExport { | |
| 299 | + | ref config, | |
| 300 | + | ref available_profiles, | |
| 301 | + | ref items, | |
| 302 | + | .. | |
| 303 | + | } = state.import_wf.import_mode | |
| 304 | + | else { | |
| 305 | + | return; | |
| 306 | + | }; | |
| 307 | + | ||
| 308 | + | // Owned first, borrowed second: `Choice` holds `&str` and the fillings need | |
| 309 | + | // `state` mutably. Same division as `import_screens::configure` — the | |
| 310 | + | // question is described, this app's encoding of the answer stays private. | |
| 311 | + | // | |
| 312 | + | // The empty value is "no profile", which is a real answer and not an | |
| 313 | + | // unanswered chooser: it is the default, it has a label, and choosing it is | |
| 314 | + | // what "export exactly what I configured" means. So this field is not the | |
| 315 | + | // shape makeover-layout `661ff8ba` blocks. | |
| 316 | + | let profile_options: Vec<(String, String)> = | |
| 317 | + | std::iter::once((String::new(), String::from("None (manual)"))) | |
| 318 | + | .chain( | |
| 319 | + | available_profiles | |
| 320 | + | .iter() | |
| 321 | + | .map(|p| (p.name.clone(), format!("{} ({})", p.name, p.manufacturer))), | |
| 322 | + | ) | |
| 323 | + | .collect(); | |
| 324 | + | let profile_choices: Vec<makeover_layout::Choice<'_>> = profile_options | |
| 325 | + | .iter() | |
| 326 | + | .map(|(value, label)| makeover_layout::Choice { value, label }) | |
| 327 | + | .collect(); | |
| 328 | + | ||
| 329 | + | // The chosen device's readout, which stays a block under the field rather | |
| 330 | + | // than becoming its `hint`. The rule this port has been applying is that a | |
| 331 | + | // message belongs to the thing it is about; these lines are about the | |
| 332 | + | // device, not about the question, and there are up to four of them. | |
| 333 | + | let profile_detail: Vec<String> = config | |
| 334 | + | .device_profile | |
| 335 | + | .as_ref() | |
| 336 | + | .and_then(|name| available_profiles.iter().find(|p| &p.name == name)) | |
| 337 | + | .map(|p| { | |
| 338 | + | std::iter::once(format!("by {}", p.manufacturer)) | |
| 339 | + | .chain(p.format_summary.clone()) | |
| 340 | + | .chain(p.category.clone()) | |
| 341 | + | .chain(p.notes.clone()) | |
| 342 | + | .collect() | |
| 343 | + | }) | |
| 344 | + | .unwrap_or_default(); | |
| 345 | + | ||
| 346 | + | // A profile answers the format, rate, depth and channel questions itself, | |
| 347 | + | // so those four are not asked while one is chosen. Conditional membership | |
| 348 | + | // in the field list, the same way the import strategy's vault name is. | |
| 349 | + | let has_profile = config.device_profile.is_some(); | |
| 350 | + | let needs_encoding_options = matches!(config.format, ExportFormat::Wav | ExportFormat::Aiff); | |
| 351 | + | let re_encoding = config.format != ExportFormat::Original; | |
| 352 | + | ||
| 353 | + | let mut profile_value = config.device_profile.clone().unwrap_or_default(); | |
| 354 | + | let mut format_value = String::from(match config.format { | |
| 355 | + | ExportFormat::Original => "original", | |
| 356 | + | ExportFormat::Wav => "wav", | |
| 357 | + | ExportFormat::Aiff => "aiff", | |
| 358 | + | }); | |
| 359 | + | let mut rate_value = config | |
| 360 | + | .sample_rate | |
| 361 | + | .map(|r| r.to_string()) | |
| 362 | + | .unwrap_or_default(); | |
| 363 | + | let mut depth_value = config.bit_depth.map(|d| d.to_string()).unwrap_or_default(); | |
| 364 | + | let mut channels_value = String::from(match config.channels { | |
| 365 | + | ExportChannels::Original => "original", | |
| 366 | + | ExportChannels::Mono => "mono", | |
| 367 | + | ExportChannels::Stereo => "stereo", | |
| 368 | + | }); | |
| 369 | + | let mut structure_value = String::from(if config.flatten { | |
| 370 | + | "flatten" | |
| 371 | + | } else { | |
| 372 | + | "preserve" | |
| 373 | + | }); | |
| 374 | + | let mut sidecar = config.metadata_sidecar; | |
| 375 | + | let mut pattern = config.naming_pattern.clone().unwrap_or_default(); | |
| 376 | + | let flatten = config.flatten; | |
| 377 | + | ||
| 378 | + | // The pattern's preview and its parse error are both facts about the value | |
| 379 | + | // in the field, and they are mutually exclusive, so one field carries both: | |
| 380 | + | // the preview as `hint`, the parse failure as `error`. It used to be two | |
| 381 | + | // detached lines under a text box with no well. | |
| 382 | + | let (pattern_hint, pattern_error) = if pattern.is_empty() { | |
| 383 | + | (None, None) | |
| 384 | + | } else { | |
| 385 | + | match audiofiles_core::rename::RenamePattern::parse(&pattern) { | |
| 386 | + | Ok(parsed) => ( | |
| 387 | + | items.first().map(|first| { | |
| 388 | + | let ctx = audiofiles_core::rename::RenameContext { | |
| 389 | + | name: first.name.clone(), | |
| 390 | + | extension: first.ext.clone(), | |
| 391 | + | bpm: first.bpm, | |
| 392 | + | musical_key: first.musical_key.clone(), | |
| 393 | + | duration: first.duration, | |
| 394 | + | index: 0, | |
| 395 | + | }; | |
| 396 | + | let stem = parsed.resolve(&ctx); | |
| 397 | + | if first.ext.is_empty() { | |
| 398 | + | format!("Preview: {stem}") | |
| 399 | + | } else { | |
| 400 | + | format!("Preview: {stem}.{}", first.ext) | |
| 401 | + | } | |
| 402 | + | }), | |
| 403 | + | None, | |
| 404 | + | ), | |
| 405 | + | Err(e) => (None, Some(format!("Pattern: {e}"))), | |
| 406 | + | } | |
| 407 | + | }; | |
| 408 | + | ||
| 409 | + | const FORMATS: [makeover_layout::Choice<'_>; 3] = [ | |
| 410 | + | makeover_layout::Choice { | |
| 411 | + | value: "original", | |
| 412 | + | label: "Original (copy as-is)", | |
| 413 | + | }, | |
| 414 | + | makeover_layout::Choice { | |
| 415 | + | value: "wav", | |
| 416 | + | label: "WAV (decode and re-encode)", | |
| 417 | + | }, | |
| 418 | + | makeover_layout::Choice { | |
| 419 | + | value: "aiff", | |
| 420 | + | label: "AIFF (decode and re-encode)", | |
| 421 | + | }, | |
| 422 | + | ]; | |
| 423 | + | const RATES: [makeover_layout::Choice<'_>; 4] = [ | |
| 424 | + | makeover_layout::Choice { | |
| 425 | + | value: "", | |
| 426 | + | label: "Original", | |
| 427 | + | }, | |
| 428 | + | makeover_layout::Choice { | |
| 429 | + | value: "44100", | |
| 430 | + | label: "44,100 Hz", | |
| 431 | + | }, | |
| 432 | + | makeover_layout::Choice { | |
| 433 | + | value: "48000", | |
| 434 | + | label: "48,000 Hz", | |
| 435 | + | }, | |
| 436 | + | makeover_layout::Choice { | |
| 437 | + | value: "96000", | |
| 438 | + | label: "96,000 Hz", | |
| 439 | + | }, | |
| 440 | + | ]; | |
| 441 | + | const DEPTHS: [makeover_layout::Choice<'_>; 3] = [ | |
| 442 | + | makeover_layout::Choice { | |
| 443 | + | value: "", | |
| 444 | + | label: "Original", | |
| 445 | + | }, | |
| 446 | + | makeover_layout::Choice { | |
| 447 | + | value: "16", | |
| 448 | + | label: "16-bit", | |
| 449 | + | }, | |
| 450 | + | makeover_layout::Choice { | |
| 451 | + | value: "24", | |
| 452 | + | label: "24-bit", | |
| 453 | + | }, | |
| 454 | + | ]; | |
| 455 | + | const CHANNELS: [makeover_layout::Choice<'_>; 3] = [ | |
| 456 | + | makeover_layout::Choice { | |
| 457 | + | value: "original", | |
| 458 | + | label: "Original", | |
| 459 | + | }, | |
| 460 | + | makeover_layout::Choice { | |
| 461 | + | value: "mono", | |
| 462 | + | label: "Mono", | |
| 463 | + | }, | |
| 464 | + | makeover_layout::Choice { | |
| 465 | + | value: "stereo", | |
| 466 | + | label: "Stereo", | |
| 467 | + | }, | |
| 468 | + | ]; | |
| 469 | + | const STRUCTURES: [makeover_layout::Choice<'_>; 2] = [ |
Lines truncated