45 Commits

Author SHA1 Message Date
ba5284d121 restructure into modules, etc. 2022-01-11 18:34:15 +00:00
cd41400b6f restructure into modules 2021-04-27 10:16:25 +01:00
fb7d76bebf better button label 2021-04-27 09:20:15 +01:00
61b1f45ce3 spelling 2021-04-27 08:59:11 +01:00
c9a5eada5e updated build/deploy scripts 2021-04-27 08:44:15 +01:00
ca779f8839 make this conditional - don't need to execute this on linux for example 2021-04-27 08:43:30 +01:00
908ca1d769 stop cpal from crashing on windows! 2021-04-26 22:00:43 +01:00
7db845e03e avoid parsing config file as a sound 2021-04-26 22:00:30 +01:00
fa7361d20f change name of sounds dir function 2021-04-26 21:59:24 +01:00
3aad33189c windows cross-compilation script 2021-04-26 21:58:48 +01:00
6ac967c6d9 try to fix paths for windows 2021-04-26 21:58:11 +01:00
005c90adb0 more sound ideas 2021-04-26 21:57:03 +01:00
5b2688e091 version bump 2021-04-20 10:32:14 +01:00
9f626b8265 remove debug print 2021-04-20 10:31:50 +01:00
2b2ac66708 remove/ignore some things 2021-04-10 12:22:34 +01:00
4fd83be754 save and restore window size and position 2021-04-09 18:46:23 +01:00
3e01249ecf added fireplace sound 2020-11-20 15:58:27 +00:00
5f42e1a03c bump for new UI 2020-10-27 17:31:00 +00:00
06960b6f9c better build/deployment 2020-10-27 17:27:16 +00:00
7e045c0a8d don't use tar.gz as it breaks the itch desktop version 2020-10-27 16:58:55 +00:00
0af09fcd96 tidyup 2020-10-27 16:53:39 +00:00
eba133090a do both tar and zip 2020-10-27 16:52:51 +00:00
f4805e94b0 do both tar and zip 2020-10-27 16:52:06 +00:00
51c636e572 add license to package 2020-10-27 15:16:06 +00:00
982f1f0f21 take todo out of readme 2020-10-27 15:15:09 +00:00
459f1705c0 use tar.gz to preserve permissions 2020-10-27 15:13:00 +00:00
d46e1f48e2 +x 2020-10-27 08:08:11 +00:00
4f821fccd5 change styling 2020-10-27 08:08:03 +00:00
4f58c1eece update sounds todo 2020-10-20 22:23:25 +01:00
e3087bfa63 add repository field 2020-10-20 22:18:55 +01:00
3255fe5d47 version bump 2020-10-20 21:54:08 +01:00
ae81570c07 alphabetical order 2020-10-20 21:36:55 +01:00
6e203c3fbd added sound 2020-10-20 21:33:15 +01:00
0afc497f78 better error popup 2020-10-04 12:28:28 +01:00
b281f8af00 target-specific file manager 2020-10-03 13:17:20 +01:00
183f1064ec todo 2020-09-30 12:47:26 +01:00
c7d94235e9 move readme again 2020-09-26 11:44:32 +01:00
575f0887df update readme 2020-09-26 11:26:45 +01:00
f54853fea2 move audio credits to separate readme 2020-09-26 11:24:40 +01:00
5fe1bcbf2c todo icon 2020-09-24 12:38:37 +01:00
47fd032f90 mark these sounds as done 2020-09-24 12:38:19 +01:00
937c7614a0 MIT license 2020-09-24 10:54:56 +01:00
31deb7929e apparently this prevents a terminal window from popping up when running on windows 2020-09-24 09:49:34 +01:00
b8eb801e49 ignore some more stuff 2020-09-24 08:34:35 +01:00
87315c4d6f version bump and description 2020-09-24 08:34:21 +01:00
15 changed files with 467 additions and 140 deletions

10
.gitignore vendored
View File

