1use std::sync::{LazyLock, Mutex};
4
5use burn::{
6 backend::{ndarray::NdArrayDevice, NdArray},
7 config::Config,
8 module::Module,
9 record::{BinBytesRecorder, Recorder},
10};
11
12use crate::{
13 analyze::base::{get_frequency_space, get_smoothed_frequency_space},
14 core::{base::Res, chord::Chord, pitch::Pitch},
15 ml::base::{
16 data::kord_item_to_sample_tensor,
17 helpers::{logits_to_predictions, logits_to_probabilities},
18 model::KordModel,
19 KordItem, StorePrecisionSettings, TrainConfig, FREQUENCY_SPACE_SIZE, PITCH_CLASS_COUNT,
20 },
21};
22
23#[cfg(feature = "ml_target_folded_bass")]
24use crate::ml::base::TARGET_FOLDED_BASS_NOTE_OFFSET;
25
26#[derive(Debug, Clone)]
28pub struct InferenceResult {
29 pub pitches: Vec<Pitch>,
31 pub chords: Vec<Chord>,
33 pub pitch_deltas: [f32; 12],
36}
37
38struct InferenceState {
43 model: KordModel<NdArray<f32>>,
44 thresholds: Vec<f32>,
45}
46
47static INFERENCE_STATE: LazyLock<Mutex<InferenceState>> = LazyLock::new(|| Mutex::new({
48 let device = NdArrayDevice::Cpu;
49
50 let config = TrainConfig::load_binary(CONFIG).expect("Could not load the config from within the binary");
51
52 let recorder = BinBytesRecorder::<StorePrecisionSettings>::new()
53 .load(Vec::from_iter(STATE_BINCODE.iter().cloned()), &device)
54 .expect("Could not load the state from within the binary");
55
56 let model = KordModel::<NdArray<f32>>::new(&device, config.mha_heads, config.dropout, config.trunk_hidden_size).load_record(recorder);
57
58 let thresholds: Vec<f32> = serde_json::from_slice(THRESHOLDS_JSON).expect("failed to deserialize thresholds");
59
60 InferenceState { model, thresholds }
61}));
62
63pub fn infer(audio_data: &[f32], length_in_seconds: u8) -> Res<InferenceResult> {
69 let frequency_space = get_frequency_space(audio_data, length_in_seconds);
70 let smoothed_frequency_space: [_; FREQUENCY_SPACE_SIZE] = get_smoothed_frequency_space(&frequency_space, length_in_seconds)
71 .into_iter()
72 .take(FREQUENCY_SPACE_SIZE)
73 .map(|(_, v)| v)
74 .collect::<Vec<_>>()
75 .try_into()
76 .map_err(|_| anyhow::Error::msg("Failed to convert smoothed frequency space into array"))?;
77
78 let kord_item = KordItem {
79 frequency_space: smoothed_frequency_space,
80 ..Default::default()
81 };
82
83 let state = INFERENCE_STATE.lock().map_err(|e| anyhow::anyhow!("inference state lock poisoned: {e}"))?;
84 let device = NdArrayDevice::Cpu;
85
86 let sample = kord_item_to_sample_tensor(&device, &kord_item).detach();
88
89 let logits = state.model.forward(sample).detach();
91 let logits_vec: Vec<f32> = logits
92 .into_data()
93 .convert::<f32>()
94 .to_vec()
95 .map_err(|e| anyhow::anyhow!("failed to convert logits tensor to vec: {e:?}"))?;
96 let probabilities = logits_to_probabilities(&logits_vec);
97 let inferred = logits_to_predictions(&probabilities, &state.thresholds);
98
99 #[cfg(feature = "ml_target_full")]
105 compile_error!("Inference with ml_target_full is not supported; use ml_target_folded or ml_target_folded_bass.");
106 #[cfg(feature = "ml_target_folded_bass")]
107 let note_offset = TARGET_FOLDED_BASS_NOTE_OFFSET;
108 #[cfg(feature = "ml_target_folded")]
109 let note_offset = 0;
110
111 let mut pitches = Vec::new();
112 let mut pitch_deltas = [0.0f32; 12];
113
114 for pitch_class_index in 0..PITCH_CLASS_COUNT {
115 let idx = note_offset + pitch_class_index;
116 let is_present = inferred.get(idx).copied().unwrap_or(0.0);
117
118 let probability = probabilities.get(idx).copied().unwrap_or(0.0);
120 let threshold = state.thresholds.get(idx).copied().unwrap_or(0.5);
121 pitch_deltas[pitch_class_index] = probability - threshold;
122
123 if is_present == 1.0 {
124 let pitch = Pitch::try_from(pitch_class_index as u8).map_err(|e| anyhow::Error::msg(format!("Invalid pitch class {}: {}", pitch_class_index, e)))?;
125 pitches.push(pitch);
126 }
127 }
128
129 let chords = if pitches.is_empty() {
133 vec![]
134 } else {
135 #[allow(unused_variables)]
136 Chord::try_from_pitches(&pitches).unwrap_or_else(|e| {
137 #[cfg(feature = "cli")]
138 eprintln!("Could not determine chords from pitches: {}", e);
139 vec![]
140 })
141 };
142
143 Ok(InferenceResult { pitches, chords, pitch_deltas })
144}
145
146static CONFIG: &[u8] = include_bytes!("../../../model/model_config.json");
148static STATE_BINCODE: &[u8] = include_bytes!("../../../model/state.json.bin");
149static THRESHOLDS_JSON: &[u8] = include_bytes!("../../../model/thresholds.json");
150
151#[cfg(test)]
154#[cfg(feature = "ml_infer")]
155mod tests {
156 use std::{fs::File, io::Read};
157
158 use crate::core::base::HasName;
159
160 use super::*;
161
162 #[test]
163 fn test_inference() {
164 let mut file = File::open("tests/vec.bin").unwrap();
165 let file_size = file.metadata().unwrap().len() as usize;
166 let float_size = std::mem::size_of::<f32>();
167 let element_count = file_size / float_size;
168 let mut buffer = vec![0u8; file_size];
169
170 file.read_exact(&mut buffer).unwrap();
172
173 let audio_data: Vec<f32> = unsafe { std::slice::from_raw_parts(buffer.as_ptr() as *const f32, element_count).to_vec() };
175
176 let inference_result = infer(&audio_data, 5).unwrap();
178
179 assert!(!inference_result.pitches.is_empty(), "expected at least one pitch class");
182 assert!(!inference_result.chords.is_empty(), "expected at least one chord candidate");
183
184 let name = inference_result.chords[0].name_ascii();
185 assert!(name.starts_with("C7") || name.starts_with("C/C 7"), "expected a C7 chord variant, got: {name}");
186 }
187}