1use std::io::Cursor;
2
3use anyhow::Result;
4use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
5
6use widgetry::{EventCtx, GfxCtx, HorizontalAlignment, Outcome, Panel, Toggle, VerticalAlignment};
7
8pub const OUT_OF_GAME: f32 = 0.5;
12pub const IN_GAME: f32 = 1.0;
13
14pub struct Music {
15 inner: Option<Inner>,
16}
17
18impl Default for Music {
19 fn default() -> Music {
20 Music::empty()
21 }
22}
23
24struct Inner {
25 _stream: OutputStream,
27 stream_handle: OutputStreamHandle,
28 sink: Sink,
29 unmuted_volume: f32,
30 current_song: String,
31
32 panel: Panel,
33}
34
35impl Music {
36 pub fn empty() -> Music {
37 Music { inner: None }
38 }
39
40 pub fn start(ctx: &mut EventCtx, play_music: bool, song: &str) -> Music {
41 match Inner::new(ctx, play_music, song) {
42 Ok(inner) => Music { inner: Some(inner) },
43 Err(err) => {
44 error!("No music, sorry: {}", err);
45 Music::empty()
46 }
47 }
48 }
49
50 pub fn event(&mut self, ctx: &mut EventCtx, play_music: &mut bool) {
51 if let Some(ref mut inner) = self.inner {
52 match inner.panel.event(ctx) {
53 Outcome::Clicked(_) => unreachable!(),
54 Outcome::Changed(_) => {
55 if inner.panel.is_checked("play music") {
56 *play_music = true;
57 inner.unmute();
58 } else {
59 *play_music = false;
60 inner.mute();
61 }
62 }
63 _ => {}
64 }
65 }
66 }
67
68 pub fn draw(&self, g: &mut GfxCtx) {
69 if let Some(ref inner) = self.inner {
70 inner.panel.draw(g);
71 }
72 }
73
74 pub fn specify_volume(&mut self, volume: f32) {
75 if let Some(ref mut inner) = self.inner {
76 inner.specify_volume(volume);
77 }
78 }
79
80 pub fn change_song(&mut self, song: &str) {
81 if let Some(ref mut inner) = self.inner {
82 if let Err(err) = inner.change_song(song) {
83 warn!("Couldn't play {}: {}", song, err);
84 }
85 }
86 }
87}
88
89impl Inner {
90 fn new(ctx: &mut EventCtx, play_music: bool, song: &str) -> Result<Inner> {
91 if cfg!(windows) {
92 bail!("Audio disabled on Windows: https://github.com/a-b-street/abstreet/issues/430");
93 }
94
95 let (stream, stream_handle) = OutputStream::try_default()?;
96 let sink = rodio::Sink::try_new(&stream_handle)?;
97
98 let raw_bytes = Cursor::new(abstio::slurp_file(abstio::path(format!(
99 "system/assets/music/{}.ogg",
100 song
101 )))?);
102 sink.append(Decoder::new_looped(raw_bytes)?);
103 if !play_music {
104 sink.set_volume(0.0);
105 }
106
107 let panel = Panel::new_builder(
108 Toggle::new_widget(
109 play_music,
110 ctx.style()
111 .btn_plain
112 .icon("system/assets/tools/volume_off.svg")
113 .build(ctx, "play music"),
114 ctx.style()
115 .btn_plain
116 .icon("system/assets/tools/volume_on.svg")
117 .build(ctx, "mute music"),
118 )
119 .named("play music")
120 .container(),
121 )
122 .aligned(
123 HorizontalAlignment::LeftInset,
124 VerticalAlignment::BottomInset,
125 )
126 .build(ctx);
127
128 Ok(Inner {
129 _stream: stream,
130 stream_handle,
131 sink,
132 unmuted_volume: 1.0,
133 current_song: song.to_string(),
134 panel,
135 })
136 }
137
138 fn unmute(&mut self) {
139 self.sink.set_volume(self.unmuted_volume);
140 }
141
142 fn mute(&mut self) {
143 self.sink.set_volume(0.0);
144 }
145
146 fn specify_volume(&mut self, volume: f32) {
147 self.unmuted_volume = volume;
148 if self.sink.volume() != 0.0 {
149 self.unmute();
150 }
151 }
152
153 fn change_song(&mut self, song: &str) -> Result<()> {
154 if self.current_song == song {
155 return Ok(());
156 }
157 self.current_song = song.to_string();
158 let old_volume = self.sink.volume();
159
160 self.sink = rodio::Sink::try_new(&self.stream_handle)?;
161 let raw_bytes = Cursor::new(abstio::slurp_file(abstio::path(format!(
162 "system/assets/music/{}.ogg",
163 song
164 )))?);
165 self.sink.append(Decoder::new_looped(raw_bytes)?);
166 self.sink.set_volume(old_volume);
167 Ok(())
168 }
169}