From 5c36ac0835617746f6acee6c0fb32d1ff59b751e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Sun, 12 Nov 2023 09:04:54 -0500 Subject: [PATCH 1/6] Fix test following toml update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hubert Figuière --- src/instruments/instrument.rs | 2 +- src/instruments/mod.rs | 2 +- src/soundbanks/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/instruments/instrument.rs b/src/instruments/instrument.rs index 7e2dcc5..e542713 100644 --- a/src/instruments/instrument.rs +++ b/src/instruments/instrument.rs @@ -93,7 +93,7 @@ mod tests { #[test] fn test_instrument() { - let list = load_preset_list(include_bytes!("tests/presets.toml")); + let list = load_preset_list(include_str!("tests/presets.toml")); let preset = list.get("steinway"); assert!(preset.is_some()); diff --git a/src/instruments/mod.rs b/src/instruments/mod.rs index 546af38..4d7c8cc 100644 --- a/src/instruments/mod.rs +++ b/src/instruments/mod.rs @@ -78,7 +78,7 @@ mod tests { #[test] fn test_instruments_loading() { - let list = super::load_preset_list(include_bytes!("tests/presets.toml")); + let list = super::load_preset_list(include_str!("tests/presets.toml")); assert!(!list.is_empty()); assert_eq!(list.len(), 6); diff --git a/src/soundbanks/mod.rs b/src/soundbanks/mod.rs index 55f3dfd..a14236b 100644 --- a/src/soundbanks/mod.rs +++ b/src/soundbanks/mod.rs @@ -136,7 +136,7 @@ mod tests { #[test] fn test_soundbank_loading() { - let list = super::load_soundbank_list(include_bytes!("tests/soundbanks.toml")); + let list = super::load_soundbank_list(include_str!("tests/soundbanks.toml")); assert!(!list.is_empty()); assert_eq!(list.len(), 3); -- GitLab From 209d6b71b58f8e38a38e4b4caffe2a071c67e18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Sun, 12 Nov 2023 09:08:51 -0500 Subject: [PATCH 2/6] test: allow 60 seconds because meson can dissociate build from test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hubert Figuière --- src/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/src/meson.build b/src/meson.build index 6a02ff1..97908d3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -112,6 +112,7 @@ test( 'cargo-test', cargo, env: cargo_env, + timeout: 60, args: [ 'test', cargo_options -- GitLab From c5fd4893c966185d257d14444e646eaec8f6359e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Sun, 5 Nov 2023 19:47:13 -0500 Subject: [PATCH 3/6] Issue #4 - audio: Switch to pipewire - Reworked qwertone to deliver more than one sample at a time - No more CPAL - Remove audio output selection UI - We are now 48KHz https://gitlab.gnome.org/hub/compiano/-/issues/4 --- Cargo.toml | 2 +- meson.build | 1 + src/audio/engine.rs | 200 +++++++++++++++++----------------- src/audio/mixer.rs | 35 ++++-- src/audio/mod.rs | 26 +---- src/audio_stack.rs | 17 +-- src/events.rs | 3 - src/instruments/instrument.rs | 4 +- src/main.rs | 4 +- src/settings.rs | 4 +- src/synth/fluidlite_synth.rs | 20 ++-- src/synth/qwertone/mod.rs | 119 ++++++++++++++------ src/synth/qwertone_synth.rs | 6 +- src/ui/preferences.rs | 33 ------ src/ui/preferences.ui | 11 -- src/window.rs | 41 +------ src/window.ui | 20 ---- 17 files changed, 241 insertions(+), 305 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 28f9799..8cbc039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ anyhow = "1.0.43" async-channel = "1.6.1" async-std = { version = "1.9.0", features = ["attributes", "tokio1"] } async-trait = "0.1.50" -cpal = "0.15.2" crossbeam-channel = "0.5.1" dbus = "0.9.7" env_logger = "^0.10.0" @@ -33,6 +32,7 @@ lzma-rs = "0.2.0" midir = "0.9.1" midi-control = "0.2.2" once_cell = "1.15.0" +pipewire = { version = "0.7.2", features = ["v0_3_65"] } serde = "1.0.189" serde_derive = "^1.0" sha2 = "0.10.6" diff --git a/meson.build b/meson.build index e097268..283c176 100644 --- a/meson.build +++ b/meson.build @@ -12,6 +12,7 @@ i18n = import('i18n') dependency('gtk4', version: '>= 4.4') dependency('libadwaita-1', version: '>= 1.2.0') +dependency('libpipewire-0.3', version: '>= 0.3.65') cargo = find_program('cargo', required: true) version = meson.project_version() diff --git a/src/audio/engine.rs b/src/audio/engine.rs index d013f17..db7ed87 100644 --- a/src/audio/engine.rs +++ b/src/audio/engine.rs @@ -3,122 +3,124 @@ // use anyhow::Result; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use pipewire::{ + keys, properties, spa, + stream::{Stream, StreamFlags, StreamRef}, + Context, MainLoop, +}; +use spa::{data::Data, pod::Pod, Direction}; use super::AudioSource; +const CHAN_SIZE: usize = std::mem::size_of::(); + /// The audio engine, takes a source and output to the sound subsystem. -pub struct Engine { - host: cpal::Host, - device: Option, - config: Option, - stream: Option, -} +pub struct Engine(std::thread::JoinHandle<()>); impl Engine { - /// Create the audio engine. - pub fn new() -> Option { - let host = cpal::default_host(); - let device = host.default_output_device(); - // .expect("no output device available"); - let config = device - .as_ref() - .and_then(|device| device.default_output_config().ok()); - // .expect("failed getting default output format"); // XXX properly handle - - Some(Engine { - host, - device, - config, - stream: None, - }) - } - - pub fn select_output(&mut self, device_name: &str) -> Result<()> { - let device = self.host.output_devices()?.find(|device| { - device - .name() - .map(|name| name == device_name) - .unwrap_or(false) - }); - let config = device - .as_ref() - .and_then(|device| device.default_output_config().ok()); - self.device = device; - self.config = config; - - Ok(()) - } - /// Returns the sample rate associated to the output format. - pub fn get_sample_rate(&self) -> u32 { - self.config - .as_ref() - .map(|config| config.sample_rate().0) - .unwrap_or(0) + pub fn sample_rate(&self) -> u32 { + super::DEFAULT_RATE } /// Start the audio engine and run it. /// /// It is run on a different thread, hence the requirement for the [`AudioSource`] /// to be also `Send`. - pub fn start(&mut self, source: Box) -> Result<()> { - let config = self - .config - .as_ref() - .ok_or(cpal::BuildStreamError::StreamConfigNotSupported)?; - let stream = match config.sample_format() { - cpal::SampleFormat::F32 => self.run::(source)?, - cpal::SampleFormat::I16 => self.run::(source)?, - cpal::SampleFormat::U16 => self.run::(source)?, - _ => Err(anyhow::anyhow!("Unsupported format"))?, - }; + pub fn start(mut source: Box) -> Result { + pipewire::init(); - self.stream = Some(stream); - Ok(()) - } + let engine = Engine( + std::thread::Builder::new() + .name("audio engine loop".to_string()) + .spawn(move || { + let mainloop = MainLoop::new().expect("Failed to create main loop"); + let context = Context::new(&mainloop).expect("Failed to create context"); + if let Ok(core) = context.connect(None) { + // let registry = core.get_registry()?; + let stream = Stream::new( + &core, + "compiano synth", + properties! { + *keys::MEDIA_TYPE => "Audio", + *keys::MEDIA_CATEGORY => "Playback", + *keys::MEDIA_ROLE => "Music" + }, + ) + .expect("Failed to create stream"); - /// Start the audio thread with the source. - fn run(&self, mut source: Box) -> Result - where - T: cpal::Sample + cpal::SizedSample + cpal::FromSample, - { - let err_fn = |err| eprintln!("an error occurred on stream: {err}"); - if let Some(ref config) = self.config { - let channels = config.channels() as usize; - let device = self - .device - .as_ref() - .ok_or(cpal::BuildStreamError::DeviceNotAvailable)?; - let stream = device.build_output_stream( - &config.config(), - move |data: &mut [T], _: &cpal::OutputCallbackInfo| { - source.process_messages(); - write_data(data, channels, &mut source) - }, - err_fn, - None, - )?; - stream.play()?; - Ok(stream) - } else { - log::error!("no config"); - Err(anyhow::Error::new( - cpal::BuildStreamError::StreamConfigNotSupported, - )) - } + let _listener = stream + .add_local_listener() + .process(move |stream: &StreamRef, _: &mut ()| { + match stream.dequeue_buffer() { + None => println!("No buffer"), + Some(mut b) => { + source.process_messages(); + let datas = b.datas_mut(); + let data = &mut datas[0]; + write_data(data, &mut source); + } + } + }) + .register() + .expect("Couldn't register listener"); + + let mut audio_info = spa::param::audio::AudioInfoRaw::new(); + audio_info.set_format(spa::param::audio::AudioFormat::F32LE); + audio_info.set_rate(super::DEFAULT_RATE); + audio_info.set_channels(super::DEFAULT_CHANNELS as u32); + + let values: Vec = spa::pod::serialize::PodSerializer::serialize( + std::io::Cursor::new(Vec::new()), + &spa::pod::Value::Object(spa::pod::Object { + type_: spa::sys::SPA_TYPE_OBJECT_Format, + id: spa::sys::SPA_PARAM_EnumFormat, + properties: audio_info.into(), + }), + ) + .unwrap() + .0 + .into_inner(); + + let mut params = [Pod::from_bytes(&values).unwrap()]; + + stream + .connect( + Direction::Output, + None, + StreamFlags::AUTOCONNECT + | StreamFlags::MAP_BUFFERS + | StreamFlags::RT_PROCESS, + &mut params, + ) + .expect("Connect failed"); + mainloop.run(); + } + })?, + ); + Ok(engine) } } -fn write_data(output: &mut [T], channels: usize, source: &mut Box) -where - T: cpal::Sample + cpal::FromSample, -{ - for frame in output.chunks_mut(channels) { - let values = source.get_samples(); - let values = [T::from_sample(values[0]), T::from_sample(values[1])]; - for sample in frame.iter_mut() { - *sample = values[0]; +fn write_data(output: &mut Data, source: &mut Box) { + let stride = super::DEFAULT_CHANNELS * CHAN_SIZE; + let n_frames = if let Some(data) = output.data() { + let n_frames = data.len() / stride; + // XXX hardcoded to 2 in get_samples(). + let values = source.get_samples(n_frames); + for f in 0..n_frames { + for (i, channel) in values.iter().enumerate().take(super::DEFAULT_CHANNELS) { + let start = f * stride + i * CHAN_SIZE; + let chan = &mut data[start..start + CHAN_SIZE]; + chan.copy_from_slice(&f32::to_le_bytes(channel[f])); + } } - } + n_frames + } else { + 0 + }; + let chunk = output.chunk_mut(); + *chunk.offset_mut() = 0; + *chunk.stride_mut() = stride as _; + *chunk.size_mut() = (stride * n_frames) as _; } diff --git a/src/audio/mixer.rs b/src/audio/mixer.rs index 6606d6a..7b84cb9 100644 --- a/src/audio/mixer.rs +++ b/src/audio/mixer.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: (c) Andrii Zymohliad, Hubert Figuière +// SPDX-FileCopyrightText: (c) Andrii Zymohliad, 2022-2023 Hubert Figuière // Originally copied from https://gitlab.com/azymohliad/qwertone/-/blob/master/src/audio/sources/mixer.rs use crossbeam_channel::{self, Receiver, Sender}; @@ -33,6 +33,7 @@ pub struct Mixer { messages: Receiver, synth_id: Option, last_id: usize, + buffer: [[f32; super::DEFAULT_RATE as usize]; 2], } impl Mixer { @@ -43,6 +44,7 @@ impl Mixer { messages: rx, synth_id: None, last_id: 1, + buffer: [[0.0_f32; super::DEFAULT_RATE as usize]; 2], }; (mixer, tx) } @@ -81,12 +83,31 @@ impl Mixer { } impl AudioSource for Mixer { - fn get_samples(&mut self) -> [f32; 2] { - self.inputs - .iter_mut() - .filter(|x| x.enabled) - .map(|x| x.source.get_samples().map(|v| v * x.volume)) - .fold([0.0, 0.0], |acc, a| [acc[0] + a[0], acc[1] + a[1]]) + fn get_samples(&mut self, n_frames: usize) -> [&[f32]; 2] { + let mut num_inputs = 0; + self.buffer.iter_mut().for_each(|ch| { + ch.fill(0.0); + }); + let buffer = &mut self.buffer; + self.inputs.iter_mut().filter(|x| x.enabled).for_each(|x| { + num_inputs += 1; + let volume = x.volume; + x.source + .get_samples(n_frames) + .iter() + .enumerate() + .for_each(|(idx, ch)| { + let out_ch = &mut buffer[idx]; + ch.iter() + .map(|v| v * volume) + .enumerate() + .for_each(|(idx, v)| out_ch[idx] += v) + }) + }); + self.buffer.iter_mut().for_each(|ch| { + let _ = ch.iter_mut().map(|v| *v / num_inputs as f32); + }); + [&self.buffer[0], &self.buffer[1]] } fn process_messages(&mut self) { diff --git a/src/audio/mod.rs b/src/audio/mod.rs index 359bf70..eb1de4f 100644 --- a/src/audio/mod.rs +++ b/src/audio/mod.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: (c) 2020-2022 Hubert Figuière +// SPDX-FileCopyrightText: (c) 2020-2023 Hubert Figuière // mod engine; @@ -9,32 +9,16 @@ pub use engine::Engine; pub use mixer::Message as MixerMessage; pub use mixer::{Mixer, MixerHandle}; +pub const DEFAULT_RATE: u32 = 48000; +pub const DEFAULT_CHANNELS: usize = 2; + /// The trait for the AudioSource. /// Implement that trait to output sound. pub trait AudioSource { /// Get the next sample. - fn get_samples(&mut self) -> [f32; 2]; + fn get_samples(&mut self, n_frames: usize) -> [&[f32]; 2]; /// Process messages. /// /// This is where you want to run the sound generator after processing the input. fn process_messages(&mut self); } - -pub struct Host {} - -impl Host { - pub fn devices() -> Vec { - use cpal::traits::{DeviceTrait, HostTrait}; - - let mut d = vec![]; - let host = cpal::default_host(); // host_from_id(cpal::HostId::Alsa).ok() - if let Ok(devices) = host.output_devices() { - for device in devices { - let name = device.name().unwrap_or_default(); - d.push(name); - } - } - - d - } -} diff --git a/src/audio_stack.rs b/src/audio_stack.rs index 714228d..ca01536 100644 --- a/src/audio_stack.rs +++ b/src/audio_stack.rs @@ -23,8 +23,6 @@ use crate::toolkit; pub enum AudioMessage { /// A soundbank has been made available (download finished) SoundbankAvailable(String, std::path::PathBuf), - /// Select audio output - SelectAudioOutput(String), } /// The audio stack (controller) @@ -37,7 +35,7 @@ pub enum AudioMessage { /// * Requesting the application to display the UI with a [`Message`]. pub struct AudioStack { /// The audio engine. - engine: RefCell, + _engine: RefCell, /// MIDI control sender. pub midi_control: MidiControlSender, /// The mixer handle. @@ -51,19 +49,15 @@ pub struct AudioStack { impl AudioStack { /// Create a new audio stack with a MidiControl pub fn new(midi_control: MidiControlSender) -> Arc { - let mut engine = audio::Engine::new().expect("Can't create the audio engine"); - - let sample_rate = engine.get_sample_rate(); let (mixer, mixer_handle) = audio::Mixer::new(); let (sender, receiver) = glib::MainContext::channel::(glib::PRIORITY_DEFAULT); - engine - .start(Box::new(mixer)) - .expect("Failed to start audio engine."); + let engine = audio::Engine::start(Box::new(mixer)).expect("Failed to start audio engine."); + let sample_rate = engine.sample_rate(); let stack = Arc::new(AudioStack { - engine: RefCell::new(engine), + _engine: RefCell::new(engine), midi_control, sample_rate, mixer: mixer_handle, @@ -159,9 +153,6 @@ impl AudioStack { .midi_control .send(MidiControlMessage::SoundbankAvailable(soundbank, path))); } - SelectAudioOutput(device) => { - print_on_err!(self.engine.borrow_mut().select_output(&device)) - } } } } diff --git a/src/events.rs b/src/events.rs index d78be18..c2929e0 100644 --- a/src/events.rs +++ b/src/events.rs @@ -20,8 +20,6 @@ pub enum Message { PianoKeyDown(u8), /// A piano key is up PianoKeyUp(u8), - /// Audio output device changed - AudioOutputChanged(String), /// An instrument was select. Contain the instrument id (string). InstrumentSelected(String), /// Show instrument UI, sent by the [`AudioStack`][crate::audio_stack::AudioStack] @@ -43,7 +41,6 @@ pub fn dispatch(event: Message, window: &window::WindowController) { ChannelSelected(ch) => window.channel_selected(ch), PianoKeyDown(note) => window.piano_key_down(note), PianoKeyUp(note) => window.piano_key_up(note), - AudioOutputChanged(s) => window.audio_output_changed(&s), InstrumentSelected(s) => window.instrument_selected(&s), ShowInstrumentUi(s, ui) => window.show_instrument_ui(&s, ui), KeyboardShortcuts => {} diff --git a/src/instruments/instrument.rs b/src/instruments/instrument.rs index e542713..54dceac 100644 --- a/src/instruments/instrument.rs +++ b/src/instruments/instrument.rs @@ -99,7 +99,7 @@ mod tests { assert!(preset.is_some()); let preset = preset.unwrap(); - let instrument = Instrument::new(preset, 44100); //XXX does that value work? + let instrument = Instrument::new(preset, crate::audio::DEFAULT_RATE); assert!(instrument.is_some()); let instrument = instrument.unwrap(); assert!(instrument.soundbank.is_some()); @@ -111,7 +111,7 @@ mod tests { assert!(preset2.is_some()); let preset2 = preset2.unwrap(); - let instrument2 = Instrument::new(preset2, 44100); //XXX does that value work? + let instrument2 = Instrument::new(preset2, crate::audio::DEFAULT_RATE); assert!(instrument2.is_some()); let instrument2 = instrument2.unwrap(); assert!(instrument2.soundbank.is_none()); diff --git a/src/main.rs b/src/main.rs index 09e8a08..981e1ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: (c) 2020-2022 Hubert Figuière +// SPDX-FileCopyrightText: (c) 2020-2024 Hubert Figuière // use std::rc::Rc; @@ -54,8 +54,6 @@ fn main() { icon_theme.add_resource_path("/net/figuiere/compiano/icons"); } - crate::audio::Host::devices(); - let app = adw::Application::new(Some(config::APP_ID), Default::default()); app.connect_activate(move |app| { let (tx, rx) = async_channel::unbounded(); diff --git a/src/settings.rs b/src/settings.rs index 32f22b8..acfe412 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: (c) 2020-2022 Hubert Figuière +// SPDX-FileCopyrightText: (c) 2020-2023 Hubert Figuière // //! Provide high-level primitives to access key values stored in settings. @@ -9,8 +9,6 @@ use gtk4::gio::prelude::*; use crate::config; -/// The last audio output. -pub static AUDIO_OUTPUT_DEVICE: &str = "audio-output-device"; /// The last MIDI input device. pub static MIDI_INPUT_DEVICE: &str = "midi-input-device"; /// The MIDI channel for input. diff --git a/src/synth/fluidlite_synth.rs b/src/synth/fluidlite_synth.rs index d0fbbb8..8a26d05 100644 --- a/src/synth/fluidlite_synth.rs +++ b/src/synth/fluidlite_synth.rs @@ -118,8 +118,7 @@ fn synth_load_soundfont( }) } -// About .05 sec worth -const SAMPLES_SIZE: usize = 2205 * 2; +const SAMPLES_SIZE: usize = crate::audio::DEFAULT_RATE as usize; //2205 * 2; /// A synth using fluidlite to play soundfonts. `.sf` and `.sf2`. pub struct FluidLiteSynth { @@ -146,13 +145,9 @@ pub struct FluidLiteSynth { } impl AudioSource for FluidLiteSynth { - fn get_samples(&mut self) -> [f32; 2] { - let s = self.samples[self.pos]; - self.pos += 1; - if self.pos == SAMPLES_SIZE { - self.fill_buffer(); - } - [s, s] + fn get_samples(&mut self, n_frames: usize) -> [&[f32]; 2] { + self.fill_buffer(n_frames); + [&self.samples, &self.samples] } fn process_messages(&mut self) { @@ -263,6 +258,7 @@ impl FluidLiteSynth { // XXX only do that if necessary. let (sender, receiver) = glib::MainContext::channel::(glib::PRIORITY_DEFAULT); + let samples = [0.0f32; SAMPLES_SIZE]; let this = FluidLiteSynth { ui: Some(sender), synth, @@ -271,7 +267,7 @@ impl FluidLiteSynth { soundbank_name, bank, preset_num, - samples: [0.0f32; SAMPLES_SIZE], + samples, pos: 0, }; let handle = FluidLiteSynthHandle(tx); @@ -327,8 +323,8 @@ impl FluidLiteSynth { } /// Fill the buffer of samples from fluidlite, and reset the position. - fn fill_buffer(&mut self) { - print_on_err!(self.synth.write(self.samples.as_mut())); + fn fill_buffer(&mut self, n_frames: usize) { + print_on_err!(self.synth.write(self.samples[0..n_frames].as_mut())); self.pos = 0; } } diff --git a/src/synth/qwertone/mod.rs b/src/synth/qwertone/mod.rs index 3bf8b8e..da2e614 100644 --- a/src/synth/qwertone/mod.rs +++ b/src/synth/qwertone/mod.rs @@ -1,15 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: (c) Andrii Zymohliad +// SPDX-FileCopyrightText: (c) Andrii Zymohliad, (c) 2020-2023 Hubert Figuière // Originally copied from https://gitlab.com/azymohliad/qwertone/-/blob/master/src/audio/sources/synthesizer/mod.rs // +// Ported to be compatible with pipewire. -pub mod amplitude; -pub mod tone; -pub mod utils; +mod amplitude; +mod tone; +mod utils; + +use std::cell::RefCell; + +use crossbeam_channel::{self, Receiver, Sender}; use crate::audio::AudioSource; use amplitude::AmplitudeMap; -use crossbeam_channel::{self, Receiver, Sender}; use tone::Tone; const CHANNEL_SIZE: usize = 256; @@ -21,18 +25,25 @@ pub enum Message { pub type SynthesizerHandle = Sender; -pub struct Synthesizer { +struct State { // State notes: Vec, position: u32, + obertones: Vec<(f32, f32)>, + active_notes: Vec, +} + +pub struct Synthesizer { + state: RefCell, // Settings sample_rate: u32, - obertones: Vec<(f32, f32)>, normalizer: f32, pressed_fade_map: AmplitudeMap, released_fade_map: AmplitudeMap, // Message queue messages: Receiver, + // Buffer for output. + buffer: [f32; crate::audio::DEFAULT_RATE as usize], } impl Synthesizer { @@ -41,26 +52,40 @@ impl Synthesizer { let (tx, rx) = crossbeam_channel::bounded(CHANNEL_SIZE); let obertones = vec![(1.0, 1.0), (2.0, 0.25), (4.0, 0.125)]; let normalizer = utils::get_obertone_normalizer(&obertones); + let notes = (0..notes_number) .map(|i| Tone::new(utils::get_note_frequency(i))) .collect(); - let synthesizer = Synthesizer { + let state = State { notes, position: 0, - sample_rate, obertones, + active_notes: Vec::with_capacity(notes_number), + }; + let synthesizer = Synthesizer { + state: RefCell::new(state), + sample_rate, normalizer, pressed_fade_map: amplitude::default_hold(), released_fade_map: amplitude::default_release(), messages: rx, + buffer: [0.0; crate::audio::DEFAULT_RATE as usize], }; (synthesizer, tx) } - // -- Private methods ----------------------------------------------------- + fn note_hold(&mut self, note_id: usize) { + self.state.get_mut().note_hold(note_id); + } + + fn note_release(&mut self, note_id: usize) { + self.state.get_mut().note_release(note_id); + } +} +impl State { fn note_hold(&mut self, note_id: usize) { self.notes.get_mut(note_id).map(Tone::hold); } @@ -69,14 +94,19 @@ impl Synthesizer { self.notes.get_mut(note_id).map(Tone::release); } - fn advance_sample_state(&mut self) { - self.position = (self.position + 1) % (self.sample_rate * 60); + fn advance_sample_state( + &mut self, + sample_rate: u32, + pressed_fade_map: &AmplitudeMap, + released_fade_map: &AmplitudeMap, + ) { + self.position = (self.position + 1) % (sample_rate * 60); for note in self.notes.iter_mut().filter(|note| note.is_active) { - let time = note.hold_elapsed_cycles as f32 / self.sample_rate as f32; - note.amplitude_target = self.pressed_fade_map.get_value(time); + let time = note.hold_elapsed_cycles as f32 / sample_rate as f32; + note.amplitude_target = pressed_fade_map.get_value(time); if !note.is_held { - let time = note.release_elapsed_cycles as f32 / self.sample_rate as f32; - note.amplitude_target *= self.released_fade_map.get_value(time) + let time = note.release_elapsed_cycles as f32 / sample_rate as f32; + note.amplitude_target *= released_fade_map.get_value(time) } note.advance(); } @@ -86,24 +116,45 @@ impl Synthesizer { // -- Impl AudioSource trait -------------------------------------------------- impl AudioSource for Synthesizer { - fn get_samples(&mut self) -> [f32; 2] { - self.advance_sample_state(); - let time = self.position as f32 / self.sample_rate as f32; - let value = self - .notes - .iter() - .filter(|note| note.is_active) - .map(|note| { - let x = time * note.frequency * 2.0 * std::f32::consts::PI; - self.obertones - .iter() - .map(|(mul, den)| (mul * x).sin() * den) - .sum::() - * note.amplitude_current - / self.normalizer - }) - .sum::(); - [value, value] + fn get_samples(&mut self, n_frames: usize) -> [&[f32]; 2] { + let normalizer = self.normalizer; + let sample_rate = self.sample_rate; + let pressed_fade_map = &self.pressed_fade_map; + let released_fade_map = &self.released_fade_map; + let buffer = &mut self.buffer[0..n_frames]; + let state = self.state.get_mut(); + + // active notes haven't change, so we get them once. + state.active_notes.truncate(0); + state.active_notes.extend( + state + .notes + .iter() + .enumerate() + .filter(|(_, note)| note.is_active) + .map(|(idx, _)| idx), + ); + + buffer.iter_mut().for_each(|value| { + state.advance_sample_state(sample_rate, pressed_fade_map, released_fade_map); + let time = state.position as f32 / sample_rate as f32; + *value = state + .active_notes + .iter() + .map(|idx| { + let note = &state.notes[*idx]; + let x = time * note.frequency * std::f32::consts::TAU; + state + .obertones + .iter() + .map(|(mul, den)| (mul * x).sin() * den) + .sum::() + * note.amplitude_current + / normalizer + }) + .sum::() + }); + [&self.buffer, &self.buffer] } fn process_messages(&mut self) { diff --git a/src/synth/qwertone_synth.rs b/src/synth/qwertone_synth.rs index f606fc5..a8bd29c 100644 --- a/src/synth/qwertone_synth.rs +++ b/src/synth/qwertone_synth.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: (c) 2020-2022 Hubert Figuière +// SPDX-FileCopyrightText: (c) 2020-2023 Hubert Figuière // use midi_control as midi; @@ -58,8 +58,8 @@ impl SynthHandle for QwertoneSynthHandle { pub struct QwertoneSynth(qwertone::Synthesizer); impl AudioSource for QwertoneSynth { - fn get_samples(&mut self) -> [f32; 2] { - self.0.get_samples() + fn get_samples(&mut self, n_frames: usize) -> [&[f32]; 2] { + self.0.get_samples(n_frames) } fn process_messages(&mut self) { diff --git a/src/ui/preferences.rs b/src/ui/preferences.rs index 61081cf..5bfcc4e 100644 --- a/src/ui/preferences.rs +++ b/src/ui/preferences.rs @@ -14,7 +14,6 @@ use i18n_format::i18n_fmt; use midi_control as midi; use super::dialog_controller::DialogController; -use crate::audio; use crate::events; use crate::midi::MidiState; use crate::settings; @@ -24,39 +23,11 @@ use crate::toolkit; pub struct PreferenceDialog { dialog: adw::PreferencesWindow, - audio_output_combo: adw::ComboRow, midi_input_combo: adw::ComboRow, midi_channel_combo: adw::ComboRow, } impl PreferenceDialog { - /// Setup prefs UI for audio output. - fn setup_audio( - &self, - sender: async_channel::Sender, - settings: &gio::Settings, - ) { - let default_output = settings.string(settings::AUDIO_OUTPUT_DEVICE); - let mut output_id = 0; - let model = gtk4::StringList::new(&[]); - for (i, name) in audio::Host::devices().iter().enumerate() { - model.append(name); - if name == default_output.as_str() { - output_id = i; - } - } - self.audio_output_combo.set_model(Some(&model)); - self.audio_output_combo.connect_selected_notify( - glib::clone!(@strong sender, @strong model => move |w| { - if let Some(name) = model.string(w.selected()) { - let sender = sender.clone(); - toolkit::utils::send_async_local!(events::Message::AudioOutputChanged(name.to_string()), sender); - } - }), - ); - self.audio_output_combo.set_selected(output_id as u32); - } - /// Setup the dialog pub(crate) fn setup( &self, @@ -64,8 +35,6 @@ impl PreferenceDialog { midi_state: Rc>, settings: &gio::Settings, ) { - self.setup_audio(sender.clone(), settings); - let default_device = settings.string(settings::MIDI_INPUT_DEVICE); let mut default_id = 0; let model = gtk4::StringList::new(&[]); @@ -130,7 +99,6 @@ impl DialogController for PreferenceDialog { get_widget!(builder, adw::PreferencesWindow, dialog); self.dialog = dialog; - get_widget!(builder, adw::ComboRow, audio_output_combo); get_widget!(builder, adw::ComboRow, midi_input_combo); get_widget!(builder, adw::ComboRow, midi_channel_combo); get_widget!(builder, gtk4::Button, open_soundbanks_location); @@ -147,7 +115,6 @@ impl DialogController for PreferenceDialog { let p = crate::soundbanks::DownloadManager::get_soundbank_dir(); open_soundbank_row.set_subtitle(p.to_str().unwrap_or("")); - self.audio_output_combo = audio_output_combo; self.midi_input_combo = midi_input_combo; self.midi_channel_combo = midi_channel_combo; } diff --git a/src/ui/preferences.ui b/src/ui/preferences.ui index dc7ebff..f7d496e 100644 --- a/src/ui/preferences.ui +++ b/src/ui/preferences.ui @@ -1,5 +1,4 @@ - @@ -28,16 +27,6 @@ - - - Audio - - - Audio Output - - - - Soundbanks diff --git a/src/window.rs b/src/window.rs index 699c6e6..05113f7 100644 --- a/src/window.rs +++ b/src/window.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; -use crate::audio_stack::{AudioMessage, AudioStack}; +use crate::audio_stack::AudioStack; use crate::events; use crate::instruments; use crate::instruments::{Preset, FACTORY_PRESETS}; @@ -110,9 +110,6 @@ impl WindowController { } } - let audio_output = win.audio_output().expect("No audio output widget"); - obj.populate_audio(&audio_output); - obj } @@ -191,27 +188,6 @@ impl WindowController { instruments.set_active_id(Some(selected.as_str())); } - fn populate_audio(&self, audio_output: >k4::Label) { - let settings = self.w.settings(); - - settings.connect_changed( - Some(settings::AUDIO_OUTPUT_DEVICE), - glib::clone!(@strong self.audio_stack as audio_stack, @strong audio_output => move |settings, detail| { - let device = settings.string(detail); - print_on_err!(audio_stack - .sender - .send(AudioMessage::SelectAudioOutput(device.to_string()))); - audio_output.set_label(&device); - }), - ); - let device = settings.string(settings::AUDIO_OUTPUT_DEVICE); - print_on_err!(self - .audio_stack - .sender - .send(AudioMessage::SelectAudioOutput(device.to_string()))); - audio_output.set_label(&device); - } - /// Populate the MIDI UI and connect to the settings for update. fn populate_midi(&self, midi_device: >k4::Label, midi_channel: >k4::Label) { let settings = self.w.settings(); @@ -269,14 +245,6 @@ impl WindowController { .send(MidiControlMessage::FilterChannel(ch))); } - /// The audio output device changed - pub fn audio_output_changed(&self, device: &str) { - print_on_err!(self - .w - .settings() - .set_string(settings::AUDIO_OUTPUT_DEVICE, device)); - } - /// The `instrument` was selected (from `events::Message::InstrumentSelected`) pub fn instrument_selected(&self, instrument: &str) { print_on_err!(self @@ -392,10 +360,6 @@ impl Window { self.imp().widgets().map(|w| w.midi_channel.clone()) } - pub fn audio_output(&self) -> Option { - self.imp().widgets().map(|w| w.audio_output.clone()) - } - /// Get the [`Settings`][gio::Settings] instance for the window. pub fn settings(&self) -> &gio::Settings { self.imp().settings.get().expect("Could not get settings.") @@ -459,7 +423,6 @@ mod imp { pub instrument_info_popover: InstrumentInfoPopover, pub midi_device: gtk4::Label, pub midi_channel: gtk4::Label, - pub audio_output: gtk4::Label, pub piano: PianoWidget, toast_overlay: adw::ToastOverlay, } @@ -540,7 +503,6 @@ mod imp { get_widget!(builder, gtk4::ComboBoxText, instruments_combo); get_widget!(builder, gtk4::Label, midi_device); get_widget!(builder, gtk4::Label, midi_channel); - get_widget!(builder, gtk4::Label, audio_output); get_widget!(builder, adw::ToastOverlay, toast_overlay); get_widget!(builder, PianoWidget, piano); @@ -551,7 +513,6 @@ mod imp { instrument_info_popover, midi_device, midi_channel, - audio_output, piano, toast_overlay, //_receiver_connection: receiver_connection, diff --git a/src/window.ui b/src/window.ui index 29a904a..33788bd 100644 --- a/src/window.ui +++ b/src/window.ui @@ -113,26 +113,6 @@ - - - 6 - 6 - audio-card-symbolic - Audio output device - - - - - 6 - 6 - True - [None] - 0.0 - - - - - dock -- GitLab From 0d7eb38780bcc3cd95e9dfff4c6a85282da84425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Tue, 7 Nov 2023 00:26:39 -0500 Subject: [PATCH 4/6] utils: print_on_err! now print errors with Debug --- src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 155f83b..e75bc5f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,7 +7,7 @@ macro_rules! print_on_err { ($e:expr) => { if let Err(err) = $e { log::error!( - "{}:{} Error '{}': {}", + "{}:{} Error '{}': {:?}", file!(), line!(), stringify!($e), -- GitLab From ea7d447157ac219d27599403aea7fd2bb3f9d5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Tue, 7 Nov 2023 00:28:07 -0500 Subject: [PATCH 5/6] doc: fix typos --- doc/instruments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/instruments.md b/doc/instruments.md index bf1db03..e76988e 100644 --- a/doc/instruments.md +++ b/doc/instruments.md @@ -20,8 +20,8 @@ Depending on the value of `synth`, `params` is needed. Synths ------ -* `qwertone` is a simple sine wave synth. It has been lifted out of the Rust -application _qwertone_. There is not parameter t set. +* `qwertone` is a simple sine wave synth. It has been lifted out of the Rust +application _qwertone_. There is not parameter to set. * `fluidlite` is a SoundFont sample syhnthesizer using _fluidlite_. Some params are required: -- GitLab From 27f8f3b263dfac8096a05a444084b7742559c36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Sun, 12 Nov 2023 08:45:11 -0500 Subject: [PATCH 6/6] flatpak: Added llvm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This is needed for https://gitlab.gnome.org/hub/compiano/-/issues/4 Signed-off-by: Hubert Figuière --- net.figuiere.compiano.json | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/net.figuiere.compiano.json b/net.figuiere.compiano.json index 30195e3..af1174f 100644 --- a/net.figuiere.compiano.json +++ b/net.figuiere.compiano.json @@ -1,13 +1,14 @@ { - "app-id" : "net.figuiere.compiano", - "runtime" : "org.gnome.Platform", + "app-id": "net.figuiere.compiano", + "runtime": "org.gnome.Platform", "runtime-version": "master", - "sdk" : "org.gnome.Sdk", - "sdk-extensions" : [ + "sdk": "org.gnome.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.llvm16", "org.freedesktop.Sdk.Extension.rust-stable" ], - "command" : "compiano", - "finish-args" : [ + "command": "compiano", + "finish-args": [ "--share=network", "--share=ipc", "--socket=fallback-x11", @@ -28,7 +29,8 @@ } }, "build-options" : { - "append-path" : "/usr/lib/sdk/rust-stable/bin", + "append-path" : "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm16/bin", + "prepend-ld-library-path": "/usr/lib/sdk/llvm16/lib", "build-args" : [ "--share=network" ], -- GitLab