@@ -1,2 +1,12 @@
/target /target
/Cargo.lock /Cargo.lock
/.idea/
/sounds/*.flac
/sounds/*.mp3
/sounds/*.wav
/sounds.zip
/lull.zip
/dist/
/package/
/*.kra
/*.png

View File

@@ -1,12 +1,21 @@
[package] [package]
name = "lull" name = "lull"
description = "a catalogue of soothing sounds" description = "a looping sound player for generating atmospheric soundscapes"
version = "0.1.0" version = "1.1.0"
authors = ["Max Bradbury <max@tinybird.info>"] authors = ["Max Bradbury <max@tinybird.info>"]
repository = "https://tinybird.dev/max/lull"
license = "MIT"
edition = "2018" edition = "2018"
[dependencies] [dependencies]
dirs = "^3.0.1" dirs = "^3.0.1"
eframe = "^0.11.0"
egui = "^0.11.0"
gdk = "^0.13.2"
gio = "^0" gio = "^0"
gtk = "^0" gtk = "^0"
orbtk = "^0.2.31"
rodio = "^0.11.0" rodio = "^0.11.0"
serde = "^1.0.125"
serde_derive = "^1.0.125"
toml = "^0.5.8"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Max Bradbury
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.

View File

@@ -1,27 +1,10 @@
# lull # lull
https://ruin.itch.io/lull
a looping sound player. a looping sound player.
add your own favourite noises and blend them to create your ideal ambience. add your own favourite noises and blend them to create your ideal ambience.
## to do created by [Max Bradbury](mailto:max@tinybird.info).
* save volume preferences to disk released under the MIT license.
* cross-compile to Windows?
* watch data dir for new sounds?
* disown file manager subcommand
* get some good nature sounds
* rain on tin roof
* campfire
* wind
* waves
* tape hiss
* white noise?
* fan
## audio credits
*rain under parasol* and *waterfall* by [Samuel Strågefors](https://freesound.org/people/straget/)
*waves* by [Florian Reichelt](https://freesound.org/people/florianreichelt/)
*campfire* by [sagetyrtle](https://freesound.org/people/sagetyrtle/)

23
SOUNDS.md Normal file
View File

@@ -0,0 +1,23 @@
# lull - basic sounds pack
## installation
1. open *lull*
2. click "manage sounds" and the sounds folder will open in your file manager.
3. extract these sounds to the sounds folder
you can skip/remove any unwanted sounds, and add your own.
## credits
*waterfall* and *rain on parasol* by [Samuel Strågefors](https://freesound.org/people/straget/)
*waves* by [Florian Reichelt](https://freesound.org/people/florianreichelt/)
*campfire* by [sagetyrtle](https://freesound.org/people/sagetyrtle/)
*rain on glass* by [Benboncan](https://freesound.org/people/Benboncan/)
*birdsong* by [reinsamba](https://freesound.org/people/reinsamba/)
*fireplace* by [aunrea](https://freesound.org/people/aunrea/)

17
TODO.md Normal file
View File

@@ -0,0 +1,17 @@
# to do
* save volume preferences to disk
* cross-compile to Windows?
* watch data dir for new sounds?
* disown file manager subcommand
* get some good nature sounds
* wind
* tape hiss
* vinyl crackle
* white noise?
* fan
* cat purr
* café
* rustling leaves
* set a window icon
* create a nice icon?

19
build.sh Normal file → Executable file
View File

@@ -1,9 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
cargo build --release mkdir dist
cp target/release/lull .
strip lull
zip -r lull.zip README.md lull
rm lull
zip -r sounds.zip README.md sounds/*.mp3 cargo build --release
mkdir dist/linux
cp target/release/lull dist/linux
cp README.md dist/linux
cp LICENSE dist/linux
strip dist/linux/lull
mkdir dist/sounds
cp sounds/*.mp3 SOUNDS.md dist/sounds

5
deploy.sh Normal file → Executable file
View File

@@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
butler push lull.zip ruin/lull:linux butler push dist/linux ruin/lull:linux
butler push sounds.zip ruin/lull:sounds butler push dist/windows ruin/lull:windows
butler push dist/sounds ruin/lull:sounds

59
src/config.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::fs::File;
use std::io::{Write, Read};
use serde_derive::{Serialize, Deserialize};
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct Config {
pub(crate) position: (i32, i32),
pub(crate) size: (i32, i32),
}
pub(crate) fn save_config(config: Config) {
let mut config_path = dirs::config_dir()
.expect("Couldn't find user config directory");
config_path.push("ruin");
config_path.push("lull");
if !config_path.exists() {
std::fs::create_dir_all(&config_path)
.expect("Couldn't create lull config directory");
}
let toml = toml::to_string(&config)
.expect("Couldn't convert config to toml");
config_path.push("lull.toml");
let mut config_file = File::create(config_path)
.expect("Couldn't create config file");
config_file.write(&toml.into_bytes())
.expect("Couldn't write config file");
}
pub(crate) fn load_config() -> Option<Config> {
let mut config_path = dirs::config_dir()
.expect("Couldn't find user config directory");
config_path.push("ruin");
config_path.push("lull");
config_path.push("lull.toml");
let file = File::open(config_path);
if file.is_err() {
return None;
}
let mut toml = String::new();
file.unwrap().read_to_string(&mut toml)
.expect("Couldn't read config file");
let config = toml::from_str(&toml)
.expect("Couldn't parse config");
Some(config)
}

View File

@@ -1,71 +1,72 @@
use gio::prelude::*; #![windows_subsystem = "windows"]
use gtk::prelude::*;
use gtk::Orientation; mod config;
use rodio::{Sink, Source}; mod ui;
use std::env::args;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf; use std::path::PathBuf;
use std::io::BufReader;
use std::fs::File;
use rodio::{Sink, Source};
use std::process::Command; use std::process::Command;
const SOUNDS_BUTTON_LABEL: &'static str = "open sounds folder";
const SPACING: i32 = 16; const SPACING: i32 = 16;
fn error_popup(message: &str) { #[cfg(target_os = "windows")]
let popup = gtk::Window::new(gtk::WindowType::Toplevel); fn file_manager() -> &'static str {
popup.set_title("error"); "explorer"
popup.set_border_width(SPACING as u32);
popup.set_position(gtk::WindowPosition::Center);
popup.set_default_size(256, 64);
let vertical = gtk::Box::new(Orientation::Vertical, SPACING);
popup.add(&vertical);
let message = gtk::Label::new(Some(message));
vertical.add(&message);
let button_ok = gtk::Button::with_label("OK");
vertical.add(&button_ok);
popup.show_all();
button_ok.connect_clicked(move |_| unsafe {
popup.destroy();
});
} }
fn get_data_dir() -> PathBuf { #[cfg(not(target_os = "windows"))]
fn file_manager() -> &'static str {
"xdg-open"
}
fn get_sounds_dir() -> PathBuf {
let mut data_dir = dirs::data_dir().expect("Couldn't find user data directory"); let mut data_dir = dirs::data_dir().expect("Couldn't find user data directory");
data_dir.push("ruin/lull"); data_dir.push("ruin");
data_dir.push("lull");
// on windows, data dir and config dir are both User\AppData\Roaming
#[cfg(target_os = "windows")] {
data_dir.push("sounds");
}
if !data_dir.exists() { if !data_dir.exists() {
std::fs::create_dir_all(&data_dir).expect("Couldn't create lull data directory"); std::fs::create_dir_all(&data_dir)
.expect("Couldn't create lull sounds directory");
} }
data_dir data_dir
} }
fn build_ui(application: &gtk::Application) { fn open_file_manager() {
let window = gtk::ApplicationWindow::new(application); let mut file_manager = Command::new(file_manager());
file_manager.arg(crate::get_sounds_dir());
file_manager.output().unwrap();
}
window.set_title("lull"); struct Sound {
window.set_border_width(SPACING as u32); name: String,
window.set_position(gtk::WindowPosition::Center); sink: Sink,
window.set_default_size(256, 256); volume: f64,
}
let vertical = gtk::Box::new(Orientation::Vertical, SPACING); fn get_sounds(device: &rodio::Device) -> Vec<Sound> {
vertical.set_homogeneous(true); let mut sounds = Vec::new();
window.add(&vertical); let mut paths = std::fs::read_dir(get_sounds_dir())
.expect("Couldn't read lull sounds directory")
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>()
.expect("Couldn't read files from lull sounds directory");
let device = rodio::default_output_device().unwrap(); paths.sort();
let paths = std::fs::read_dir(get_data_dir())
.expect("Couldn't read from lull data directory");
for path in paths { for path in paths {
let path = path.unwrap().path(); let name = path.file_stem().unwrap().to_str().unwrap().into();
let name: &str = path.file_stem().unwrap().to_str().unwrap();
let file = File::open(&path) let file = File::open(&path)
.expect("Couldn't open audio file"); .expect("Couldn't open audio file");
@@ -74,8 +75,9 @@ fn build_ui(application: &gtk::Application) {
BufReader::new(file) BufReader::new(file)
); );
#[cfg(not(target_os = "windows"))]
if source.is_err() { if source.is_err() {
error_popup(&format!( ui::gtk::error_popup(&format!(
"Couldn't parse file {}. \n{}.", "Couldn't parse file {}. \n{}.",
path.to_str().unwrap(), path.to_str().unwrap(),
source.err().unwrap() source.err().unwrap()
@@ -85,74 +87,26 @@ fn build_ui(application: &gtk::Application) {
let source = source.unwrap().repeat_infinite(); let source = source.unwrap().repeat_infinite();
let sink = Sink::new(&device); let sink = rodio::Sink::new(&device);
sink.append(source); sink.append(source);
sink.pause(); sink.pause();
let row = gtk::Box::new(Orientation::Horizontal, SPACING); sounds.push(Sound { name, sink, volume: 0.0 });
row.set_homogeneous(true);
let label = gtk::Label::new(Some(name));
row.add(&label);
let adjustment = gtk::Adjustment::new(
0.0,
0.0,
1.0,
0.0,
0.0,
0.0
);
let slider = gtk::Scale::new(
Orientation::Horizontal,
Some(&adjustment)
);
slider.set_draw_value(false);
slider.connect_value_changed(move |scale| {
let volume = scale.get_value();
if volume == 0. {
sink.pause();
} else {
sink.play();
sink.set_volume(volume as f32);
}
});
row.add(&slider);
vertical.add(&row);
} }
let row_add = gtk::Box::new(Orientation::Horizontal, SPACING); sounds
row_add.set_homogeneous(true);
let button_manage_sounds = gtk::Button::with_label("manage sounds");
button_manage_sounds.connect_clicked(|_| {
let mut file_manager = Command::new("xdg-open");
file_manager.arg(get_data_dir());
file_manager.output().unwrap();
});
row_add.add(&button_manage_sounds);
vertical.add(&row_add);
window.show_all();
} }
// #[cfg(not(target_os = "windows"))]
fn main() { fn main() {
let application = gtk::Application::new( crate::ui::gtk::instantiate();
Some("dev.tinybird.max.lull"),
Default::default()
).expect("Initialization failed...");
application.connect_activate(|app| {
build_ui(app);
});
application.run(&args().collect::<Vec<_>>());
} }
// #[cfg(target_os = "windows")]
// fn main() {
// eframe::run_native(Box::new(lull::ui::egui::State::default()));
// }
// fn main() {
// crate::ui::orbtk::instantiate();
// }

79
src/ui/egui.rs Normal file
View File

@@ -0,0 +1,79 @@
use eframe::{egui, epi};
pub(crate) use crate::{Sound, get_sounds, open_file_manager};
use egui::{Color32, Stroke, Vec2};
const COLOUR_BG: Color32 = Color32::from_rgb(76, 58, 78);
const COLOUR_FG: Color32 = Color32::from_rgb(156, 143, 109);
pub struct State {
device: rodio::Device,
sounds: Vec<Sound>,
}
impl Default for State {
fn default() -> Self {
let device = rodio::default_output_device().unwrap();
let sounds = get_sounds(&device);
Self { device, sounds }
}
}
impl epi::App for State {
fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
let State { device: _device, sounds} = self;
for sound in sounds.into_iter() {
if sound.volume > 0.0 {
sound.sink.set_volume(sound.volume as f32);
sound.sink.play();
} else {
sound.sink.pause();
}
}
egui::CentralPanel::default().frame(egui::Frame {
margin: Vec2 { x: crate::SPACING as f32, y: crate::SPACING as f32 },
corner_radius: 0.0,
shadow: Default::default(),
fill: COLOUR_BG,
stroke: Stroke {
width: 1.0,
color: COLOUR_FG,
}
}).show(ctx, |ui| {
egui::Grid::new("grid")
.spacing([crate::SPACING as f32; 2])
.show(ui, |ui| {
for sound in sounds {
ui.add(
egui::Label::new(&sound.name).text_color(COLOUR_FG)
);
ui.add(
egui::Slider::new(
&mut sound.volume,
0.0..=1.0
).show_value(false)
);
ui.end_row();
}
if ui.add(
egui::Button::new(crate::SOUNDS_BUTTON_LABEL)
.text_color(COLOUR_FG)
).clicked() {
open_file_manager();
}
ui.end_row();
});
});
}
fn name(&self) -> &str {
"lull"
}
}

130
src/ui/gtk.rs Normal file
View File

@@ -0,0 +1,130 @@
use std::env::args;
use gio::prelude::*;
use gtk::prelude::*;
use gtk::Orientation;
use crate::open_file_manager;
use crate::config::load_config;
pub(crate) fn error_popup(message: &str) {
let popup = gtk::Window::new(gtk::WindowType::Toplevel);
popup.set_title("error");
popup.set_border_width(crate::SPACING as u32);
popup.set_position(gtk::WindowPosition::Center);
popup.set_default_size(256, 64);
popup.set_type_hint(gdk::WindowTypeHint::Dialog);
popup.set_resizable(false);
let message = gtk::Label::new(Some(message));
popup.add(&message);
popup.show_all();
}
fn build_ui(application: &gtk::Application) {
let window = gtk::ApplicationWindow::new(application);
window.set_title("lull");
window.set_border_width(crate::SPACING as u32);
window.set_position(gtk::WindowPosition::Center);
window.set_default_size(256, 128);
if let Some(config) = load_config() {
window.move_(config.position.0, config.position.1);
window.resize(config.size.0, config.size.1);
}
let button_manage_sounds = gtk::Button::with_label(crate::SOUNDS_BUTTON_LABEL);
button_manage_sounds.connect_clicked(|_| {
open_file_manager();
});
let sounds_manage = gtk::Box::new(Orientation::Vertical, crate::SPACING);
let columns = gtk::Box::new(Orientation::Horizontal, crate::SPACING);
let column_labels = gtk::Box::new(Orientation::Vertical, crate::SPACING);
let column_sliders = gtk::Box::new(Orientation::Vertical, crate::SPACING);
columns.set_homogeneous(false);
column_labels.set_homogeneous(true);
column_sliders.set_homogeneous(true);
column_labels.set_property_expand(false);
column_sliders.set_property_expand(true);
column_sliders.set_property_width_request(128);
window.add(&sounds_manage);
sounds_manage.add(&columns);
sounds_manage.add(&button_manage_sounds);
columns.add(&column_labels);
columns.add(&column_sliders);
let device = rodio::default_output_device().unwrap();
let sounds = crate::get_sounds(&device);
for sound in sounds {
let label = gtk::Label::new(Some(sound.name.as_ref()));
label.set_halign(gtk::Align::End);
column_labels.add(&label);
let adjustment = gtk::Adjustment::new(
0.0,
0.0,
1.0,
0.0,
0.0,
0.0
);
let slider = gtk::Scale::new(
Orientation::Horizontal,
Some(&adjustment)
);
slider.set_draw_value(false);
slider.connect_value_changed(move |scale| {
let volume = scale.get_value();
if volume == 0. {
sound.sink.pause();
} else {
sound.sink.play();
sound.sink.set_volume(volume as f32);
}
});
column_sliders.add(&slider);
}
window.show_all();
window.connect_delete_event(|window, _event| {
crate::config::save_config(crate::config::Config {
position: window.get_position(),
size: window.get_size()
});
Inhibit(false)
});
}
pub(crate) fn instantiate() {
#[cfg(target_os = "windows")] {
// instantiate rodio before gtk and don't do anything with it
// this is silly but it's the easiest way to prevent cpal crashing on windows
rodio::default_output_device().unwrap();
}
let application = gtk::Application::new(
Some("dev.tinybird.max.lull"),
Default::default()
).expect("Initialisation failed...");
application.connect_activate(|app| {
build_ui(app);
});
application.run(&args().collect::<Vec<_>>());
}

3
src/ui/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
// pub mod egui;
pub mod gtk;
// pub mod orbtk;

15
src/ui/orbtk.rs Normal file
View File

@@ -0,0 +1,15 @@
use orbtk::{prelude::*, widgets::themes::*};
pub(crate) fn instantiate() {
Application::new()
.window(|ctx| {
Window::new()
.title("OrbTk - showcase example")
.position((100, 100))
.size(1000, 730)
.resizeable(true)
.child(MainView::new().build(ctx))
.build(ctx)
})
.run();
}

16
windows.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_PATH=/usr/i686-w64-mingw32/lib/pkgconfig
rm -rf package
docker start lull-build-3 -ai | less
# gtk icons to keep:
# Adwaita/16x16/ui/window-*
# Adwaita/24x24/ui/window-*
# Adwaita/48x48/ui/window-*
# Adwaita/64x64/ui/window-*
# Adwaita/96x96/ui/window-*
# Adwaita/scalable/ui/window-*
# Adwaita/cursors
# (are all of these sizes necessary?)